/* ============================================================
   HR Cluster V3 — Main Stylesheet
   Mobile-first, minimal Scandinavian design.
   Organised in sections: Reset → Tokens → Base → Layout →
   Components → Utilities → Login → Admin → Workspace
   ============================================================ */


/* ── 1. Reset & box model ────────────────────────────────────── */

*, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

html {
    font-size: 16px;
    -webkit-text-size-adjust: 100%;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    font-size: 1rem;
    line-height: 1.5;
    color: var(--color-text);
    background: var(--color-bg);
    min-height: 100vh;
}

img, svg { display: block; max-width: 100%; }
a { color: var(--color-accent); text-decoration: none; }
a:hover { text-decoration: underline; }
button, input, select, textarea { font: inherit; }


/* ── 2. Design tokens (CSS variables) ───────────────────────── */

:root {
    /* Colours — core */
    --color-bg:             #f5f5f5;
    --color-surface:        #ffffff;
    --color-white:          #ffffff;
    --color-border:         #e0e0e0;
    --color-border-strong:  #c0c0c0;
    --color-text:           #1a1a1a;
    --color-text-muted:     #666666;
    --color-text-light:     #999999;
    --color-accent:         #1a1a1a;       /* near-black — deliberately understated */
    --color-accent-hover:   #333333;
    --color-accent-subtle:  #f5f6f8;       /* ghost hover, nav active bg */
    --color-danger:         #c0392b;
    --color-warning:        #e67e22;
    --color-success:        #27ae60;
    --color-info:           #2980b9;
    --color-primary:        #ea2527;       /* red accent — competence chips, not main buttons */
    --color-graph-green:    #1d9e75;       /* HR Cluster operational dashboard chart green */

    /* Danger variants */
    --color-danger-hover:       #a93226;
    --color-danger-ghost:       #b42318;
    --color-danger-ghost-hover: #7a271a;
    --color-danger-ghost-bg:    #fef3f2;

    /* Alert / banner backgrounds */
    --color-info-bg:        #eff6ff;
    --color-info-border:    #bfdbfe;
    --color-info-text:      #1e40af;
    --color-success-bg:     #f0fdf4;
    --color-success-border: #bbf7d0;
    --color-success-text:   #14532d;
    --color-warning-bg:     #fffbeb;
    --color-warning-border: #fde68a;
    --color-warning-text:   #92400e;
    --color-danger-bg:      #fef2f2;
    --color-danger-border:  #fecaca;
    --color-danger-text:    #991b1b;

    /* Status colours */
    --color-status-active:    #27ae60;
    --color-status-invited:   #2980b9;
    --color-status-suspended: #e67e22;
    --color-status-removed:   #999999;

    /* Badge / status backgrounds */
    --color-badge-success-bg: #e8f5e9;
    --color-badge-success:    #2e7d32;
    --color-badge-info-bg:    #e3f2fd;
    --color-badge-warning-bg: #fff3e0;
    --color-badge-danger-bg:  #fdecea;
    --color-badge-neutral-bg: #f0f0f0;
    --color-badge-neutral:    #757575;
    --color-badge-dark-bg:    #e8e8e8;
    --color-badge-maint-bg:   #f3e5f5;
    --color-badge-maint:      #7b1fa2;

    /* Flash / toast messages */
    --color-flash-success-bg:     #e8f5e9;
    --color-flash-success-text:   #1b5e20;
    --color-flash-success-border: #a5d6a7;
    --color-flash-error-bg:       #fdecea;
    --color-flash-error-text:     #7f0000;
    --color-flash-error-border:   #ef9a9a;
    --color-flash-warning-bg:     #fff3e0;
    --color-flash-warning-text:   #7f4000;
    --color-flash-warning-border: #ffcc80;
    --color-flash-info-bg:        #e3f2fd;
    --color-flash-info-text:      #0d3c61;
    --color-flash-info-border:    #90caf9;

    /* Sidebar / nav */
    --color-nav-bg:             #f8f8f7;
    --color-nav-surface:        #ffffff;
    --color-nav-border:         #e4e7ed;
    --color-nav-text:           #5a6275;
    --color-lang-bg:            #f2f3f5;
    --color-lang-border:        #e4e7ed;
    --color-lang-text:          #9aa0ad;
    --color-lang-active-border: #dce0e6;
    --color-nav-active-text:    #0f1117;
    --color-avatar-bg:          #111318;

    /* Schedule */
    --color-schedule-warning:      #fbbf24;
    --color-schedule-warning-text: #b45309;
    --color-schedule-urgent:       #ef4444;
    --color-schedule-urgent-text:  #dc2626;
    --color-schedule-partial:      #f59e0b;
    --color-schedule-good:         #22c55e;
    --color-schedule-good-text:    #16a34a;

    /* Spacing */
    --space-xs:   0.25rem;   /*  4px */
    --space-sm:   0.5rem;    /*  8px */
    --space-md:   1rem;      /* 16px */
    --space-lg:   1.5rem;    /* 24px */
    --space-xl:   2rem;      /* 32px */
    --space-2xl:  3rem;      /* 48px */

    /* Type */
    --font-size-sm:   0.875rem;
    --font-size-base: 1rem;
    --font-size-lg:   1.125rem;
    --font-size-xl:   1.25rem;
    --font-size-2xl:  1.5rem;
    --font-size-3xl:  2rem;

    /* Borders */
    --radius-sm:  3px;
    --radius-md:  8px;       /* buttons, cards, inputs, tables */
    --radius-lg:  10px;

    /* Shadows */
    --shadow-sm:  0 1px 3px rgba(0,0,0,0.07);
    --shadow-md:  0 2px 8px rgba(0,0,0,0.10);

    /* Layout */
    --sidebar-width: 220px;
    --topbar-height: 48px;
    --content-max:   none;
}


/* ── 3. Typography ───────────────────────────────────────────── */

h1 { font-size: var(--font-size-2xl); font-weight: 600; letter-spacing: -0.02em; }
h2 { font-size: var(--font-size-xl);  font-weight: 600; }
h3 { font-size: var(--font-size-lg);  font-weight: 600; }
h4 { font-size: var(--font-size-base);font-weight: 600; }

p  { color: var(--color-text); }

.text-muted  { color: var(--color-text-muted); }
.text-light  { color: var(--color-text-light); }
.text-small  { font-size: var(--font-size-sm); }
.text-danger { color: var(--color-danger); }
.text-success{ color: var(--color-success); }


/* ── 4. Layout — Workspace shell ────────────────────────────── */

/* Two-column layout: fixed sidebar + scrollable main */
.app-shell {
    display: flex;
    min-height: 100vh;
    /* clip, not hidden: overflow-x hidden made .app-shell a scroll
       container, which silently killed position:sticky for EVERY
       descendant — including .app-topbar's own sticky, which had
       never actually stuck. clip clips horizontal overflow just as
       hard (harder: no programmatic scroll either) without creating
       a scroll container, so sticky binds to the viewport as
       intended. Found 2026-08-21 while the Help page's sticky TOC
       refused to stick. */
    overflow-x: clip;
}

/* Sidebar navigation */
.app-sidebar {
    width: var(--sidebar-width);
    height: 100vh;
    min-height: 0;
    background: var(--color-surface);
    border-right: 1px solid var(--color-border);
    display: flex;
    flex-direction: column;
    position: fixed;
    top: 0;
    left: 0;
    z-index: 100;
    overflow: hidden;
}

/* Main content area — offset for sidebar */
.app-main {
    margin-left: var(--sidebar-width);
    flex: 1;
    min-height: 100vh;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

/* Top bar inside main */
.app-topbar {
    height: var(--topbar-height);
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
    display: flex;
    align-items: center;
    padding: 0 var(--space-lg);
    gap: var(--space-md);
    position: sticky;
    top: 0;
    z-index: 50;
}

/* Page content wrapper — mobile-first, fluid width */
.app-content {
    flex: 1;
    padding: var(--space-md) 12px;
    width: 100%;
    min-width: 0;
}

/* Hamburger button — hidden on desktop */
.sidebar-toggle {
    display: none;
}

/* Close button and footer links inside nav — hidden on desktop,
   but shown when nav is open (overridden in media query) */
.nav-close,
.nav-footer-links {
    display: none;
}

/* Sidebar logo area */
.sidebar-brand {
    padding: var(--space-lg) var(--space-md);
    border-bottom: 1px solid var(--color-border);
    font-weight: 700;
    font-size: var(--font-size-lg);
    letter-spacing: -0.02em;
    color: var(--color-text);
    display: flex;
    align-items: center;
    gap: var(--space-sm);
}

.sidebar-brand span.account-name {
    font-size: var(--font-size-sm);
    font-weight: 400;
    color: var(--color-text-muted);
    display: block;
}

/* Sidebar nav links — scrollable middle area */
.sidebar-nav {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    padding: var(--space-sm) 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.sidebar-nav a {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: 8px 20px;
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    border-radius: 0;
    margin: 0;
    transition: color 0.15s ease, background 0.15s ease;
    text-decoration: none;
}

.sidebar-nav a:hover {
    color: var(--color-text);
    background: var(--color-accent-subtle);
    text-decoration: none;
}

.sidebar-nav a.active {
    color: var(--color-text);
    font-weight: 500;
    background: var(--color-accent-subtle);
}

.sidebar-nav a.active::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
    background: var(--color-text);
}

/* Sidebar bottom section — anchored at bottom, never overlaps nav */
.sidebar-footer {
    flex: 0 0 auto;
    padding: var(--space-md);
    border-top: 1px solid var(--color-border);
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    background: var(--color-surface);
}

.sidebar-footer a {
    color: var(--color-text-muted);
}

.sidebar-footer a:hover {
    color: var(--color-danger);
    text-decoration: none;
}


/* ── 5. Page header ──────────────────────────────────────────── */

.page-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--space-xl);
    gap: var(--space-md);
    flex-wrap: wrap;
}

.page-header h1 {
    font-size: var(--font-size-2xl);
}

/* Auto-rendered L2 page heading (from _head.php when $page_title_l2 is set) */
.page-heading {
    font-size: var(--font-size-2xl);
    font-weight: 700;
    margin: 0 0 var(--space-lg);
    line-height: 1.2;
}


/* ── 6. Cards ────────────────────────────────────────────────── */

.card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--space-lg);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
}

.card + .card {
    margin-top: var(--space-md);
}

/* Cards wrapped in their own <form> don't match .card + .card (not direct
   siblings). Handle the gap at the form level so multi-form settings pages
   don't visually collide between cards. */
form:has(> .card) + form:has(> .card),
form:has(> .card) + .card,
.card + form:has(> .card) {
    margin-top: var(--space-md);
}

.card-title {
    font-size: var(--font-size-base);
    font-weight: 600;
    margin-bottom: var(--space-md);
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-size: var(--font-size-sm);
}

.card-title .btn {
    text-transform: none;
    letter-spacing: normal;
}

.card-description {
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    margin-bottom: var(--space-md);
}

/* ── Settings sub-navigation (segmented control) ─────────── */
.settings-nav {
    display: inline-flex;
    flex-wrap: wrap;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: 3px;
    gap: 2px;
}

.settings-nav-btn {
    display: inline-block;
    padding: 5px 14px;
    font-size: var(--font-size-sm);
    font-weight: 500;
    letter-spacing: 0.03em;
    color: var(--color-text-muted);
    text-decoration: none;
    border: 1px solid transparent;
    border-radius: calc(var(--radius-lg) - 3px);
    transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}

.settings-nav-btn:hover {
    color: var(--color-text);
    text-decoration: none;
}

.settings-nav-btn-active {
    color: var(--color-text);
    font-weight: 700;
    background: #ebebeb;
    border-color: var(--color-border-strong);
    box-shadow: none;
}

/* ── Module group titles ──────────────────────────────────── */
.module-group {
    margin-bottom: var(--space-lg);
}

.module-group:last-child {
    margin-bottom: 0;
}

.module-group-title {
    font-size: var(--font-size-sm);
    font-weight: 600;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-bottom: var(--space-sm);
    padding-bottom: var(--space-xs);
    border-bottom: 1px solid var(--color-border);
}


/* ── 7. Stat widgets (dashboard) ─────────────────────────────── */

.stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: var(--space-md);
    margin-bottom: var(--space-xl);
}

.stat-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--space-lg);
    text-align: center;
}

a.stat-card {
    display: block;
    text-decoration: none;
    color: inherit;
    transition: border-color 0.15s, box-shadow 0.15s;
}

a.stat-card:hover {
    border-color: var(--color-text-muted);
    box-shadow: 0 2px 8px rgba(0,0,0,0.07);
}

.stat-card .stat-value {
    font-size: var(--font-size-3xl);
    font-weight: 700;
    letter-spacing: -0.03em;
    line-height: 1;
    margin-bottom: var(--space-xs);
}

.stat-card .stat-label {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

.stat-card .stat-hint {
    margin-top: var(--space-xs);
    font-size: 0.75rem;
    color: var(--color-text-muted);
    line-height: 1.3;
}

/* Dashboard two-column grid (Active now + Unstaffed) */
.dash-two-col {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-lg);
    margin-bottom: var(--space-lg);
    align-items: start;
}

.dash-col-card {
    margin-bottom: 0;
}

/* Override .card + .card margin inside the dashboard grid — specificity must beat (0,2,0) */
.dash-two-col .card + .card {
    margin-top: 0;
}

/* ── Dashboard v2 ───────────────────────────────────────────── */

/* KPI stat card — status color variants */
.stat-card.status-warning {
    border-color: var(--color-warning);
    background: color-mix(in srgb, var(--color-warning) 6%, var(--color-surface));
}
.stat-card.status-warning .stat-value {
    color: var(--color-warning);
}
.stat-card.status-danger {
    border-color: var(--color-danger);
    background: color-mix(in srgb, var(--color-danger) 6%, var(--color-surface));
}
.stat-card.status-danger .stat-value {
    color: var(--color-danger);
}

/* Mobile KPI grid: 2 columns on narrow screens */
@media (max-width: 640px) {
    .dash-kpi-grid {
        grid-template-columns: 1fr 1fr;
    }
}

/* …but not at the smallest width we support. Measured at 320px the tiles
   resolve to 140px each, which is where the value and its label stop
   having room. One full-width tile per row reads cleanly instead. 360px
   and up keep the 2-up density.

   COMPOUND SELECTOR ON PURPOSE. The element is class="stat-grid
   dash-kpi-grid", and `@media (max-width: 768px) { .stat-grid }` further
   down this file sets repeat(auto-fit, minmax(140px, 1fr)). That has the
   same specificity as a bare `.dash-kpi-grid` and comes LATER, so it
   wins — which is why the plain selector was measured still resolving to
   2 columns at 320px. `.stat-grid.dash-kpi-grid` is 0-2-0 and wins on
   specificity wherever it sits. Do not "simplify" it back to one class.

   Same cascade defeats the 640px rule above: it is inert today (the
   768px .stat-grid rule overrides it at every width it covers), so the
   2-up at 320px came from auto-fit, not from that declaration. Left
   as-is deliberately — giving it specificity would force 2 columns
   across 480-640px where auto-fit currently fits 3, a behaviour change
   nobody asked for.

   359.98px, not 359px. A viewport is not always an integer: measured on
   a devicePixelRatio 1.5 display, matchMedia('(max-width: 359px)') was
   FALSE at a nominal 359px wide viewport, leaving a dead band just under
   360 where the rule silently did not apply. The fractional bound covers
   every width below 360 regardless of scaling. */
@media (max-width: 359.98px) {
    .stat-grid.dash-kpi-grid {
        grid-template-columns: 1fr;
    }
}

/* Section heading with inline date */
.dash-section-title {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    flex-wrap: wrap;
}
.dash-section-date {
    font-weight: 400;
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}
.dash-section-badge {
    font-size: var(--font-size-xs);
    margin-left: auto;
}

/* Shift table */
.dash-table {
    width: 100%;
}
.dash-table th {
    white-space: nowrap;
}

/* Shift row color tinting */
.dash-row-danger  { background: color-mix(in srgb, var(--color-danger)  5%, transparent); }
.dash-row-warning { background: color-mix(in srgb, var(--color-warning) 5%, transparent); }

/* Shift row cells */
.dash-cell-shift {
    display: flex;
    align-items: flex-start;
    gap: var(--space-sm);
    min-width: 160px;
}
.dash-shift-title { font-weight: 500; }
.dash-shift-meta  {
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
    margin-top: 1px;
}
.dash-shift-meta.status-danger  { color: var(--color-danger); }
.dash-shift-meta.status-warning { color: var(--color-warning); }

/* Dot indicator */
.dash-dot {
    flex-shrink: 0;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-top: 5px;
    background: var(--color-border);
}
.dash-dot--danger  { background: var(--color-danger); }
.dash-dot--warning { background: var(--color-warning); }
.dash-dot--success { background: var(--color-success); }
.dash-dot--info    { background: var(--color-primary); }
.dash-dot--neutral { background: var(--color-border); }

/* Coverage badge */
.dash-coverage-badge {
    display: inline-block;
    font-size: var(--font-size-sm);
    font-weight: 600;
    padding: 2px 8px;
    border-radius: var(--radius-sm);
    background: var(--color-surface-alt, var(--color-bg));
}
.dash-coverage-badge--danger  {
    background: color-mix(in srgb, var(--color-danger)  12%, transparent);
    color: var(--color-danger);
}
.dash-coverage-badge--warning {
    background: color-mix(in srgb, var(--color-warning) 15%, transparent);
    color: var(--color-warning);
}
.dash-coverage-badge--success {
    background: color-mix(in srgb, var(--color-success) 12%, transparent);
    color: var(--color-success);
}
.dash-coverage-badge--info {
    background: color-mix(in srgb, var(--color-primary) 10%, transparent);
    color: var(--color-primary);
}

/* Status utility classes (used on text values) */
.status-danger  { color: var(--color-danger) !important; }
.status-warning { color: var(--color-warning) !important; }
.status-success { color: var(--color-success) !important; }
.status-info    { color: var(--color-primary) !important; }

/* Mobile arrow link */
.dash-mobile-arrow {
    font-size: 1.2rem;
    color: var(--color-text-muted);
    text-decoration: none;
    padding: 0 var(--space-xs);
}

/* Cell alignment helpers */
.dash-cell-time     { white-space: nowrap; }
.dash-cell-coverage { text-align: center; }
.dash-cell-clocked  { text-align: center; }
.dash-cell-actions  { text-align: right; white-space: nowrap; }

/* Mobile: table rows become cards */
@media (max-width: 640px) {
    .dash-table thead { display: none; }

    .dash-table tbody tr {
        display: block;
        border: 1px solid var(--color-border);
        border-radius: var(--radius-md);
        margin-bottom: var(--space-sm);
        padding: var(--space-sm) var(--space-md);
        background: var(--color-surface);
    }
    .dash-table tbody tr.dash-row-danger  { border-left: 3px solid var(--color-danger); }
    .dash-table tbody tr.dash-row-warning { border-left: 3px solid var(--color-warning); }

    .dash-table tbody td {
        display: inline;
        border: none;
        padding: 0;
    }

    /* Shift name + meta: full-width block */
    .dash-cell-shift {
        display: flex;
        width: 100%;
        margin-bottom: var(--space-xs);
    }

    /* Time + coverage on same line */
    .dash-cell-time::before    { content: ''; }
    .dash-cell-coverage        { display: inline-block; margin-left: var(--space-sm); }
    .dash-cell-clocked         { display: inline-block; margin-left: var(--space-xs); }
    .dash-cell-actions         { display: block; margin-top: var(--space-xs); text-align: left; }

    /* Suppress table-wrap border on mobile — rows have their own borders */
    .table-wrap:has(.dash-table) {
        border: none;
        border-radius: 0;
        background: transparent;
    }

    /* Reset card edge-flush on mobile — rows are self-bordered cards */
    .card .table-wrap:has(.dash-table) {
        margin-left: 0;
        margin-right: 0;
        margin-bottom: 0;
    }
}


/* ── Ops dashboard operational columns ───────────────────────── */
/* My tasks · My departments · Up next — the three cards under the
   Ops KPI cards. Compact stacked lists rather than tables: at ~300px
   a table cannot hold title + time + department + location without
   either wrapping badly or scrolling sideways. */

.dash-three-col {
    display: grid;
    /* min(300px, 100%) is load-bearing: a bare minmax(300px, 1fr)
       lays out a 300px track inside a ~296px container at 320px
       viewport and scrolls the page horizontally. Clamping the
       minimum to the container makes the single-column case exact. */
    grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
    gap: var(--space-lg);
    margin-bottom: var(--space-lg);
    align-items: start;
}

/* Beat `.card + .card { margin-top }` inside the grid (0,2,0) — same
   reason .dash-two-col carries this. */
.dash-three-col .card + .card { margin-top: 0; }
.dash-ops-card { margin-bottom: 0; }

/* Pill bar wrapper. Spacing lives HERE, never inline on the nav:
   main.js moves .settings-nav into a scroll viewport, and an inline
   margin would be trapped inside it and inflate the shell. */
.dash-ops-pills { margin-bottom: var(--space-sm); }

.dash-ops-list { display: flex; flex-direction: column; }

.dash-ops-row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-sm);
    /* Negative inline margin lets the overdue tint and the row rule
       reach the card's inner edge instead of floating in a gutter. */
    padding: 7px 8px;
    margin: 0 -8px;
    border-top: 1px solid var(--color-border);
}
.dash-ops-list > .dash-ops-row:first-child { border-top: none; }

.dash-ops-row .dash-dot { margin-top: 6px; }

/* min-width:0 is what allows the ellipsis below to engage — without
   it the flex item refuses to shrink past its content. */
.dash-ops-row-body { flex: 1 1 auto; min-width: 0; }

.dash-ops-row-title,
.dash-ops-row-meta {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.dash-ops-row-title {
    font-size: var(--font-size-sm);
    font-weight: 500;
}
/* 0.75rem literal, not var(--font-size-xs): that token is referenced
   in ~10 places in this file but is not defined in :root, so it
   resolves to nothing and the rule silently inherits. .stat-hint above
   uses the literal for the same reason. */
.dash-ops-row-meta {
    font-size: 0.75rem;
    color: var(--color-text-muted);
    margin-top: 1px;
}
.dash-ops-row-status {
    flex: 0 0 auto;
    padding-top: 1px;
    font-size: 0.75rem;
    color: var(--color-text-muted);
    white-space: nowrap;
}

/* Overdue — the state the operator must not miss on a triage card:
     · the fill Production Plan already uses for an overdue row
       (--color-danger-ghost-bg, see .pp-row-overdue)
     · the red dot in the row's leading slot
     · a red "Overdue" label in the status slot
   Deliberately no left accent border — PP marks overdue with the
   ghost fill alone, and the border shifted the row's geometry. */
.dash-ops-row--overdue {
    background: var(--color-danger-ghost-bg);
}
.dash-ops-row--overdue .dash-ops-row-meta,
.dash-ops-row--overdue .dash-ops-row-status {
    color: var(--color-danger);
}
.dash-ops-row--overdue .dash-ops-row-status { font-weight: 600; }

/* Quiet context under a card heading — the single-department name.
   Deliberately not a control: no border, no hover, no affordance. */
.dash-ops-cardmeta {
    margin: -2px 0 var(--space-sm);
    font-size: 0.75rem;
    color: var(--color-text-muted);
}

/* Latest changes — real transitions that already happened. Quieter
   than upcoming work so history cannot dominate the card. */
.dash-ops-row--past .dash-ops-row-title {
    font-weight: 400;
    color: var(--color-text-muted);
}

/* Temporal group heading — Overdue / Today / Tomorrow / Later, and
   "Latest changes". The same marker on all three cards, so the
   operator learns one shape and reads every card with it. */
.dash-ops-sublabel {
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-text-muted);
    margin-bottom: 2px;
}

/* Space between groups, but never above the first one — the card
   title already provides that. :first-child would not survive the
   group order changing, so key off the preceding element instead.

   The gap is what separates one temporal group from the next, and
   it carries the whole hierarchy on its own: there is deliberately
   no box, background or divider here. At 8px the heading sat too
   close to the preceding group — read against the empty Today
   state, "TODAY / No tasks today / LATER" ran together as three
   similar-weight lines. 16px is enough to group them without
   turning the card into a stack of panels. */
.dash-ops-list + .dash-ops-sublabel,
.dash-ops-empty + .dash-ops-sublabel {
    margin-top: var(--space-md);
}

.dash-ops-sublabel--danger { color: var(--color-danger); }

.dash-ops-foot {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-sm);
    margin-top: var(--space-sm);
    padding-top: var(--space-sm);
    border-top: 1px solid var(--color-border);
    font-size: var(--font-size-sm);
}
.dash-ops-foot .text-muted { font-size: 0.75rem; }

.dash-ops-empty { margin: 0; }

/* Mobile: one column, matching .dash-two-col. auto-fit would still
   fit two ~300px tracks at 640px, which is too tight to scan. */
@media (max-width: 640px) {
    .dash-three-col { grid-template-columns: 1fr; }
}


/* ── 8. Tables ───────────────────────────────────────────────── */

.table-wrap {
    overflow-x: auto;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
}

/* Inside a card the card already provides border + background.
   Pull the table flush to the card edges so borders connect cleanly. */
.card .table-wrap {
    border: none;
    border-radius: 0;
    background: none;
    margin-left: calc(-1 * var(--space-lg));
    margin-right: calc(-1 * var(--space-lg));
}
.card .table-wrap:last-child {
    margin-bottom: calc(-1 * var(--space-lg));
}
.card .table-wrap table thead th:first-child,
.card .table-wrap table tbody td:first-child {
    padding-left: var(--space-lg);
}
.card .table-wrap table thead th:last-child,
.card .table-wrap table tbody td:last-child {
    padding-right: var(--space-lg);
}

table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--font-size-sm);
}

thead th {
    padding: var(--space-sm) var(--space-md);
    text-align: left;
    font-weight: 600;
    color: var(--color-text-muted);
    border-bottom: 1px solid var(--color-border);
    white-space: nowrap;
    background: var(--color-bg);
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

tbody td {
    padding: var(--space-sm) var(--space-md);
    border-bottom: 1px solid var(--color-border);
    vertical-align: middle;
}

/* Action column — last th/td right-aligned by default */
thead th:last-child { text-align: right; }
tbody td:last-child  { text-align: right; white-space: nowrap; }

tbody tr:last-child td { border-bottom: none; }

tbody tr:hover { background: var(--color-bg); }


/* ── 9. Badges / status pills ───────────────────────────────── */

.badge {
    display: inline-block;
    padding: 2px var(--space-sm);
    border-radius: 100px;
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    white-space: nowrap;
}

/* Two or more badges under a name or title. Badges are nowrap, so a
   plain block lets them wrap onto the next line with no vertical gap
   and the pills touch on a phone; a wrapping flex row keeps the gap in
   both axes. gap is written as two values on purpose (row, column). */
.badge-row {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 6px;
    margin-top: 3px;
}

.badge-active    { background: var(--color-badge-success-bg); color: var(--color-status-active); }
.badge-invited   { background: var(--color-badge-info-bg); color: var(--color-status-invited); }
.badge-suspended { background: var(--color-badge-warning-bg); color: var(--color-status-suspended); }
.badge-removed   { background: var(--color-bg); color: var(--color-status-removed); }
.badge-danger    { background: var(--color-badge-danger-bg); color: var(--color-danger); }
.badge-pending_review { background: var(--color-badge-warning-bg); color: var(--color-warning); }
.badge-draft     { background: var(--color-badge-neutral-bg); color: var(--color-badge-neutral); }
.badge-archived  { background: var(--color-badge-neutral-bg); color: var(--color-badge-neutral); }
.badge-success     { background: var(--color-badge-success-bg); color: var(--color-badge-success); }
.badge-warning     { background: var(--color-badge-warning-bg); color: var(--color-warning); }
.badge-info        { background: var(--color-badge-info-bg); color: var(--color-info); }
.badge-secondary   { background: var(--color-badge-neutral-bg); color: var(--color-badge-neutral); }
.badge-dark        { background: var(--color-badge-dark-bg); color: var(--color-accent-hover); }
.badge-maintenance { background: var(--color-badge-maint-bg); color: var(--color-badge-maint); }

/* Supplier category chip — color tint applied inline via the chip helper.
   Falls back to neutral surface when no color is set (or invalid input). */
.supplier-category-chip {
    display: inline-flex;
    align-items: center;
    padding: 2px var(--space-sm);
    border-radius: 100px;
    border: 1px solid var(--color-border);
    background: var(--color-bg);
    color: var(--color-text-muted);
    font-size: 0.75rem;
    font-weight: 500;
    line-height: 1.4;
    white-space: nowrap;
    vertical-align: middle;
}
.supplier-category-chip-row {
    margin-left: var(--space-xs);
}

/* Stacked metadata line under the supplier name in the list view:
   category chip + freshness fact + sub-supplier parent indicator,
   muted and wrapping so dense rows don't shove the action column. */
.suppliers-list-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    margin-top: 4px;
    font-size: 0.78rem;
    color: var(--color-text-muted);
    line-height: 1.4;
}
.suppliers-list-meta .supplier-category-chip-row {
    margin-left: 0;
}


/* ── 10. Buttons ─────────────────────────────────────────────── */

/* A submit button that must read as an inline text link.

   For an action that needs POST semantics - a side effect, a CSRF token -
   but belongs in running prose rather than in an actions row. The signup
   "back to shift selection" control is the case it exists for: it RELEASES
   a held shift reservation, so it must not be a GET a browser could
   prefetch.

   Colour is deliberately NOT set here, so .text-muted or any sibling
   utility keeps control of it; the UA's buttontext only applies when no
   author rule does. Underline behaviour mirrors the base `a` rule. */
.btn-textlink {
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
    text-decoration: none;
}
.btn-textlink:hover { text-decoration: underline; }

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-xs);
    padding: 0 14px;
    border-radius: var(--radius-md);
    font-size: 14px;
    font-weight: 500;
    border: 1px solid transparent;
    cursor: pointer;
    text-decoration: none;
    transition: background 0.15s, color 0.15s, border-color 0.15s;
    white-space: nowrap;
    min-height: 36px;
}
/* REQUIRED - see the note on .flash[hidden]. Buttons are toggled with the
   `hidden` attribute in five places, and the author `display` above made
   every one of them inert: the forms review modal offered Set pending and
   Confirm manually on a NEW submission, which its own ACTIONS_BY_STATE map
   excludes. Do not drop this rule while .btn sets display. */
.btn[hidden] { display: none; }
.btn:focus-visible {
    outline: 2px solid var(--color-text);
    outline-offset: 2px;
}
.btn:active:not(:disabled) {
    transform: scale(0.98);
}

/* Disabled state */
.btn:disabled, .btn[disabled] {
    opacity: 0.5;
    cursor: not-allowed;
    pointer-events: none;
}

/* Primary — dark solid */
.btn-primary {
    background: var(--color-accent);
    color: var(--color-white);
    border-color: var(--color-accent);
}
.btn-primary:hover {
    background: var(--color-accent-hover);
    text-decoration: none;
    color: var(--color-white);
}

/* Secondary — outlined */
.btn-secondary {
    background: transparent;
    color: var(--color-text);
    border-color: var(--color-border-strong);
}
.btn-secondary:hover {
    background: var(--color-bg);
    text-decoration: none;
}

/* Outline-strong — emphasised escalation/secondary path.
   Heavier border + bolder text than .btn-secondary so it reads as a
   distinct affordance without competing with the filled primary.
   Used for: SJA "Report incident" (RA detail Quick Actions). */
.btn-outline-strong {
    background: transparent;
    color: var(--color-text);
    border: 2px solid var(--color-text);
    font-weight: 600;
    /* Compensate the extra 1px border so height matches btn-primary
       in a button row (base .btn is border:1px + min-height:36px). */
    padding: 0 13px;
}
.btn-outline-strong:hover {
    background: var(--color-text);
    color: var(--color-white);
    text-decoration: none;
}

/* Context strip — compact operational orientation panel.
   Used when a workspace flow lands prefilled from another entity
   (currently: workspace incident-create from an SJA). Intentionally
   NOT a card / hero — it should feel like a briefing line, not a
   notice. Tight padding, no border, very subtle tint over the page
   background so the form below remains the dominant focus. */
.inc-context-strip {
    margin: 0 0 var(--space-md);
    padding: 8px 12px;
    background: #f0f0f0;
    border-radius: var(--radius-md);
    font-size: .85rem;
    line-height: 1.5;
}
.inc-context-strip-prefix {
    color: var(--color-text-muted);
    font-size: .7rem;
    text-transform: uppercase;
    letter-spacing: .06em;
    margin-right: 8px;
}
.inc-context-strip-anchor code { font-weight: 600; }
.inc-context-strip-meta {
    color: var(--color-text-muted);
    margin-top: 2px;
}
.inc-context-strip-meta .inc-context-strip-label {
    color: var(--color-text);
}
.inc-context-strip-meta-sep {
    margin: 0 8px;
    color: var(--color-text-muted);
    opacity: .5;
}

/* Danger */
.btn-danger {
    background: var(--color-danger);
    color: var(--color-white);
    border-color: var(--color-danger);
}
.btn-danger:hover {
    background: var(--color-danger-hover);
    text-decoration: none;
    color: var(--color-white);
}

/* Danger outline variant */
.btn-danger-outline {
    background: transparent;
    color: var(--color-danger);
    border-color: var(--color-danger);
}
.btn-danger-outline:hover {
    background: var(--color-danger);
    color: var(--color-white);
    text-decoration: none;
}

/* Ghost — minimal, borderless (for table actions) */
.btn-ghost {
    background: transparent;
    border-color: transparent;
    color: var(--color-text-muted);
}
.btn-ghost:hover {
    background: var(--color-accent-subtle);
    color: var(--color-text);
    text-decoration: none;
}

/* Ghost danger — destructive actions in tables */
.btn-ghost-danger {
    background: transparent;
    border-color: transparent;
    color: var(--color-danger-ghost);
}
.btn-ghost-danger:hover {
    background: var(--color-danger-ghost-bg);
    color: var(--color-danger-ghost-hover);
    text-decoration: none;
}

/* Button group — connected segmented controls */
.btn-group {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 0;
}
.btn-group .btn {
    border-radius: 0;
    margin-left: -1px;
}
.btn-group .btn:first-child {
    border-radius: var(--radius-md) 0 0 var(--radius-md);
    margin-left: 0;
}
.btn-group .btn:last-child {
    border-radius: 0 var(--radius-md) var(--radius-md) 0;
}
.btn-group .btn-primary {
    position: relative;
    z-index: 1;
}

/* Small variant — never use for primary actions */
.btn-sm {
    padding: 0 10px;
    font-size: 0.8rem;
    min-height: 32px;
}

/* Large variant */
.btn-lg {
    padding: 0 24px;
    font-size: 1rem;
    min-height: 44px;
}


/* ── 11. Forms ───────────────────────────────────────────────── */

.form-group {
    margin-bottom: var(--space-md);
}

.form-group label {
    display: block;
    font-size: 0.82rem;
    font-weight: 500;
    margin-bottom: var(--space-xs);
    color: var(--color-text-muted);
}

.label-hint {
    font-weight: 400;
    color: var(--color-text-light);
    margin-left: 4px;
}

.req {
    color: var(--color-danger);
    font-weight: 600;
    margin-left: 2px;
}

.form-control {
    display: block;
    width: 100%;
    padding: 6px 10px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    font-size: var(--font-size-base);
    color: var(--color-text);
    background: var(--color-white);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
    min-height: 38px;
}

.form-control:focus {
    outline: none;
    border-color: var(--color-text);
    box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05);
}

.form-control:disabled {
    background: var(--color-accent-subtle);
    opacity: 0.7;
}

.form-control::placeholder { color: var(--color-text-light); }

.form-control.is-invalid {
    border-color: var(--color-danger);
    background: var(--color-danger-bg);
}
.form-control.is-invalid:focus {
    border-color: var(--color-danger);
}

.form-control-static {
    padding: var(--space-sm) 0;
    font-size: var(--font-size-base);
    color: var(--color-text);
    margin: 0;
}

.form-hint {
    margin-top: var(--space-xs);
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

.form-hint-inline {
    font-size: 0.78rem;
    font-weight: 400;
    color: var(--color-text-muted);
    margin-left: 4px;
}

/* ── Radio group ──────────────────────────────────────────────── */
.radio-group {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    margin-top: var(--space-xs);
}
.radio-label {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    font-size: var(--font-size-base);
    font-weight: 400;
    color: var(--color-text);
    cursor: pointer;
}
.radio-label input[type="radio"] {
    margin: 0;
    flex-shrink: 0;
    accent-color: var(--color-text);
    width: 16px;
    height: 16px;
    cursor: pointer;
}

/* ── Checkbox label ──────────────────────────────────────────── */
.checkbox-label {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    font-size: var(--font-size-base);
    font-weight: 400;
    color: var(--color-text);
    cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
    margin: 0;
    flex-shrink: 0;
    accent-color: var(--color-text);
    width: 16px;
    height: 16px;
    cursor: pointer;
}

/* ── Scope selector (segmented radio tabs for forms) ─────────── */
.scope-selector {
    display: flex;
    gap: var(--space-xs);
    margin-top: var(--space-xs);
}
.scope-selector label {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-sm) var(--space-md);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    cursor: pointer;
    font-size: var(--font-size-sm);
    font-weight: 500;
    color: var(--color-text-muted);
    background: var(--color-white);
    transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
    user-select: none;
    text-align: center;
    min-height: 38px;
}
.scope-selector label:hover {
    border-color: var(--color-text-light);
    color: var(--color-text);
}
.scope-selector label.is-active {
    border-color: var(--color-text);
    color: var(--color-text);
    background: var(--color-bg);
    font-weight: 600;
}
.scope-selector input[type="radio"] {
    display: none;
}

/* ── Filter bar (inline toolbar for pickers) ────────────────── */
.filter-bar {
    display: flex;
    gap: var(--space-sm);
    flex-wrap: wrap;
    padding: var(--space-sm);
    background: var(--color-bg);
    border-radius: var(--radius-md);
    border: 1px solid var(--color-border);
    margin-bottom: var(--space-sm);
}
.filter-bar .form-control {
    flex: 1;
    min-width: 140px;
}
.filter-bar .form-control-auto {
    flex: 0 0 auto;
}

/* ── Picker toolbar (select all / clear) ────────────────────── */
.picker-toolbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: var(--space-xs);
    font-size: var(--font-size-sm);
}
.picker-toolbar-count {
    color: var(--color-text-muted);
}
.picker-toolbar-actions {
    display: flex;
    gap: var(--space-sm);
}

/* ── Linked form row ────────────────────────────────────────── */
.link-form-row {
    display: flex;
    gap: var(--space-sm);
    align-items: center;
    margin-bottom: var(--space-sm);
}
.link-form-row .form-control {
    flex: 1;
}

/* ── Picker list container ──────────────────────────────────── */
.picker-list {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    max-height: 420px;
    overflow-y: auto;
    background: var(--color-surface);
}

/* ── Toggle pill (two-state segmented control) ───────────────── */
/* Core design component — reusable for modules, permissions, flags */
.toggle-label {
    display: inline-flex;
    align-items: center;
    cursor: pointer;
}
.toggle-label input[type="checkbox"] {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}
.toggle-pill {
    display: inline-grid;
    grid-template-columns: 1fr 1fr;
    position: relative;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: 3px;
    flex-shrink: 0;
}
/* Sliding highlight */
.toggle-pill::before {
    content: '';
    position: absolute;
    top: 3px;
    left: 3px;
    width: calc(50% - 3px);
    height: calc(100% - 6px);
    background: var(--color-white);
    border: 1px solid var(--color-border);
    border-radius: calc(var(--radius-lg) - 3px);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
    transition: transform 0.18s ease;
}
/* Slide right when checked */
.toggle-label input[type="checkbox"]:checked ~ .toggle-pill::before {
    transform: translateX(100%);
}
/* Segment labels */
.toggle-pill-off,
.toggle-pill-on {
    position: relative;
    z-index: 1;
    padding: 3px 10px;
    font-size: 0.72rem;
    font-weight: 500;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    line-height: 1.3;
    text-align: center;
    white-space: nowrap;
    transition: color 0.18s ease;
    user-select: none;
    color: var(--color-text-muted);
}
/* Active segment text — matches settings nav active state */
.toggle-label input[type="checkbox"]:not(:checked) ~ .toggle-pill .toggle-pill-off {
    color: var(--color-text);
    font-weight: 600;
}
.toggle-label input[type="checkbox"]:checked ~ .toggle-pill .toggle-pill-on {
    color: var(--color-text);
    font-weight: 600;
}
/* Focus ring */
.toggle-label input[type="checkbox"]:focus-visible ~ .toggle-pill {
    outline: 2px solid var(--color-text);
    outline-offset: 2px;
}
/* Disabled state */
.toggle-label input[type="checkbox"]:disabled ~ .toggle-pill {
    opacity: 0.5;
    cursor: not-allowed;
}
.toggle-label:has(input:disabled) {
    cursor: not-allowed;
}

/* ── iOS-style toggle switch ────────────────────────────────── */
/* Default for all binary on/off settings.                       */
/* Use segmented pill (.toggle-pill) ONLY for meaningful mode    */
/* selection (Manual/Automatic, Draft/Published) — not ON/OFF.  */

.toggle-switch {
    display: inline-flex;
    align-items: center;
    cursor: pointer;
}
.toggle-switch input[type="checkbox"] {
    position: absolute;
    opacity: 0;
    width: 0;
    height: 0;
}
/* Track — OFF = quiet neutral gray, ON = warm dark neutral */
.toggle-switch-track {
    width: 36px;
    height: 20px;
    border-radius: 10px;
    background: #e5e7eb;
    border: 1px solid #d1d5db;
    position: relative;
    flex-shrink: 0;
    margin-left: auto;
    transition: all 0.2s ease;
}
/* Knob — pure white, tactile shadow */
.toggle-switch-track::before {
    content: '';
    position: absolute;
    top: 2px;
    left: 2px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--color-white);
    box-shadow: 0 1px 2px rgba(0,0,0,0.18), 0 0 0 0.5px rgba(0,0,0,0.04);
    transition: all 0.2s ease;
}
/* Checked — warm dark neutral (control, not status) */
.toggle-switch input[type="checkbox"]:checked ~ .toggle-switch-track {
    background: #1f2937;
    border-color: #1f2937;
    box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
}
.toggle-switch input[type="checkbox"]:checked ~ .toggle-switch-track::before {
    transform: translateX(16px);
}
/* Subtle press feedback */
.toggle-switch:active .toggle-switch-track::before {
    transform: scale(1.02);
}
.toggle-switch:active input[type="checkbox"]:checked ~ .toggle-switch-track::before {
    transform: translateX(16px) scale(1.02);
}
/* Focus ring */
.toggle-switch input[type="checkbox"]:focus-visible ~ .toggle-switch-track {
    outline: 2px solid var(--color-text);
    outline-offset: 2px;
}
/* Disabled state — clearly muted */
.toggle-switch input[type="checkbox"]:disabled ~ .toggle-switch-track {
    opacity: 0.4;
    cursor: not-allowed;
}
.toggle-switch:has(input:disabled) {
    cursor: not-allowed;
}

/* Toggle row layout — label left, switch right */
.toggle-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-sm) 0;
    gap: var(--space-md);
}
.toggle-row + .toggle-row {
    border-top: 1px solid var(--color-border);
}
.toggle-row-text {
    flex: 1;
    min-width: 0;
}
.toggle-row-label {
    font-size: 14px;
    font-weight: 500;
    color: var(--color-text);
}
.toggle-row-hint {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    margin-top: 2px;
}

/* ── Module autosave indicator ───────────────────────────────── */
.module-save-indicator {
    display: inline-block;
    margin-left: var(--space-xs);
    font-size: 0.75rem;
    min-width: 16px;
    transition: opacity 0.2s;
}
.module-save-indicator.saving { color: var(--color-text-muted); }
.module-save-indicator.saved  { color: var(--color-schedule-good); }
.module-save-indicator.error  { color: var(--color-danger); }

/* ── Module overview (card grid) ─────────────────────────────── */
.module-overview-section {
    margin-bottom: var(--space-xl);
}
.module-overview-section:last-child {
    margin-bottom: 0;
}

.module-overview-section-title {
    font-size: var(--font-size-sm);
    font-weight: 600;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    margin-bottom: var(--space-md);
    padding-bottom: var(--space-xs);
    border-bottom: 1px solid var(--color-border);
}

.module-overview-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--space-md);
}

.module-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--space-lg);
    display: flex;
    flex-direction: column;
    transition: border-color 0.15s, box-shadow 0.15s;
}

.module-card.module-card-on {
    border-color: var(--color-border-strong);
}

.module-card-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-sm);
    margin-bottom: var(--space-xs);
    min-height: 28px;
}

.module-card-title {
    font-weight: 600;
    color: var(--color-text);
    font-size: var(--font-size-base);
    flex: 1;
    min-width: 0;
    padding-top: 2px;
}
a.module-card-title:hover {
    text-decoration: underline;
}

.module-card-control {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    flex-shrink: 0;
}

.module-card-desc {
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    margin-bottom: var(--space-md);
    flex-grow: 1;
}

.module-card-footer {
    margin-top: auto;
    padding-top: var(--space-sm);
}

/* ── Subpage header (back link → title → optional intro) ──────
   Reusable three-row pattern for settings sub-pages. Used by
   all /settings/modules/{key} config pages; extend to other
   subpage groups as they're built. */
.module-config-header {
    margin-bottom: var(--space-lg);
}

.module-config-back {
    display: inline-block;
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    margin-bottom: var(--space-xs);
}
.module-config-back:hover {
    color: var(--color-text);
}

.module-config-title {
    font-size: var(--font-size-lg);
    font-weight: 600;
    margin: 0;
    line-height: 1.2;
}

.module-config-description {
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    margin: var(--space-xs) 0 0 0;
}

/* ── Input with unit affix ────────────────────────────────────── */
.input-affix-wrap {
    display: flex;
    align-items: center;
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--color-surface);
}
.input-affix-wrap .form-control {
    border: 0;
    border-radius: 0;
    flex: 1;
    min-width: 0;
}
.input-affix-wrap .form-control:focus {
    outline: none;
    border-color: transparent;
}
.input-affix-wrap:focus-within {
    outline: none;
    border-color: var(--color-text);
}
.input-affix-unit {
    padding: 0 var(--space-sm);
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    white-space: nowrap;
    background: var(--color-bg);
    border-left: 1px solid var(--color-border);
    align-self: stretch;
    display: flex;
    align-items: center;
}

/* ── Session summary table ────────────────────────────────────���─ */
.summary-section-label {
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--color-text-muted);
    margin: 0 0 var(--space-xs);
}
.summary-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--font-size-base);
}
.summary-table td {
    padding: 3px 0;
    vertical-align: top;
}
.summary-table td:first-child {
    color: var(--color-text);
    width: 60%;
}
.summary-table td:last-child {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* ── Settings divider ─────────────────────────────────────────── */
.settings-rule-divider {
    border: 0;
    border-top: 1px solid var(--color-border);
    margin: var(--space-md) 0 0;
}

/* ── Narrow select ────────────────────────────────────────────── */
.form-control-narrow {
    width: auto;
    min-width: 160px;
    max-width: 260px;
}

/* ── Form layout helpers ──────────────────────────────────────── */
.form-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
    margin-bottom: var(--space-md);
}
.form-row .form-group {
    flex: 1 1 160px;
    min-width: 120px;
    margin-bottom: 0;
}

/* A .form-row left holding a single field (Project, after the shift
   Group picker was removed). Without this the lone control stretches
   the full width and stops lining up with the two-column rows above. */
.form-row--half > .form-group {
    flex: 0 1 calc(50% - (var(--space-sm) / 2));
}

/* Mobile: stack paired fields into one column */
@media (max-width: 640px) {
    .form-row {
        flex-direction: column;
        gap: var(--space-md);
    }
    .form-row .form-group {
        flex: none !important;
        width: 100% !important;
        min-width: 0;
    }
}

.form-actions {
    margin-top: var(--space-md);
    padding-top: var(--space-md);
    border-top: 1px solid var(--color-border);
    display: flex;
    gap: var(--space-sm);
    align-items: center;
}

/* Split actions row: back/cancel pinned far left, the forward action far
   right. Opt-in, so every plain .form-actions keeps its left-aligned pair.
   A lone forward action still lands right (nothing to push), so the one- and
   two-button steps of the same flow agree instead of swapping sides. */
.form-actions--split {
    justify-content: flex-end;
}
.form-actions--split > .btn-secondary:first-child {
    margin-right: auto;
}
@media (max-width: 480px) {
    /* Side by side, two width:100% buttons shrink to half each and wrap their
       labels. Stack instead, forward action on top — which is what the
       existing `.form-actions .btn { width: 100% }` was already asking for.
       Doubled class because a second, unscoped `.form-actions` block further
       down this file re-declares align-items and would otherwise win. */
    .form-actions.form-actions--split {
        flex-direction: column-reverse;
        align-items: stretch;
    }
}

/* ── Form grid (two-column layout, collapses on mobile) ────── */
.form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-sm);
}
.form-grid .span-full { grid-column: 1 / -1; }
@media (max-width: 480px) {
    .form-grid { grid-template-columns: 1fr; }
    .form-grid .span-full { grid-column: auto; }
}


/* ── Phone country-code + local number group ─────────────────── */
/* Used on every phone field (admin user-edit, self-service profile,
   public registration). Country-code select sits left at fixed width;
   local number flexes to fill the row. */
.phone-group {
    display: flex;
    gap: 6px;
}
.phone-group select.phone-cc {
    width: 110px;
    flex-shrink: 0;
}
.phone-group input { flex: 1; }


/* ── Readonly form fields ────────────────────────────────────── */
/* Locked-not-broken treatment for fields the user is allowed to
   read but not edit (e.g. name/email/ID number on self-service
   profile). Visually distinct from editable inputs but still
   readable; intentional, not error state. */
.form-control[readonly] {
    background-color: var(--color-surface-muted, #f4f4f4);
    color: var(--text-muted, #555);
    cursor: not-allowed;
}
.form-control[readonly]:focus {
    outline: none;
    box-shadow: none;
    border-color: var(--color-border, #ddd);
}


/* ── Multi-date chip picker ──────────────────────────────────── */
.multi-date-picker {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}
.date-add-btn {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    background: none;
    border: 1px dashed var(--color-border-strong);
    color: var(--color-text-muted);
    border-radius: 14px;
    padding: 2px 10px;
    font-size: 0.78rem;
    font-weight: 500;
    cursor: pointer;
    white-space: nowrap;
    transition: border-color 0.15s, color 0.15s;
}
.date-add-btn:hover {
    border-color: var(--color-accent);
    color: var(--color-text);
}
/* Native date input overlaid transparently on top of the "+ Add date" button so a
   tap lands on the OS control directly — reliable on iOS WebKit, where calling
   showPicker() on a hidden/zero-size input is a no-op. */
.date-add-wrap {
    position: relative;
    display: inline-flex;
}
.date-picker-overlay {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    border: 0;
    opacity: 0;
    background: none;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
}
.date-chips {
    display: contents;
}
.date-chip {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    background: var(--color-bg);
    color: var(--color-text);
    border: 1px solid var(--color-border);
    border-radius: 14px;
    padding: 2px 6px 2px 9px;
    font-size: 0.78rem;
    font-weight: 500;
    white-space: nowrap;
}
.date-chip-label {
    line-height: 1.3;
}
.date-chip-remove {
    background: none;
    border: none;
    color: var(--color-text-light);
    cursor: pointer;
    font-size: 0.85rem;
    line-height: 1;
    padding: 0 1px;
    display: flex;
    align-items: center;
    border-radius: 50%;
    transition: color 0.15s;
}
.date-chip-remove:hover {
    color: var(--color-danger);
}

/* ── 12. Flash messages ──────────────────────────────────────── */

.flash {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-md);
    border-radius: var(--radius-sm);
    margin-bottom: var(--space-md);
    font-size: var(--font-size-sm);
    font-weight: 500;
}

/* REQUIRED wherever a flash is toggled with the `hidden` attribute. The UA
   stylesheet's `[hidden] { display: none }` loses to ANY author `display`
   declaration regardless of specificity, so the `display: flex` above
   silently defeated it: the forms review modal's existing-user banner set
   hidden on every non-matching submission and still rendered as an empty
   yellow bar. Do not drop this rule while .flash sets display. */
.flash[hidden] { display: none; }

.flash span { flex: 1; }

.flash-close {
    appearance: none;
    -webkit-appearance: none;
    background: none;
    border: 0;
    box-shadow: none;
    cursor: pointer;
    font-size: 1rem;
    line-height: 1;
    opacity: 0.45;
    padding: 0 2px;
    color: inherit;
    flex-shrink: 0;
    outline: none;
}
.flash-close:hover { opacity: 1; }

.flash-success { background: var(--color-flash-success-bg); color: var(--color-flash-success-text); border: 1px solid var(--color-flash-success-border); }
.flash-error   { background: var(--color-flash-error-bg); color: var(--color-flash-error-text); border: 1px solid var(--color-flash-error-border); }
.flash-warning { background: var(--color-flash-warning-bg); color: var(--color-flash-warning-text); border: 1px solid var(--color-flash-warning-border); }
.flash-info    { background: var(--color-flash-info-bg); color: var(--color-flash-info-text); border: 1px solid var(--color-flash-info-border); }


/* ── 13. Platform notice banner ──────────────────────────────── */

.platform-notices {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 1000;
}

.platform-notice {
    padding: var(--space-sm) var(--space-lg);
    text-align: center;
    font-size: var(--font-size-sm);
    font-weight: 500;
}

.platform-notice-info        { background: var(--color-flash-info-bg); color: var(--color-flash-info-text); }
.platform-notice-warning     { background: var(--color-flash-warning-bg); color: var(--color-flash-warning-text); }
.platform-notice-maintenance { background: var(--color-flash-error-bg); color: var(--color-flash-error-text); }


/* ── 14. Login page ──────────────────────────────────────────── */

/* Login page fills full viewport, footer pinned at bottom */
body.login-page {
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
}

body.login-page .site-footer {
    flex-shrink: 0;
    margin-top: 0;
}

/* Cookie banner is position:fixed — no layout padding needed */

.login-shell {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent; /* particles canvas shows through */
    padding: var(--space-lg);
    position: relative;
    z-index: 1;
}

body.login-page .site-footer {
    position: relative;
    z-index: 1;
    background: transparent;
}

.login-box {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--space-2xl);
    width: 100%;
    max-width: 380px;
}

.login-brand {
    text-align: center;
    margin-bottom: var(--space-xl);
}

.login-logo {
    height: 56px;
    width: auto;
    display: block;
    margin: 0 auto var(--space-sm);
}

.login-brand-name {
    font-size: var(--font-size-xl);
    font-weight: 700;
    letter-spacing: -0.02em;
    margin: 0 0 var(--space-xs);
}

.sidebar-logo {
    height: 20px;
    width: auto;
    display: block;
    margin-bottom: var(--space-xs);
}

.login-brand p {
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    margin-top: var(--space-xs);
}

.login-box .btn-primary {
    width: 100%;
    justify-content: center;
    min-height: 48px;
    font-size: var(--font-size-base);
    margin-top: var(--space-md);
}

/* Login — mobile adjustments */
@media (max-height: 700px) {
    .login-shell { padding: var(--space-md) var(--space-md); }
    .login-box { padding: var(--space-lg) var(--space-lg); }
    .login-brand { margin-bottom: var(--space-md); }
    .login-logo { height: 40px; }
    .login-brand-name { font-size: var(--font-size-lg); }
}


/* ── 15. Workspace picker ────────────────────────────────────── */

/* Dedicated class — must NOT reuse .picker-list, which carries a bordered
   scroll-container style (see line ~1246) intended for the signup shift
   filter picker. Sharing the class framed the workspace cards in an outer
   border, producing a heavy shared line between stacked options. */
.workspace-picker {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    margin-top: var(--space-lg);
    /* Explicitly frame-less — the list itself must never render an outer
       border/box. Borders live only on .picker-item. */
    border: 0;
    border-radius: 0;
    overflow: visible;
    max-height: none;
}

.picker-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-md);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    text-decoration: none;
    color: var(--color-text);
    transition: border-color 0.15s, background 0.15s;
}

.picker-item:hover {
    border-color: var(--color-text);
    background: var(--color-bg);
    text-decoration: none;
}

.picker-item .picker-name {
    font-weight: 600;
}

.picker-item .picker-arrow {
    color: var(--color-text-muted);
    font-size: var(--font-size-lg);
}


/* ── 16. Mobile responsive ───────────────────────────────────── */

/* Hide/show on mobile */
.show-mobile { display: none !important; }

@media (max-width: 768px) {
    .hide-mobile { display: none !important; }
    .show-mobile { display: inline !important; }
}

/* Clickable table rows */
.clickable-row:hover {
    background: var(--color-bg);
}

@media (max-width: 768px) {

    .app-shell {
        flex-direction: column;
    }

    /* Slim top bar */
    .app-sidebar {
        width: 100%;
        height: auto;
        min-height: auto;
        position: relative;
        top: auto;
        left: auto;
        flex-direction: row;
        flex-wrap: nowrap;
        align-items: center;
        justify-content: flex-start;
        gap: 8px;
        border-right: none;
        border-bottom: 1px solid var(--color-border);
        padding: 4px 12px;
        overflow: visible;
        z-index: 200;
    }

    .sidebar-brand {
        display: flex;
        flex: 1 1 auto;
        min-width: 0;
        padding: 0;
        border-bottom: none;
        font-size: var(--font-size-base);
    }

    /* Hamburger button — 44×44 tap target, dense centered icon */
    .sidebar-toggle {
        display: flex;
        align-items: center;
        justify-content: center;
        background: none;
        border: none;
        cursor: pointer;
        color: var(--color-text);
        padding: 0;
        line-height: 1;
        flex: 0 0 44px;
        width: 44px;
        height: 44px;
        margin-right: -8px;
        border-radius: var(--radius-sm, 4px);
        -webkit-tap-highlight-color: transparent;
    }

    .sidebar-toggle:active {
        background: var(--color-accent-subtle);
    }

    .sidebar-toggle .icon-hamburger {
        display: block;
    }

    /* Full-screen overlay menu — top-aligned, scrollable */
    .sidebar-nav {
        display: none;
        position: fixed;
        inset: 0;
        background: var(--color-surface);
        z-index: 999;
        flex-direction: column;
        align-items: stretch;
        justify-content: flex-start;
        gap: 2px;
        padding: 3.5rem var(--space-md) var(--space-xl);
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
    }

    .sidebar-nav.open {
        display: flex;
    }

    /* Tap-friendly nav links — structured list, left-aligned */
    .sidebar-nav a {
        position: relative;
        font-size: 1.1rem;
        font-weight: 500;
        padding: 12px var(--space-md);
        border-radius: 0;
        margin: 0;
        width: 100%;
        text-align: left;
        background: none;
        color: var(--color-text-muted);
        transition: color 0.15s ease, background 0.15s ease;
    }

    .sidebar-nav a:hover {
        color: var(--color-text);
        background: var(--color-accent-subtle);
        text-decoration: none;
    }

    /* Active link — same system as desktop */
    .sidebar-nav a.active {
        color: var(--color-text);
        font-weight: 600;
        background: var(--color-accent-subtle);
    }

    .sidebar-nav a.active::before {
        content: "";
        display: block;
        position: absolute;
        left: 0;
        top: 0;
        bottom: 0;
        width: 3px;
        background: var(--color-text);
    }

    /* Close button — top right */
    .sidebar-nav.open .nav-close,
    .sidebar-nav.open .nav-footer-links {
        display: block;
    }

    .nav-close {
        position: absolute;
        top: 4px;
        right: 4px;
        width: 44px;
        height: 44px;
        display: flex;
        align-items: center;
        justify-content: center;
        background: none;
        border: none;
        font-size: 1.85rem;
        cursor: pointer;
        color: var(--color-text);
        line-height: 1;
        padding: 0;
        z-index: 10;
        border-radius: var(--radius-sm, 4px);
    }

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

    /* Signed-in user info + logout at the bottom — pushed down by flex */
    .nav-footer-links {
        margin-top: auto;
        padding-top: var(--space-lg);
        text-align: center;
        font-size: var(--font-size-sm);
        color: var(--color-text-light);
    }

    .nav-footer-links .nav-user-name {
        display: block;
        margin-bottom: var(--space-xs);
        font-weight: 500;
        color: var(--color-text-muted);
    }

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

    .nav-footer-links a:hover {
        color: var(--color-danger);
    }

    .sidebar-footer {
        display: none;
    }

    .app-main {
        margin-left: 0;
    }

    .app-topbar {
        height: 44px;
        padding: var(--space-xs) var(--space-md);
        gap: var(--space-xs);
    }

    /* Topbar: hide terminal link on mobile, compact lang switcher */
    body.workspace .topbar-terminal-link {
        display: none;
    }

    .lang-btn {
        padding: 3px 6px;
        font-size: 0.6875rem;
    }

    .stat-grid {
        grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    }

    /* Dashboard two-column grid stacks on mobile */
    .dash-two-col {
        grid-template-columns: 1fr;
    }

    .table-wrap {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
    }

    .page-header {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--space-sm);
    }

    /* Modal slides up from bottom on mobile */
    .modal {
        max-width: 100%;
        margin: 0;
        border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    }

    .modal-overlay {
        align-items: flex-end;
        padding: 0;
    }

    /* Modal footer: allow buttons to wrap to a second row instead
       of crowding/clipping at narrow widths. Tighter horizontal
       padding so buttons don't lose tap-area to chrome. */
    .modal-footer {
        flex-wrap: wrap;
        padding: var(--space-md) var(--space-md);
    }
    .modal-footer .btn {
        flex-shrink: 0;
    }
}


/* ── 16b. Content padding breakpoints (mobile-first) ────────── */

@media (min-width: 641px) {
    .app-content {
        padding: var(--space-md) 12px var(--space-lg);
    }
}

@media (min-width: 1025px) {
    .app-content {
        padding: var(--space-md) 12px var(--space-xl);
    }
}

/* ── Outer shell density (history) ─────────────────────────────
   The tight top/side padding above IS the application standard now
   (2026-08-10). It began life as an opt-in modifier,
   .app-content-operational, for command/list surfaces — with a rule
   that detail pages, editors, long forms and settings forms keep a
   roomier shell. That rule died in practice: settings/general (a
   form) opted in, users detail and contracts inherited the flag from
   their dispatchers, vehicles/detail — the canonical review surface —
   rendered tight via vehicles/index. By the time 54 pages had opted
   in, the "default" survived only on the 16 settings/modules
   sub-pages, two power editors and some stubs, and navigating
   Modules ➜ Configure visibly jumped the shell.

   So the modifier's values were folded into .app-content itself and
   the modifier rule deleted. $workspace_content_density and the
   app-content-operational class are still EMITTED by _head.php and
   set by ~54 pages — inert, kept to avoid a 54-file sweep in the
   same change; remove them opportunistically. The print block still
   names the class in its selector lists, which is harmless either
   way.

   Bottom padding still grows with the viewport (space-lg ➜ space-xl):
   the modifier always inherited it, so this preserves the exact
   geometry operational pages already had. Mobile (<641px) was never
   affected by the split and is unchanged.

   Spacing INSIDE cards and forms is unrelated to this — that is
   .card padding, form-group gaps etc., deliberately untouched. */


/* ── 17. Utilities ───────────────────────────────────────────── */

.flex         { display: flex; }
.flex-col     { flex-direction: column; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-sm       { gap: var(--space-sm); }
.gap-md       { gap: var(--space-md); }
.mt-sm        { margin-top: var(--space-sm); }
.mt-md        { margin-top: var(--space-md); }
.mt-lg        { margin-top: var(--space-lg); }
.mb-md        { margin-bottom: var(--space-md); }
.mb-lg        { margin-bottom: var(--space-lg); }
.hidden       { display: none !important; }
.w-full       { width: 100%; }
.text-right   { text-align: right; }
.text-center  { text-align: center; }
.nowrap       { white-space: nowrap; }
.btn-block    { width: 100%; }
.text-danger  { color: var(--color-danger); }
.text-success { color: var(--color-success); }
.text-warning { color: var(--color-warning); }
.text-muted   { color: var(--color-text-muted); }
.gap-lg       { gap: var(--space-lg); }
.mb-sm        { margin-bottom: var(--space-sm); }


/* ── 18. Modal ───────────────────────────────────────────────── */

/* Dark overlay behind the modal */
.modal-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    z-index: 1000;
    align-items: center;
    justify-content: center;
    padding: var(--space-md);
}

/* Show when active */
.modal-overlay.open {
    display: flex;
}

/* Modal box */
.modal {
    background: var(--color-surface);
    border-radius: var(--radius-lg);
    box-shadow: 0 8px 40px rgba(0,0,0,0.18);
    width: 100%;
    max-width: 520px;
    max-height: 90vh;        /* fallback */
    max-height: 90dvh;       /* dynamic viewport — adapts to iOS chrome */
    overflow: hidden;
    position: relative;
    display: flex;
    flex-direction: column;
}

/* Form inside modal participates in flex layout */
.modal > form {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
    overflow: hidden;
}

/* Modal header — fixed, never scrolls */
.modal-header {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-xl) var(--space-xl) var(--space-lg);
    border-bottom: 1px solid var(--color-border);
}

/* Modal body — this part scrolls */
.modal-body {
    flex: 1;
    overflow-y: auto;
    min-height: 0;
    padding: var(--space-lg) var(--space-xl);
    scrollbar-width: thin;
    scrollbar-color: rgba(0,0,0,0.15) transparent;
}

.modal-body::-webkit-scrollbar {
    width: 5px;
}
.modal-body::-webkit-scrollbar-track {
    background: transparent;
}
.modal-body::-webkit-scrollbar-thumb {
    background: rgba(0,0,0,0.15);
    border-radius: 3px;
}

/* Modal footer — fixed, never scrolls */
.modal-footer {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-sm);
    padding: var(--space-md) var(--space-xl);
    border-top: 1px solid var(--color-border);
}

.modal-title {
    font-size: var(--font-size-lg);
    font-weight: 600;
}

/* Close button (×) */
.modal-close {
    background: none;
    border: none;
    font-size: 1.5rem;
    cursor: pointer;
    color: var(--color-text-muted);
    line-height: 1;
    padding: 0 var(--space-xs);
}

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

/* ── Session expiry warning ──────────────────────────────────
   Mirrors .modal-overlay exactly, but is a SEPARATE class on purpose:
   the ModalStack Escape handler in main.js binds to '.modal-overlay',
   and this one must not be dismissible. It carries a live countdown —
   closing it would hide the warning without stopping the clock. There
   is no X; both ways out (renew / log out) are explicit buttons.
   Sits above ordinary modals so it is never hidden behind one. */
.session-expiry-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    z-index: 1100;
    align-items: center;
    justify-content: center;
    padding: var(--space-md);
}

.session-expiry-overlay.open {
    display: flex;
}

/* Tabular figures so the countdown doesn't jitter as digits change */
.session-expiry-countdown {
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* ── Contract source chooser (New contract: template / upload PDF) ──
   Two-card picker used both in the New-contract modal (user Contracts
   tab) and on the standalone /contracts/new choice page. Shared here so
   both entry points render identically. */
.contract-method-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: var(--space-md);
}
.contract-method-card {
    display: block;
    padding: var(--space-lg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    text-decoration: none;
    color: inherit;
    transition: border-color .12s, box-shadow .12s;
}
.contract-method-card:hover {
    border-color: var(--color-text);
    text-decoration: none;   /* override global a:hover underline — border change is enough */
}
.contract-method-icon {
    font-size: 1.75rem;
    line-height: 1;
    margin-bottom: var(--space-sm);
}
.contract-method-title {
    font-weight: 700;
    font-size: 1rem;
    margin-bottom: 4px;
}
.contract-method-desc {
    color: var(--color-text-muted);
    font-size: 0.85rem;
}

/* ── Template add: starting-point source cards ─────────────────
   Card grid under the "copy from your templates" / "global library"
   radios; the search input above each grid filters the cards. */
.tmpl-source-cards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: var(--space-sm);
    margin-top: var(--space-sm);
}
.tmpl-source-card {
    min-width: 0;
    padding: var(--space-md);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    cursor: pointer;
    transition: border-color .12s, box-shadow .12s;
}
.tmpl-source-card:hover {
    border-color: var(--color-text);
}
.tmpl-source-card.is-selected {
    border-color: var(--color-text);
    box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05);
}
.tmpl-source-card-title {
    font-weight: 600;
    font-size: 0.9375rem;
    margin-bottom: 2px;
    overflow-wrap: anywhere;
}
.tmpl-source-card-meta {
    font-size: 0.8125rem;
    color: var(--color-text-muted);
    margin-bottom: var(--space-xs);
}
.tmpl-source-card-sections {
    font-size: 0.8125rem;
    color: var(--color-text-muted);
    line-height: 1.4;
}

/* ── Modal viewport safety on mobile (≤ 768px) ──────────────────
   iOS Safari's URL bar slides in/out as the user scrolls, which
   makes `dvh` jitter mid-interaction. If the overlay is allowed
   to scroll AND the modal is sized in `dvh`, the footer can end
   up below the visible viewport (under the bottom toolbar) the
   moment the URL bar appears.

   Fix: pin the overlay to `100svh` (the SMALLEST viewport — URL
   bar showing) and disable overlay scrolling. The modal panel is
   sized to fit inside that smallest viewport, so the header and
   footer are always visible. Long content scrolls inside the
   `.modal-body` (already `flex:1; overflow-y:auto`), not by
   scrolling the whole modal out of view. Plain `vh` kept as a
   fallback for browsers without `svh`. */
@media (max-width: 768px) {
    .modal-overlay {
        align-items: flex-start;
        padding:
            calc(8px + env(safe-area-inset-top))
            8px
            calc(8px + env(safe-area-inset-bottom));
        min-height: 100vh;
        min-height: 100svh;
        height: 100vh;
        height: 100svh;
        overflow: hidden;
    }
    .modal {
        max-height: calc(100vh  - 16px - env(safe-area-inset-top) - env(safe-area-inset-bottom));
        max-height: calc(100svh - 16px - env(safe-area-inset-top) - env(safe-area-inset-bottom));
    }
}

/* ── Date / time inputs: normalize iOS native rendering ─────────
   iOS Safari renders <input type="date|time|datetime-local"> with
   its own larger control by default, making them visually taller
   and wider than sibling text inputs in the same .form-control
   class. Strip the native appearance so they inherit `.form-control`
   sizing. The native picker still opens on tap — only the in-line
   chrome is removed. */
.form-control[type="date"],
.form-control[type="time"],
.form-control[type="datetime-local"],
.form-control[type="month"],
.form-control[type="week"] {
    -webkit-appearance: none;
    appearance: none;
    line-height: 1.5;
}

/* iOS Safari mobile: even after `appearance:none`, the displayed value
   is rendered inside `::-webkit-date-and-time-value` and wrapped in
   `::-webkit-datetime-edit`, both of which keep iOS-specific defaults
   (centered alignment, extra inner padding, slightly heavier font).
   The result: the field reads visibly bolder/wider than sibling text
   inputs in the same modal column. Force the inner pieces to match
   text-input rendering on mobile only — desktop spin/segment controls
   on macOS Safari are untouched. */
@media (max-width: 768px) {
    .form-control[type="date"]::-webkit-date-and-time-value,
    .form-control[type="time"]::-webkit-date-and-time-value,
    .form-control[type="datetime-local"]::-webkit-date-and-time-value,
    .form-control[type="month"]::-webkit-date-and-time-value,
    .form-control[type="week"]::-webkit-date-and-time-value {
        text-align: left;
        font: inherit;
        margin: 0;
    }
    .form-control[type="date"]::-webkit-datetime-edit,
    .form-control[type="time"]::-webkit-datetime-edit,
    .form-control[type="datetime-local"]::-webkit-datetime-edit,
    .form-control[type="month"]::-webkit-datetime-edit,
    .form-control[type="week"]::-webkit-datetime-edit {
        padding: 0;
    }
    .form-control[type="date"],
    .form-control[type="time"],
    .form-control[type="datetime-local"],
    .form-control[type="month"],
    .form-control[type="week"] {
        text-align: left;
        font-size: var(--font-size-base);
    }
}

/* Close on Escape and clicking overlay */


/* ── 18b. Document viewer modal ──────────────────────────────── */

/* Reusable image/PDF preview modal (layout/components/document_viewer.php).
   Larger than the standard .modal — preview area takes most of the viewport. */

.doc-viewer-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    z-index: 1100;
    align-items: center;
    justify-content: center;
    padding: var(--space-md);
}

.doc-viewer-overlay.open {
    display: flex;
}

.doc-viewer {
    background: var(--color-surface);
    border-radius: var(--radius-lg);
    box-shadow: 0 12px 48px rgba(0, 0, 0, 0.28);
    width: 100%;
    max-width: 960px;
    max-height: calc(100vh - 48px);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* The single scroll region inside the modal. Header above and footer
   below stay fixed; this wrapper takes the remaining space and any
   overflow gets a vertical scrollbar here, so preview + thumbs +
   meta + per-class verify rows all coexist without clipping. */
.doc-viewer-scroll {
    flex: 1 1 auto;
    min-height: 0;            /* allow the flex child to shrink under content */
    overflow-y: auto;
    overflow-x: hidden;
    display: flex;
    flex-direction: column;
}

.doc-viewer-header {
    flex-shrink: 0;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-md);
    padding: var(--space-md) var(--space-lg);
    border-bottom: 1px solid var(--color-border);
}

.doc-viewer-titles {
    min-width: 0;
    flex: 1;
}

.doc-viewer-title {
    font-size: var(--font-size-lg);
    font-weight: 600;
    line-height: 1.2;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.doc-viewer-subtitle {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    margin-top: 2px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.doc-viewer-body {
    flex: 0 0 auto;            /* preview no longer grows to fill the modal */
    height: 42vh;              /* stable preview height across docs */
    min-height: 220px;         /* readable on short viewports */
    background: #1e1e1e;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;          /* image/iframe contain themselves */
    position: relative;
}

.doc-viewer-body.empty,
.doc-viewer-body.no-preview {
    background: var(--color-bg);
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    padding: var(--space-xl);
    text-align: center;
    height: auto;              /* fallback message doesn't need 42vh */
    min-height: 120px;
}

.doc-viewer-image {
    max-width: 100%;
    max-height: 100%;          /* fit within the 42vh preview cap */
    object-fit: contain;
    transition: transform 0.18s ease;
    transform-origin: center center;
    background: #1e1e1e;
}

.doc-viewer-iframe {
    width: 100%;
    height: 100%;              /* fill the 42vh preview cap */
    border: 0;
    background: #fff;
    display: block;
}

.doc-viewer-meta {
    flex-shrink: 0;
    padding: var(--space-sm) var(--space-lg);
    border-top: 1px solid var(--color-border);
    background: var(--color-bg);
    font-size: var(--font-size-sm);
}

.doc-viewer-meta:empty {
    display: none;
}

/* Read-only thumb strip for multi-file bundles. Mirrors the Competence
   modal's thumb strip styling so the two viewers feel uniform, but
   without the per-thumb remove/add affordances — bundle edits live in
   the Competence modal. Horizontal scroll only. */
.doc-viewer-thumbs {
    display: flex;
    gap: var(--space-sm);
    flex-shrink: 0;
    padding: var(--space-sm) var(--space-lg);
    border-top: 1px solid var(--color-border);
    background: var(--color-bg);
    overflow-x: auto;
    overflow-y: hidden;
}

.doc-viewer-thumbs[hidden] { display: none; }

.doc-viewer-thumb {
    flex: 0 0 auto;
    width: 64px;
    height: 64px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    padding: 0;
    cursor: pointer;
    overflow: hidden;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

.doc-viewer-thumb:hover {
    border-color: var(--color-border-strong);
}

.doc-viewer-thumb.active {
    border-color: var(--color-primary);
    box-shadow: 0 0 0 1px var(--color-primary) inset;
}

.doc-viewer-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.doc-viewer-thumb-pdf,
.doc-viewer-thumb-other {
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--color-text-muted);
    letter-spacing: 0.04em;
}

.doc-viewer-meta dl {
    margin: 0;
    display: grid;
    grid-template-columns: minmax(120px, max-content) 1fr;
    column-gap: var(--space-md);
    row-gap: 4px;
}

.doc-viewer-meta dt {
    color: var(--color-text-muted);
    font-weight: 500;
}

.doc-viewer-meta dd {
    margin: 0;
    color: var(--color-text);
    word-break: break-word;
}

/* Per-class verify section — appears between meta and footer when the
   document is backed by one or more user_competence rows. Each row is
   its own approval unit so T1 / T2 / T4 of the same forklift bevis can
   be verified or rejected independently. */
.doc-viewer-classes {
    flex-shrink: 0;
    padding: var(--space-sm) var(--space-lg);
    border-top: 1px solid var(--color-border);
    background: var(--color-bg);
}
.doc-viewer-classes[hidden] { display: none; }
.doc-viewer-classes-title {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    font-weight: 500;
    margin-bottom: 8px;
}
.doc-viewer-classes-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.doc-viewer-class-row {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    padding: 8px 0;
    border-top: 1px dashed var(--color-border);
}
.doc-viewer-class-row:first-child { border-top: 0; padding-top: 0; }
.doc-viewer-class-row .class-label {
    font-weight: 600;
    flex: 0 0 auto;
}
.doc-viewer-class-row .class-state {
    font-size: 0.78rem;
    padding: 2px 8px;
    border-radius: 999px;
    line-height: 1.4;
    flex: 0 0 auto;
}
.doc-viewer-class-row .class-state.is-approved {
    background: #ecfdf5; color: #047857; border: 1px solid #a7f3d0;
}
.doc-viewer-class-row .class-state.is-rejected {
    background: #fef2f2; color: #b91c1c; border: 1px solid #fecaca;
}
.doc-viewer-class-row .class-state.is-pending {
    background: #fffbeb; color: #92400e; border: 1px solid #fde68a;
}
.doc-viewer-class-row .class-actions {
    display: flex;
    gap: 6px;
    margin-left: auto;
    flex-wrap: wrap;
}
.doc-viewer-class-row .class-reason {
    flex: 1 1 100%;
    font-size: 0.82rem;
    color: var(--color-text-muted);
    margin: -2px 0 0;
}
.doc-viewer-class-row .class-reject-panel {
    flex: 1 1 100%;
    display: none;
    flex-wrap: wrap;
    gap: 6px;
    align-items: center;
    padding-top: 4px;
}
.doc-viewer-class-row.is-rejecting .class-actions { display: none; }
.doc-viewer-class-row.is-rejecting .class-reject-panel { display: flex; }
.doc-viewer-class-row .class-reject-panel input[type="text"] {
    flex: 1 1 200px;
    min-width: 0;
}
@media (max-width: 480px) {
    .doc-viewer-class-row .class-actions { width: 100%; margin-left: 0; }
}

.doc-viewer-footer {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-lg);
    border-top: 1px solid var(--color-border);
    background: var(--color-surface);
    flex-wrap: wrap;
}

.doc-viewer-tools,
.doc-viewer-actions {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    flex-wrap: wrap;
}

.doc-viewer-tools:empty {
    display: none;
}

/* Compact action layout for narrow row buttons */
.doc-row-actions {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    flex-wrap: nowrap;
    justify-content: flex-end;
}

.doc-row-actions .btn {
    flex-shrink: 0;
}

@media (max-width: 640px) {
    .doc-viewer-iframe { height: 60vh; }
    .doc-viewer-image { max-height: 60vh; }
    .doc-viewer-meta dl {
        grid-template-columns: 1fr;
        row-gap: 2px;
    }
    .doc-viewer-meta dt {
        margin-top: var(--space-xs);
    }
    .hide-mobile-inline { display: none; }

    /* Document/competence/etc. row action button clusters wrap to a
       second line at narrow widths instead of forcing the parent
       table cell to overflow horizontally. Buttons stay full-size
       and tappable. */
    .doc-row-actions {
        flex-wrap: wrap;
        justify-content: flex-end;
    }
}


/* ── 18c. Competence review modal (unified) ──────────────────── */
/* Replaces the older verify-modal-then-doc-viewer stack. One
   modal: preview on the left (desktop) / top (mobile), controls
   on the right / bottom. */

.comp-review-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    z-index: 1100;
    align-items: center;
    justify-content: center;
    padding: var(--space-md);
}
.comp-review-overlay.open { display: flex; }

.comp-review-shell {
    background: var(--color-surface);
    border-radius: var(--radius-lg);
    box-shadow: 0 12px 48px rgba(0, 0, 0, 0.28);
    width: 100%;
    max-width: 1100px;
    max-height: 92vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.comp-review-header {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    padding: var(--space-md) var(--space-lg);
    border-bottom: 1px solid var(--color-border);
}
.comp-review-title {
    margin: 0;
    font-size: var(--font-size-lg);
    font-weight: 600;
    line-height: 1.2;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.comp-review-main {
    flex: 1 1 auto;
    display: grid;
    grid-template-columns: minmax(0, 1fr) 360px;
    min-height: 0;
}

/* Preview pane wraps the preview + the rotate overlay so the
   overlay can position itself relative to the preview. */
.comp-review-preview-pane {
    position: relative;
    display: flex;
    flex-direction: column;
    background: #1e1e1e;
    min-height: 320px;
    overflow: hidden;
}

.comp-review-preview {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: auto;
}
.comp-review-preview.empty,
.comp-review-preview.no-preview {
    background: var(--color-bg);
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    padding: var(--space-xl);
    text-align: center;
}

/* Drop zone shown when no proof is attached. Replaces the static
   "no file" fallback for managers who can upload. */
.comp-review-preview.dropzone {
    background: var(--color-bg);
    padding: var(--space-xl);
    cursor: pointer;
}
.comp-review-dropzone {
    width: 100%;
    max-width: 420px;
    border: 2px dashed var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--space-xl);
    text-align: center;
    transition: border-color 0.15s, background 0.15s, transform 0.1s;
    background: var(--color-surface);
}
.comp-review-dropzone:hover {
    border-color: var(--color-accent, #888);
    background: var(--color-surface);
}
.comp-review-dropzone.drag-over {
    border-color: var(--color-primary, #111);
    border-style: solid;
    background: var(--color-accent-subtle, var(--color-bg));
    transform: scale(1.02);
}
.comp-review-dropzone-icon {
    font-size: 2rem;
    line-height: 1;
    color: var(--color-text-muted);
    margin-bottom: var(--space-sm);
}
.comp-review-dropzone-title {
    font-weight: 600;
    color: var(--color-text);
    margin-bottom: 4px;
}
.comp-review-dropzone-hint {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}
.comp-review-image {
    max-width: 100%;
    max-height: 70vh;
    object-fit: contain;
    transition: transform 0.18s ease;
    transform-origin: center center;
}

/* Thumbnail strip — multi-file bundle navigation (migration 180).
   Horizontal scroll ONLY: overflow-y is hard-disabled. Strip has a fixed
   height so the preview pane's flex layout stays predictable, and the
   strip itself never shrinks the preview area below its min-height.
   Children (.comp-review-thumb, .comp-review-thumb-add) all carry
   flex: 0 0 auto so they never collapse under horizontal pressure. */
.comp-review-thumbs {
    flex: 0 0 auto;
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: var(--space-sm);
    height: 80px;
    padding: 0 var(--space-sm);
    background: rgba(0, 0, 0, 0.35);
    overflow-x: auto;
    overflow-y: hidden;
    border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.comp-review-thumbs[hidden] { display: none; }
.comp-review-thumb {
    position: relative;
    flex: 0 0 auto;
    flex-shrink: 0;
    width: 64px;
    height: 64px;
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--color-surface);
    border: 2px solid transparent;
    cursor: pointer;
    padding: 0;
}
.comp-review-thumb.active {
    border-color: var(--color-primary, #fff);
}
.comp-review-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.comp-review-thumb-pdf,
.comp-review-thumb-other {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    color: var(--color-text-muted);
    font-size: 0.75rem;
    font-weight: 600;
    text-align: center;
    padding: 4px;
}
.comp-review-thumb-remove {
    position: absolute;
    top: 2px;
    right: 2px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    border: 1px solid rgba(255, 255, 255, 0.6);
    background: rgba(0, 0, 0, 0.78);
    color: #fff;
    cursor: pointer;
    padding: 0;
    z-index: 2;
    /* The button renders an inline SVG cross (set in main.js); the
       SVG is geometrically symmetric so flex centering lands it on
       the box centerline without font-metric games. Always visible
       (slightly faded), keyboard-reachable. */
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0.65;
    transition: opacity 0.12s ease, background 0.12s ease;
}
.comp-review-thumb-remove svg {
    display: block;
}
/* Highlight only when the X itself is hovered or focused — not on the
   thumbnail's general hover. (The X is always rendered at opacity
   0.65 above, so it doesn't need parent-hover to appear.) */
.comp-review-thumb-remove:focus-visible,
.comp-review-thumb-remove:hover {
    opacity: 1;
    background: rgba(220, 53, 69, 0.85);
}
.comp-review-thumb-remove:focus-visible {
    outline: 2px solid #fff;
    outline-offset: 1px;
}
.comp-review-thumb.comp-review-thumb-removing {
    opacity: 0.5;
    pointer-events: none;
}

/* "Delete proof" — destructive action sitting in the action panel
   stack, directly under the verify/save/unverify buttons. The parent
   .comp-review-side already adds 24px of flex gap; we pull up by 8px
   so the destructive group reads as part of the same action stack,
   not floating down in empty space. Dashed border + padding-top mark
   it as a destructive boundary without consuming vertical room.
   JS toggles `hidden` so the zone only renders when a bundle exists. */
.comp-review-danger-zone {
    margin-top: calc(-1 * var(--space-sm));
    padding-top: var(--space-sm);
    border-top: 1px dashed rgba(255, 255, 255, 0.10);
    display: flex;
}
.comp-review-danger-zone[hidden] { display: none; }
/* Full-width to align with the modal's primary verify-action stack. */
.comp-review-delete-btn {
    width: 100%;
    justify-content: center;
}
.comp-review-thumb-add {
    flex: 0 0 auto;
    flex-shrink: 0;
    width: 64px;
    height: 64px;
    border: 2px dashed var(--color-border);
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--color-text-muted);
    font-size: 1.5rem;
    cursor: pointer;
    transition: border-color 0.15s, color 0.15s;
}
.comp-review-thumb-add:disabled {
    opacity: 0.5;
    cursor: progress;
}

/* Loading thumbnail placeholder rendered while an async upload is in
   flight. Replaced/removed when the server response lands. */
.comp-review-thumb-loading {
    flex: 0 0 auto;
    flex-shrink: 0;
    width: 64px;
    height: 64px;
    border-radius: var(--radius-sm);
    background: rgba(255, 255, 255, 0.06);
    color: var(--color-text-muted);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.2rem;
    animation: comp-review-thumb-pulse 1.1s ease-in-out infinite;
}
@keyframes comp-review-thumb-pulse {
    0%, 100% { opacity: 0.55; }
    50%      { opacity: 1; }
}

/* file-queue.js preview — selected-files list shown beneath any
   <input type="file" multiple>. Used by the public competence
   request page (mobile camera capture) and the workspace upload
   forms. Single set of styles, dual surface. */
.comp-file-queue {
    margin-top: 6px;
    font-size: 0.82rem;
}
.comp-file-queue-head {
    color: var(--color-text-muted, #6b7280);
    margin-bottom: 4px;
}
.comp-file-queue-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.comp-file-queue-list li {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 4px 6px;
    background: var(--color-surface, #fff);
    border: 1px solid var(--color-border, #e5e5e5);
    border-radius: 4px;
}
.comp-file-queue-name {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.comp-file-queue-remove {
    flex: 0 0 auto;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    border: 1px solid rgba(0, 0, 0, 0.18);
    background: rgba(255, 255, 255, 0.95);
    color: #b00020;
    cursor: pointer;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.12s ease, color 0.12s ease;
}
.comp-file-queue-remove:hover,
.comp-file-queue-remove:focus-visible {
    background: #b00020;
    color: #fff;
    outline: none;
}
.comp-file-queue-remove svg {
    display: block;
    pointer-events: none;
}

/* Inline upload error banner shown above the preview pane when an
   async modal "+" upload fails. Auto-cleared on next open or success. */
.comp-review-upload-error {
    flex: 0 0 auto;
    padding: 8px var(--space-md);
    background: rgba(220, 53, 69, 0.18);
    color: #f5b5b9;
    border-bottom: 1px solid rgba(220, 53, 69, 0.35);
    font-size: 0.85rem;
}
.comp-review-thumb-add:hover {
    border-color: var(--color-primary, #fff);
    color: var(--color-text);
}
.comp-review-iframe {
    width: 100%;
    height: 70vh;
    border: 0;
    background: #fff;
    display: block;
}

/* Image control toolbar — rotate + zoom. Sits below the thumb strip in
   the normal flow (was previously `position: absolute` overlaying the
   preview). Fixed height, never shrinks, never overlaps the image.
   On narrow screens the buttons may wrap onto a second line, but they
   never cover the preview because the toolbar is its own flex item. */
.comp-review-toolbar {
    flex: 0 0 auto;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    padding: 8px var(--space-md);
    background: rgba(0, 0, 0, 0.55);
    border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.comp-review-toolbar[hidden] { display: none; }
.comp-review-rotate-btn {
    background: transparent;
    color: #fff;
    border: 0;
    min-width: 32px;
    height: 32px;
    padding: 0 8px;
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: 1.05rem;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.comp-review-rotate-btn:hover { background: rgba(255, 255, 255, 0.18); }
.comp-review-rotate-btn:focus-visible { outline: 2px solid #fff; outline-offset: 1px; }
.comp-review-zoom-reset {
    font-size: 0.78rem;
    font-variant-numeric: tabular-nums;
    min-width: 48px;
}
.comp-review-tool-sep {
    width: 1px;
    align-self: stretch;
    margin: 4px;
    background: rgba(255, 255, 255, 0.25);
}
/* Cursor reflects current preview interaction:
     1×            — zoom-in   (double-click to zoom)
     >1×, idle     — grab      (click-and-hold to pan)
     >1×, dragging — grabbing
   user-drag prevention stops native image drag fighting the pan gesture. */
.comp-review-image {
    cursor: zoom-in;
    -webkit-user-drag: none;
    user-select: none;
    touch-action: none;
}
.comp-review-preview.zoomed .comp-review-image          { cursor: grab; }
.comp-review-preview.zoomed.dragging .comp-review-image { cursor: grabbing; }
.comp-review-preview.dragging                            { cursor: grabbing; }

.comp-review-side {
    border-left: 1px solid var(--color-border);
    background: var(--color-surface);
    padding: var(--space-lg);
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-lg);
}

.comp-review-side-label {
    color: var(--color-text-muted);
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-bottom: 4px;
}

.comp-review-state {
    /* No background — keep the right panel calm and high-contrast. */
    padding: 0;
}

.comp-review-meta {
    margin: 0;
    display: grid;
    grid-template-columns: minmax(96px, max-content) 1fr;
    column-gap: var(--space-md);
    row-gap: 6px;
    font-size: var(--font-size-sm);
    padding-top: var(--space-md);
    padding-bottom: var(--space-md);
    border-top: 1px solid var(--color-border);
    border-bottom: 1px solid var(--color-border);
}
.comp-review-meta:empty { display: none; }
.comp-review-meta dt { color: var(--color-text-muted); font-weight: 500; }
.comp-review-meta dd { margin: 0; word-break: break-word; }

.comp-review-form {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    margin: 0;
}

.comp-review-actions {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
}
.comp-review-actions .btn { width: 100%; }
/* The [hidden] attribute relies on the UA stylesheet's display:none,
   but .btn's own display:inline-flex overrides at equal specificity.
   Anchor !important here so state-driven button visibility is honored
   throughout the review modal. */
.comp-review-shell [hidden] { display: none !important; }

/* Reject confirmation panel — slides in below the action buttons. */
.comp-review-reject-panel {
    background: var(--color-bg);
    border-radius: var(--radius-md);
    padding: var(--space-md);
}
.comp-review-reject-buttons {
    display: flex;
    gap: var(--space-xs);
    justify-content: flex-end;
}

.comp-review-footer {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-lg);
    border-top: 1px solid var(--color-border);
    background: var(--color-surface);
}
.comp-review-footer-left {
    display: flex;
    gap: var(--space-xs);
    flex-wrap: wrap;
}

@media (max-width: 900px) {
    .comp-review-main {
        grid-template-columns: minmax(0, 1fr);
        grid-auto-rows: minmax(0, auto);
    }
    .comp-review-side {
        border-left: 0;
        border-top: 1px solid var(--color-border);
    }
    .comp-review-image  { max-height: 50vh; }
    .comp-review-iframe { height: 55vh; }
}

/* Phone-sized viewports — collapse the review modal to one scroll
   context so the header and footer stay pinned while the stacked
   preview + side panel scroll together. Also caps the preview height
   so action buttons stay reachable without trapped sub-scrolls. */
@media (max-width: 720px) {
    .comp-review-overlay { padding: 12px; }

    .comp-review-shell {
        max-height: calc(100vh - 24px);
        max-width: none;
    }

    .comp-review-header {
        padding: var(--space-sm) var(--space-md);
    }
    .comp-review-title {
        font-size: var(--font-size-md);
        white-space: normal;
    }

    /* One scroller — main scrolls, side and preview drop their own */
    .comp-review-main { overflow-y: auto; }
    .comp-review-side {
        padding: var(--space-md);
        overflow: visible;
        gap: var(--space-md);
    }
    .comp-review-preview-pane { min-height: 0; }
    .comp-review-image  { max-height: 40vh; }
    .comp-review-iframe { height: 50vh; }

    .comp-review-meta {
        padding-top: var(--space-sm);
        padding-bottom: var(--space-sm);
    }

    .comp-review-footer {
        padding: var(--space-sm) var(--space-md);
        flex-wrap: wrap;
    }
}


/* ── 18d. Old competence verify modal (legacy, unused) ───────── */

.comp-verify-overlay {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: 1100;
    align-items: center;
    justify-content: center;
    padding: var(--space-md);
}

.comp-verify-overlay.open { display: flex; }

.comp-verify-box {
    background: var(--color-surface);
    border-radius: var(--radius-lg);
    box-shadow: 0 12px 48px rgba(0, 0, 0, 0.28);
    width: 100%;
    max-width: 480px;
    max-height: 92vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.comp-verify-header {
    flex-shrink: 0;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-md);
    padding: var(--space-md) var(--space-lg);
    border-bottom: 1px solid var(--color-border);
}

.comp-verify-body {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: var(--space-lg);
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
}

.comp-verify-current {
    background: var(--color-bg);
    border-radius: var(--radius-md);
    padding: var(--space-sm) var(--space-md);
}

.comp-verify-actions {
    display: flex;
    gap: var(--space-sm);
    flex-wrap: wrap;
}

.comp-verify-actions .btn { flex: 1 1 auto; min-width: 110px; }

.comp-verify-footer {
    flex-shrink: 0;
    display: flex;
    justify-content: flex-end;
    padding: var(--space-sm) var(--space-lg);
    border-top: 1px solid var(--color-border);
    background: var(--color-surface);
}

/* Competence state badges (used in overview pills + user > Competence) */
.comp-state-badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: 0.7rem;
    font-weight: 600;
    line-height: 1.4;
    border: 1px solid transparent;
    white-space: nowrap;
    cursor: pointer;
    background: var(--color-badge-neutral-bg);
    color: var(--color-badge-neutral);
}
.comp-state-badge:hover { filter: brightness(0.96); }
.comp-state-verified { background: var(--color-badge-success-bg); color: var(--color-badge-success); }
.comp-state-pending  { background: var(--color-badge-warning-bg); color: var(--color-warning);   }
.comp-state-missing  { background: #f3f4f6; color: #6b7280; border-color: #e5e7eb; }
.comp-state-expiring { background: var(--color-badge-warning-bg); color: var(--color-warning);   }
.comp-state-expired  { background: var(--color-badge-danger-bg);  color: var(--color-danger);    }
.comp-state-rejected { background: var(--color-badge-danger-bg);  color: var(--color-danger);    }

/* Pill variant — used in the overview where we already render the
   competence code/letter (B, T2, …). Same color rules as the badge. */
.comp-state-pill {
    display: inline-block;
    padding: 2px 9px;
    border-radius: 999px;
    font-size: 0.78rem;
    font-weight: 600;
    line-height: 1.5;
    border: 1px solid transparent;
    background: var(--color-badge-neutral-bg);
    color: var(--color-badge-neutral);
    cursor: pointer;
    margin: 1px;
}
.comp-state-pill[disabled] { cursor: default; }
.comp-state-pill.comp-state-verified { background: var(--color-badge-success-bg); color: var(--color-badge-success); }
.comp-state-pill.comp-state-pending  { background: var(--color-badge-warning-bg); color: var(--color-warning);   }
.comp-state-pill.comp-state-missing  { background: #f3f4f6; color: #6b7280; border-color: #e5e7eb; }
.comp-state-pill.comp-state-expiring { background: var(--color-badge-warning-bg); color: var(--color-warning);   }
.comp-state-pill.comp-state-expired  { background: var(--color-badge-danger-bg);  color: var(--color-danger);    }
.comp-state-pill.comp-state-rejected { background: var(--color-badge-danger-bg);  color: var(--color-danger);    }

/* Competence overview on narrow viewports — 8 category columns can't fit
   below ~720px, so collapse rows into a compact stacked layout: name on top,
   pills directly beneath. Flex container ignores inter-td whitespace, and
   PHP marks empty category cells with .is-empty so they drop out entirely
   instead of leaving a tall airy gap. */
@media (max-width: 720px) {
    .comp-overview thead { display: none; }

    .comp-overview tbody tr {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        column-gap: 4px;
        row-gap: 6px;
        padding: 12px var(--space-md);
        border-bottom: 1px solid var(--color-border);
    }
    .comp-overview tbody tr:last-child { border-bottom: none; }
    .comp-overview tbody tr:hover { background: transparent; }

    .comp-overview tbody td {
        padding: 0;
        border: none;
        text-align: left;
    }

    /* Match the .card .table-wrap selector chain so its first-child
       padding-left (and last-child padding-right) doesn't push the name
       and trailing pill out of alignment with the row's left edge. */
    .card .table-wrap table.comp-overview tbody td:first-child {
        flex-basis: 100%;
        padding: 0;
        font-weight: 500;
        line-height: 1.3;
    }
    .card .table-wrap table.comp-overview tbody td:last-child {
        padding: 0;
    }

    /* Empty category cells (flagged in PHP) drop out entirely so the row
       hugs the name. Class hook avoids relying on :has() for compatibility. */
    .comp-overview tbody td.is-empty { display: none; }

    /* Slightly tighter pills; flex gap controls spacing instead of margin */
    .comp-overview .comp-state-pill {
        padding: 2px 8px;
        font-size: 0.6875rem;
        margin: 0;
    }
}


/* ── 19. Toast notifications ─────────────────────────────────── */

/* Fixed container — bottom-right corner, stacks toasts upward */
#toast-container {
    position: fixed;
    bottom: var(--space-lg);
    right: var(--space-lg);
    z-index: 2000;
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    max-width: 360px;
    width: calc(100vw - var(--space-xl) * 2);
    pointer-events: none;
}

/* Individual toast */
.toast {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-md);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
    font-size: var(--font-size-sm);
    font-weight: 500;
    opacity: 0;
    transform: translateY(8px);
    transition: opacity 0.25s ease, transform 0.25s ease;
    pointer-events: all;
}

.toast.toast-visible {
    opacity: 1;
    transform: translateY(0);
}

.toast-success { background: var(--color-flash-success-bg); color: var(--color-flash-success-text); border: 1px solid var(--color-flash-success-border); }
.toast-error   { background: var(--color-flash-error-bg); color: var(--color-flash-error-text); border: 1px solid var(--color-flash-error-border); }
.toast-warning { background: var(--color-flash-warning-bg); color: var(--color-flash-warning-text); border: 1px solid var(--color-flash-warning-border); }
.toast-info    { background: var(--color-flash-info-bg); color: var(--color-flash-info-text); border: 1px solid var(--color-flash-info-border); }

.toast span { flex: 1; }

.toast-close {
    background: none;
    border: none;
    cursor: pointer;
    font-size: 1.1rem;
    line-height: 1;
    opacity: 0.6;
    padding: 0;
    color: inherit;
    flex-shrink: 0;
}

.toast-close:hover { opacity: 1; }


/* ── 20. Login page notice banner ────────────────────────────── */

/* Fixed strip pinned to top of viewport — does not affect page layout */
.login-notice-banner {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 200;
}

.login-notice-banner .platform-notice {
    padding: 6px var(--space-lg);
    text-align: center;
    font-size: var(--font-size-sm);
    font-weight: 500;
    line-height: 1.4;
    border-bottom: none;
}


/* ── 21. Password requirements checker ──────────────────────── */

.pw-rules {
    margin-top: var(--space-xs);
    padding: var(--space-sm) var(--space-md);
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.pw-rule {
    font-size: 0.8rem;
    color: var(--color-text-muted);
}

.pw-rule-ok   { color: var(--color-success); }
.pw-rule-fail { color: var(--color-text-muted); }


/* ── 22. Clockwork terminal ──────────────────────────────────── */

/* Full-screen kiosk page */
body.clock-terminal {
    margin: 0;
    padding: 0;
    height: 100vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    background: var(--color-bg);
    transition: background 0.5s ease;
}

/* State backgrounds — flash on clock action */
body.clock-terminal.state-success {
    background: var(--color-success);
}

body.clock-terminal.state-error {
    background: var(--color-danger);
}

/* Main screen container */
.terminal-screen {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: var(--space-xl);
    text-align: center;
    position: relative;
}

/* Brand at top */
.terminal-brand {
    margin-bottom: var(--space-xl);
}

.terminal-logo {
    height: 40px;
    width: auto;
    margin: 0 auto var(--space-sm);
    transition: filter 0.5s ease;
}

body.clock-terminal.state-success .terminal-logo,
body.clock-terminal.state-error   .terminal-logo {
    filter: brightness(0) invert(1);
}

.terminal-account-name {
    font-size: var(--font-size-lg);
    font-weight: 700;
    letter-spacing: -0.02em;
    color: var(--color-text);
    transition: color 0.5s ease;
}

body.clock-terminal.state-success .terminal-account-name,
body.clock-terminal.state-error   .terminal-account-name {
    color: rgba(255,255,255,0.85);
}

/* PIN dot display */
.pin-display {
    display: flex;
    gap: var(--space-md);
    margin-bottom: var(--space-xl);
    justify-content: center;
}

.pin-dot {
    width: 16px;
    height: 16px;
    border-radius: 50%;
    border: 2px solid var(--color-border-strong);
    background: transparent;
    transition: background 0.1s, border-color 0.5s;
}

.pin-dot.filled {
    background: var(--color-text);
    border-color: var(--color-text);
}

body.clock-terminal.state-success .pin-dot,
body.clock-terminal.state-error   .pin-dot {
    border-color: rgba(255,255,255,0.5);
}

body.clock-terminal.state-success .pin-dot.filled,
body.clock-terminal.state-error   .pin-dot.filled {
    background: var(--color-white);
    border-color: var(--color-white);
}

/* Number pad */
.numpad {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-sm);
    max-width: 280px;
    width: 100%;
}

.numpad-btn {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    font-size: 1.5rem;
    font-weight: 500;
    height: 72px;
    cursor: pointer;
    color: var(--color-text);
    transition: background 0.1s, transform 0.05s, border-color 0.5s, background 0.5s;
    -webkit-tap-highlight-color: transparent;
    user-select: none;
}

.numpad-btn:active {
    transform: scale(0.95);
    background: var(--color-bg);
}

.numpad-btn-delete {
    font-size: 1.2rem;
    color: var(--color-text-muted);
}

.numpad-btn-submit {
    background: var(--color-text);
    color: var(--color-white);
    border-color: var(--color-text);
    font-size: 1.4rem;
}

.numpad-btn-submit:active {
    background: var(--color-accent-hover);
}

body.clock-terminal.state-success .numpad-btn,
body.clock-terminal.state-error   .numpad-btn {
    background: rgba(255,255,255,0.15);
    border-color: rgba(255,255,255,0.2);
    color: var(--color-white);
}

/* Result overlay — shown on success/error */
.terminal-result {
    display: none;
    flex-direction: column;
    align-items: center;
    gap: var(--space-md);
    color: var(--color-white);
}

body.clock-terminal.state-success .terminal-result,
body.clock-terminal.state-error   .terminal-result {
    display: flex;
}

body.clock-terminal.state-success .terminal-idle,
body.clock-terminal.state-error   .terminal-idle {
    display: none;
}

.terminal-result-icon {
    font-size: 4rem;
    line-height: 1;
}

.terminal-result-name {
    font-size: var(--font-size-3xl);
    font-weight: 700;
    letter-spacing: -0.03em;
}

.terminal-result-status {
    font-size: var(--font-size-xl);
    font-weight: 400;
    opacity: 0.9;
}

.terminal-result-time {
    font-size: var(--font-size-base);
    opacity: 0.75;
}


/* ── 22. Site footer ─────────────────────────────────────────── */

.site-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    /* Column and row gaps are set SEPARATELY and the row gap is zero.
       The shorthand `gap` sets both, which put 8px between wrapped lines on
       top of a link box already 44px tall — see HRC_FOOTER_TAP_TARGET below
       for why the two together were the whole density problem. Horizontal
       breathing room between links is still wanted; vertical is not. */
    column-gap: var(--space-sm);
    row-gap: 0;
    padding: var(--space-lg) var(--space-xl);
    font-size: 0.75rem;
    color: var(--color-text-muted);
    border-top: none;
    margin-top: var(--space-xl);
}

.site-footer a {
    color: var(--color-text-muted);
    text-decoration: none;
}

.site-footer a:hover {
    color: var(--color-text);
    text-decoration: underline;
}

.site-footer .dot { opacity: 0.45; }

/* The footer is CENTRED on every surface — the reading-column pages (legal,
   changelog, status) included. A flush variant (links left, copyright pushed
   to the right edge) existed until 2026-08-26 and was removed by owner
   decision: one layout everywhere. A few pages still carry a class of their
   own (.legal-footer, .sign-footer) but ONLY to inherit their page's reading
   width; no alignment, colour, spacing or link styling lives there. */

/* Tap target for footer legal links — and therefore the line spacing.
   ── HRC_FOOTER_TAP_TARGET: 32px ──

   These are the same number and cannot be separated. The links are the only
   thing on a footer row, and the row gap is zero, so the pitch between two
   wrapped lines IS this height. At 44px (WCAG 2.5.5 AAA / iOS HIG, which is
   what this was until 2026-08-25) the text occupies 18px of a 52px band and
   a four-line footer on a phone stood 170px tall — reported from a real
   handset as far too airy, which it was.

   32px is a deliberate step down to WCAG 2.5.8 AA (24px minimum) with room
   to spare, taken because these are secondary links on a row that is never
   the reason anyone opened the page. Do NOT solve a future density request
   by shrinking it further without re-reading 2.5.8 — below 24px the row
   stops being reachable for anyone with imprecise aim.

   Real height via min-height, NOT an expanded invisible hit area: the row
   wraps, and an overlay large enough to grow the target would cover the row
   above and below, making a tap near a line edge ambiguous. With row-gap: 0
   each link's box IS its whole row, so every point in the row belongs to
   exactly one link and there is nothing to mis-hit vertically.

   One rule, because since the footer consolidation there is one footer:
   layout/components/site_footer.php renders .site-footer on every surface in
   the product. The single exception is the PIN terminal, which is
   self-contained and loads no main.css — it carries its own copy of this
   value, and check_site_footer_single_source.php asserts the two agree so
   the copy cannot drift. Font size is unchanged by any of this. */
.site-footer a {
    display: inline-flex;
    align-items: center;
    min-height: 32px;
    padding: 0 2px;
}

/* A separator hidden by the wrap pass keeps its box (visibility, not
   display) — see layout/js/main.js. Nothing here may give .dot a
   display rule that would let JS remove it from the flow instead. */
.site-footer .dot[hidden],
.site-footer .dot.is-stranded { visibility: hidden; }

/* reCAPTCHA badge — never trapped beneath a fixed footer */
.grecaptcha-badge { z-index: 2147483647 !important; }



/* ── Detail table — read-only display rows ───────────────────── */

.detail-table {
    width: 100%;
    border-collapse: collapse;
}

.detail-table tr + tr td,
.detail-table tr + tr th {
    border-top: 1px solid var(--color-border);
}

.detail-table th,
.detail-table td {
    padding: var(--space-sm) 0;
    vertical-align: middle;
    text-align: left;
    font-weight: normal;
}

.detail-table th {
    width: 130px;
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    padding-right: var(--space-md);
}


/* ── Legal page tabs (admin) ──────────────────────────────────── */

.legal-body-tabs {
    margin-top: var(--space-md);
}
.legal-tab-bar {
    display: flex;
    gap: 0;
    border-bottom: 1px solid var(--color-border);
    margin-bottom: var(--space-md);
}
.legal-tab {
    padding: var(--space-xs) var(--space-md);
    border: 1px solid var(--color-border);
    border-bottom: none;
    background: var(--color-bg);
    cursor: pointer;
    font-size: var(--font-size-sm);
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.legal-tab.active {
    background: var(--color-surface);
}
.legal-tab-panel textarea {
    font-family: monospace;
    font-size: 0.85rem;
    width: 100%;
}

/* ── Competence data table ───────────────────────────────────── */

.comp-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.875rem;
}

.comp-table thead th {
    padding: 0 var(--space-sm) var(--space-xs) 0;
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-text-muted);
    background: transparent;
    border-bottom: 2px solid var(--color-border);
    white-space: nowrap;
    text-align: left;
}

.comp-table thead th:last-child {
    text-align: right;
    padding-right: 0;
}

.comp-table tbody tr + tr td {
    border-top: 1px solid var(--color-border);
}

.comp-table tbody td {
    padding: var(--space-sm) var(--space-sm) var(--space-sm) 0;
    vertical-align: middle;
}

.comp-table tbody td:last-child {
    text-align: right;
    padding-right: 0;
    white-space: nowrap;
}


/* ── Modal detail table (session details, etc.) ─────────────── */

.modal-detail-table {
    width: 100%;
    table-layout: auto;
}

.modal-detail-table th,
.modal-detail-table td {
    padding: var(--space-sm) 0;
    border-bottom: 1px solid var(--color-border);
    vertical-align: top;
    text-align: left;
    font-weight: normal;
}

/* Remove bottom border from the actual last row (visible or not) */
.modal-detail-table tr:last-child td,
.modal-detail-table tr:last-child th {
    border-bottom: none;
}

.modal-detail-table th {
    width: 120px;
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    padding-right: var(--space-md);
    white-space: nowrap;
}

/* Value cell: prevent overflow at narrow modal widths and let long
   strings wrap rather than push the table wider than the modal.
   Selector uses `tbody` to beat the global `tbody td:last-child`
   rule (0,1,2) that forces white-space:nowrap on the last cell. */
.modal-detail-table tbody td {
    min-width: 0;
    overflow-wrap: anywhere;
    word-break: break-word;
    white-space: normal;
    text-align: right;
}

/* Form controls inside a value cell stay left-aligned for normal
   input behaviour (right-aligned text inside <input> looks wrong) */
.modal-detail-table td .form-control,
.modal-detail-table td input,
.modal-detail-table td select,
.modal-detail-table td textarea {
    text-align: left;
}

/* Mobile: stack label above value so long content has the full
   modal width to wrap into. The <table>/<tbody> must also become
   block-level — leaving them as display:table re-introduces the
   table sizing algorithm and the table grows to fit content. */
@media (max-width: 640px) {
    .modal-detail-table,
    .modal-detail-table tbody {
        display: block;
        width: 100%;
    }
    .modal-detail-table tr {
        display: block;
        padding: var(--space-sm) 0;
        border-bottom: 1px solid var(--color-border);
    }
    .modal-detail-table tr:last-child {
        border-bottom: none;
    }
    .modal-detail-table tbody th,
    .modal-detail-table tbody td {
        display: block;
        width: 100%;
        padding: 0;
        border-bottom: none;
        text-align: left;
        white-space: normal;
    }
    .modal-detail-table th {
        margin-bottom: 2px;
    }
}


/* ── 23. Workspace topbar elements ──────────────────────────── */

/* Page title — left side of topbar, grows to push right content over.
   Rendered as <h1>; reset UA margins to keep topbar on one row. */
body.workspace .topbar-title,
body.admin-platform .topbar-title {
    flex: 1;
    margin: 0;
    font-size: var(--font-size-lg);
    font-weight: 600;
    color: var(--color-text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
}

/* Below 640px, hide the "Module • " prefix to give the entity name room.
   Visually hidden (not display:none) so screen readers still announce it. */
@media (max-width: 640px) {
    body.workspace .topbar-module {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }
}


/* Right cluster: terminal link + lang + avatar */
body.workspace .topbar-right {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
}

/* Small link to the public clock-in terminal */
body.workspace .topbar-terminal-link {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    text-decoration: none;
    white-space: nowrap;
    padding: 4px 10px;
    border-radius: var(--radius-md);
    transition: color 0.15s, background 0.15s;
}

body.workspace .topbar-terminal-link:hover {
    color: var(--color-text);
    background: var(--color-lang-bg);
    text-decoration: none;
}

/* Language switcher — compact segmented pill group */
.lang-switcher {
    display: flex;
    align-items: center;
    gap: 0;
    margin: 0;
    padding: 2px;
    background: var(--color-lang-bg);
    border: 1px solid var(--color-nav-border);
    border-radius: var(--radius-lg);
    flex-shrink: 0;
}

.lang-btn {
    background: none;
    border: 1px solid transparent;
    border-radius: calc(var(--radius-lg) - 2px);
    padding: 3px 8px;
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.03em;
    color: var(--color-lang-text);
    cursor: pointer;
    line-height: 1.4;
    text-decoration: none;
    transition: color 0.15s, background 0.15s, border-color 0.15s, box-shadow 0.15s;
}

.lang-btn:hover {
    color: var(--color-text);
}

.lang-btn-active {
    color: var(--color-text);
    background: var(--color-white);
    border-color: var(--color-lang-active-border);
    box-shadow: 0 1px 2px rgba(0,0,0,0.06);
}

/* ── Global topbar search ──────────────────────────────────────
   Understated compact field beside the terminal link. Below 768px
   the input hides behind the icon toggle; .is-open (set by
   global-search.js) expands it across the topbar and
   .topbar-search-open on .app-topbar hides the title to make room. */
.topbar-search {
    display: flex;
    align-items: center;
    flex-shrink: 0;
}

.topbar-search-toggle {
    display: none;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    background: none;
    border: none;
    border-radius: var(--radius-md);
    color: var(--color-text-muted);
    cursor: pointer;
}

.topbar-search-toggle:hover {
    color: var(--color-text);
    background: var(--color-lang-bg);
}

/* The class sets `display`, so the UA's [hidden] rule would lose —
   keep the attribute meaningful (cascade-trap guard contract) */
.topbar-search-toggle[hidden] {
    display: none;
}

.topbar-search-input {
    width: 170px;
    padding: 4px 10px;
    font-size: var(--font-size-sm);
    font-family: inherit;
    color: var(--color-text);
    background: var(--color-lang-bg);
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    transition: width 0.15s, background 0.15s, border-color 0.15s, box-shadow 0.15s;
}

.topbar-search-input::placeholder {
    color: var(--color-text-muted);
}

/* Uniform active-input state: full-strength border + subtle ring
   (same signal as .form-control:focus / .hrc-toolbar-control:focus) */
.topbar-search-input:focus {
    outline: none;
    width: 240px;
    background: var(--color-white);
    border-color: var(--color-text);
    box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05);
}

/* Native WebKit clear X — same treatment as .hrc-toolbar-control */
.topbar-search-input::-webkit-search-cancel-button {
    -webkit-appearance: none;
    appearance: none;
    height: 14px;
    width: 14px;
    margin-left: 4px;
    background: var(--color-text-muted);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18 6L6 18M6 6l12 12' stroke='black' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18 6L6 18M6 6l12 12' stroke='black' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
    cursor: pointer;
}

@media (max-width: 768px) {
    .topbar-search-toggle {
        display: flex;
    }

    .topbar-search-input {
        display: none;
    }

    .topbar-search.is-open {
        flex: 1;
        min-width: 0;
    }

    .topbar-search.is-open .topbar-search-toggle {
        display: none;
    }

    .topbar-search.is-open .topbar-search-input {
        display: block;
        width: 100%;
    }

    .topbar-search.is-open .topbar-search-input:focus {
        width: 100%;
    }

    /* Make room: the title yields while the search is expanded */
    .app-topbar.topbar-search-open .topbar-title {
        display: none;
    }

    .app-topbar.topbar-search-open .topbar-right {
        flex: 1;
        min-width: 0;
    }
}

/* Circular user-initials avatar chip */
body.workspace .topbar-avatar {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: var(--color-avatar-bg);
    color: var(--color-white);
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    cursor: default;
    user-select: none;
}

/* Thin horizontal rule before Settings in sidebar nav */
body.workspace .nav-section-divider {
    border: none;
    border-top: 1px solid rgba(255,255,255,0.10);
    margin: var(--space-sm) var(--space-md);
}


/* ── 23. Workspace theme — light sidebar ─────────────────────── */

/* Overall page background — warm off-white to contrast the white sidebar */
body.workspace,
body.admin-platform {
    background: var(--color-nav-bg);
}

body.workspace .app-sidebar {
    background: var(--color-white);
    border-right: 1px solid var(--color-nav-border);
}

/* Brand block: account name large, app name tiny below */
body.workspace .sidebar-brand {
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    border-bottom: 1px solid var(--color-nav-border);
    padding: var(--space-md);
}

body.workspace .ws-account-name {
    font-size: var(--font-size-lg);
    font-weight: 700;
    color: var(--color-nav-active-text);
    letter-spacing: -0.02em;
    line-height: 1.2;
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    max-width: 100%;
    min-width: 0;
    padding: 4px 8px;
    margin: -4px -8px;
    border-radius: var(--radius-sm, 4px);
    transition: color 0.15s, background 0.15s;
    -webkit-tap-highlight-color: transparent;
}

body.workspace .ws-account-name-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

body.workspace .ws-account-name-chev {
    flex: 0 0 auto;
    font-weight: 400;
    color: var(--color-text-muted);
    opacity: 0.5;
    transform: translateX(0);
    transition: opacity 0.15s, transform 0.15s, color 0.15s;
}

body.workspace a.ws-account-name:hover,
body.workspace a.ws-account-name:focus-visible {
    color: var(--color-nav-active-text);
    text-decoration: none;
}

body.workspace a.ws-account-name:hover .ws-account-name-chev,
body.workspace a.ws-account-name:focus-visible .ws-account-name-chev {
    opacity: 1;
    color: var(--color-nav-active-text);
    transform: translateX(2px);
}

body.workspace a.ws-account-name:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 2px;
}

/* Platform switcher — segmented pill group (Workforce / Ops) */
body.workspace .platform-switcher {
    display: flex;
    align-items: stretch;
    gap: 0;
    margin-top: 10px;
    padding: 3px;
    background: var(--color-lang-bg);
    border: 1px solid var(--color-nav-border);
    border-radius: var(--radius-lg);
    flex-shrink: 0;
    white-space: nowrap;
    width: 100%;
    max-width: 260px;
}

body.workspace .platform-btn {
    flex: 1 1 0;
    background: none;
    border: 1px solid transparent;
    border-radius: calc(var(--radius-lg) - 3px);
    height: 34px;
    padding: 0 18px;
    font-size: 0.82rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--color-lang-text);
    cursor: pointer;
    line-height: 1;
    transition: color 0.15s, background 0.15s, border-color 0.15s, box-shadow 0.15s;
}

body.workspace .platform-btn:hover {
    color: var(--color-text);
}

body.workspace .platform-btn-active {
    color: var(--color-nav-active-text);
    background: var(--color-white);
    border-color: var(--color-border-strong, var(--color-lang-active-border));
    box-shadow: 0 1px 2px rgba(0,0,0,0.08), 0 2px 4px rgba(0,0,0,0.04);
}

/* Nav group labels — hidden (cleaner sidebar without section headers) */
body.workspace .nav-group-label {
    display: none;
}

/* Platform-based sidebar switching */
body.platform-hr .sidebar-platform-ops { display: none; }
body.platform-ops .sidebar-platform-hr { display: none; }

/* Platform-based content switching — dashboard KPI cards and live regions
   carry data-dash-platform="hr|ops|shared"; shared has no rule and is
   always visible. The JS-set body class drives the toggle. */
body.platform-hr  [data-dash-platform="ops"] { display: none; }
body.platform-ops [data-dash-platform="hr"]  { display: none; }

/* Production placeholder links — not routed yet */
body.workspace .nav-placeholder {
    cursor: default;
    pointer-events: none;
}

/* Nav links */
body.workspace .sidebar-nav a {
    color: var(--color-nav-text);
    font-size: 0.85rem;
}

body.workspace .sidebar-nav a:hover {
    color: var(--color-nav-active-text);
    background: var(--color-accent-subtle);
}

body.workspace .sidebar-nav a.active {
    color: var(--color-nav-active-text);
    font-weight: 500;
    background: var(--color-accent-subtle);
}

body.workspace .sidebar-nav a.active::before {
    background: var(--color-nav-active-text);
}

/* Section divider between nav groups */
body.workspace .nav-section-divider {
    border-top-color: var(--color-nav-border);
}

/* Topbar — white to match sidebar */
body.workspace .app-topbar {
    background: var(--color-white);
    border-bottom: 1px solid var(--color-nav-border);
}

/* Avatar chip — use brand red */
body.workspace .topbar-avatar {
    background: var(--color-primary);
}

/* Sidebar footer — user block */
body.workspace .sidebar-footer {
    border-top: 1px solid var(--color-nav-border);
    padding: var(--space-sm) var(--space-md);
    background: var(--color-white);
}

body.workspace .sidebar-footer-user {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
}

body.workspace .sidebar-footer-avatar {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: var(--color-nav-border);
    color: var(--color-nav-text);
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.01em;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

body.workspace .sidebar-footer-name {
    display: block;
    font-size: 0.8rem;
    font-weight: 500;
    color: var(--color-nav-text);
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 130px;
}

body.workspace .sidebar-footer-links {
    font-size: 0.72rem;
}

body.workspace .sidebar-footer-links a {
    color: var(--color-lang-text);
    text-decoration: none;
}

body.workspace .sidebar-footer-links a:hover {
    color: var(--color-nav-active-text);
    text-decoration: none;
}

/* Hamburger icon */
body.workspace .sidebar-toggle {
    color: var(--color-nav-text);
}

/* Mobile */
@media (max-width: 768px) {
    /* Premium SaaS control bar — clean white above warm page, hairline border + faint depth */
    body.workspace .app-sidebar {
        background: var(--color-white);
        border-bottom: 1px solid var(--color-nav-border);
        box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
    }

    body.workspace .sidebar-brand {
        display: flex;
        flex: 1 1 auto;
        min-width: 0;
        flex-direction: row;
        align-items: center;
        gap: 8px;
        border-bottom: none;
        padding: 0;
    }

    body.workspace .ws-account-name {
        flex: 1 1 auto;
        min-width: 0;
        font-size: var(--font-size-base);
        line-height: 1.2;
    }

    /* Switcher: lighter weight, calmer depth, narrower */
    body.workspace .platform-switcher {
        margin-top: 0;
        margin-left: 0;
        flex: 0 1 auto;
        width: auto;
        max-width: none;
        padding: 2px;
        border-color: transparent;
        background: rgba(15, 23, 42, 0.05);
        border-radius: 999px;
    }

    body.workspace .platform-btn {
        height: 26px;
        padding: 0 9px;
        font-size: 0.72rem;
        font-weight: 600;
        letter-spacing: 0.005em;
        border-radius: 999px;
    }

    body.workspace .platform-btn-active {
        background: var(--color-white);
        border-color: transparent;
        box-shadow: 0 1px 1.5px rgba(15, 23, 42, 0.05);
    }

    /* Hamburger icon — darker for stronger affordance */
    body.workspace .sidebar-toggle {
        color: var(--color-nav-active-text);
    }

    body.workspace .sidebar-nav {
        background: var(--color-white);
    }

    /* Hide group labels + dividers in mobile overlay */
    body.workspace .nav-group-label,
    body.workspace .nav-section-divider {
        display: none;
    }

    /* Hide inactive platform nav in mobile overlay */
    body.platform-hr .sidebar-platform-ops,
    body.platform-ops .sidebar-platform-hr {
        display: none;
    }

    body.workspace .sidebar-nav a {
        color: var(--color-nav-text);
    }

    body.workspace .sidebar-nav a:hover {
        color: var(--color-nav-active-text);
        background: var(--color-accent-subtle);
    }

    body.workspace .sidebar-nav a.active {
        color: var(--color-nav-active-text);
        font-weight: 600;
        background: var(--color-accent-subtle);
    }

    body.workspace .sidebar-nav a.active::before {
        background: var(--color-nav-active-text);
    }

    body.workspace .nav-close {
        color: var(--color-nav-text);
    }

    body.workspace .nav-footer-links {
        color: var(--color-lang-text);
    }

    body.workspace .nav-footer-links a {
        color: var(--color-lang-text);
    }
}

/* Very narrow phones — let pill labels shrink without wrapping */
@media (max-width: 380px) {
    body.workspace .platform-btn {
        padding: 0 7px;
        font-size: 0.68rem;
    }
    body.workspace .ws-account-name-chev {
        display: none;
    }
}


/* ── Competence class chips ───────────────────────────────── */

.comp-class-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.comp-class-chip {
    display: inline-flex;
    align-items: center;
    padding: 5px 12px;
    border: 1.5px solid var(--color-border);
    border-radius: 6px;
    cursor: pointer;
    font-size: 0.875rem;
    font-weight: 500;
    user-select: none;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
    color: var(--color-text);
    background: var(--color-bg);
}

.comp-class-chip input[type="checkbox"] {
    display: none;
}

.comp-class-chip:has(input:checked) {
    background: var(--color-primary);
    border-color: var(--color-primary);
    color: var(--color-white);
}

.comp-class-chip:hover {
    border-color: var(--color-primary);
}

/* Wide variant for long names (SFS documented, other) */
.comp-class-grid--wide {
    flex-direction: column;
}

.comp-class-chip--wide {
    width: 100%;
    padding: 7px 12px;
    font-weight: normal;
}

.comp-class-chip--wide span {
    line-height: 1.3;
}


/* ── 24. Session filter bar (shared: users, groups, projects) ── */

.sess-filter-actions {
    display: flex;
    gap: var(--space-sm);
    margin-bottom: var(--space-lg);
    align-items: center;
    flex-wrap: wrap;
}

.sess-filter-bar {
    display: flex;
    gap: var(--space-sm);
    align-items: center;
    justify-content: space-between;
}

.sess-nav-group {
    display: flex;
    gap: var(--space-sm);
    align-items: center;
}

.sess-nav-form {
    display: flex;
    gap: var(--space-sm);
    align-items: center;
}

.sess-month-select {
    min-width: 130px;
}

.sess-year-select {
    width: auto;
}

.sess-day-select {
    width: auto;
    min-width: 70px;
}

@media (max-width: 640px) {
    .sess-filter-actions { display: block; }
    .sess-filter-bar     { width: 100%; flex-direction: column; align-items: stretch; }
    .sess-nav-group      { width: 100%; }
    .sess-nav-form       { flex: 1; display: flex; flex-wrap: wrap; gap: var(--space-sm); }
    .sess-month-select   { flex: 1; min-width: 110px; }
    .sess-day-select     { width: 70px; }
    .sess-year-select    { width: 75px; }
}

/* Unverified user indicator dot (legacy — kept for backwards-compat,
   superseded by .vum-badge-* in active sessions table) */
.cw-unverif-dot {
    cursor: pointer;
    color: var(--color-danger);
    font-weight: 700;
    margin-left: 2px;
}

/* ── Verification badges (modal title + active session rows) ─────
   Four states: needs / snoozed / verified / flagged. */
.vum-badge {
    display: inline-block;
    font-size: 0.7rem;
    font-weight: 600;
    line-height: 1;
    padding: 3px 7px;
    border-radius: 999px;
    text-transform: none;
    letter-spacing: 0.01em;
    vertical-align: middle;
    white-space: nowrap;
}
.vum-badge-needs    { background: rgba(231, 76, 60, 0.12);  color: #c0392b; border: 1px solid rgba(231, 76, 60, 0.25); }
.vum-badge-snoozed  { background: rgba(243, 156, 18, 0.14); color: #b9770e; border: 1px solid rgba(243, 156, 18, 0.28); }
.vum-badge-verified { background: rgba(39, 174, 96, 0.12);  color: #1e8449; border: 1px solid rgba(39, 174, 96, 0.25); }
.vum-badge-flagged  { background: #2c2c2c;                  color: #fff;    border: 1px solid #1a1a1a; }
.vum-badge-clickable { cursor: pointer; }
.vum-badge-clickable:hover { filter: brightness(0.95); }

/* Verification modal layout */
.vum-table {
    width: 100%;
    border-collapse: collapse;
    border-radius: var(--radius);
    overflow: hidden;
    border: 1px solid var(--color-border);
    margin-bottom: var(--space-sm);
}
.vum-table th,
.vum-table td {
    padding: var(--space-sm) var(--space-md);
    font-size: 0.9rem;
    border-bottom: 1px solid var(--color-border);
    text-align: left;
    vertical-align: top;
    transition: background-color 0.12s ease;
}
.vum-table tr:last-child th,
.vum-table tr:last-child td { border-bottom: 0; }
.vum-table th {
    background: var(--color-surface, #fafafa);
    font-weight: 500;
    color: var(--color-text-muted);
    width: 38%;
    white-space: nowrap;
}
/* Highlight the entire row on hover (label + value cells together) */
.vum-table tr:hover > th,
.vum-table tr:hover > td {
    background: var(--color-bg-hover, #ececec);
}

/* Missing critical field highlight */
.vum-missing {
    color: var(--color-danger, #c0392b);
    font-style: italic;
}
.vum-missing-summary {
    margin: var(--space-md) 0 0;
    padding: var(--space-sm) var(--space-md);
    border-left: 3px solid var(--color-danger, #c0392b);
    background: rgba(231, 76, 60, 0.06);
    font-size: 0.85rem;
    color: var(--color-danger, #c0392b);
    border-radius: 0 var(--radius) var(--radius) 0;
}

/* Modal footer split layout */
.vum-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--space-md);
    flex-wrap: wrap;
}
.vum-utility {
    display: flex;
    gap: var(--space-xs);
    flex-wrap: wrap;
}
.vum-actions {
    display: flex;
    gap: var(--space-sm);
    margin-left: auto;
    flex-wrap: wrap;
}

/* ── Verify modal — desktop shell ────────────────────────────────
   Sized a touch wider than the standard .modal (520px) because the
   verify table renders better at ~580px without forcing horizontal
   scroll. The header carries an inline status badge so the title
   row needs flex+gap. Mobile (≤ 768px) overrides max-width and
   tightens header padding below. */
.vum-modal {
    max-width: 580px;
}
#verify-user-backdrop .modal-header {
    gap: var(--space-md);
}
#verify-user-backdrop .modal-title {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
}

/* ── Verify modal — mobile (≤ 768px) ─────────────────────────────
   Full-width sheet, stacked label/value rows, sticky two-row footer.
   Designed for managers verifying users one-handed in the field. */
@media (max-width: 768px) {
    /* Sheet: small margins, centered, fully rounded — overrides global
       bottom-docked .modal mobile rule. Uses dynamic-vh + safe-area-inset
       so the sheet never clips behind iOS Safari's chrome. */
    #verify-user-backdrop {
        align-items: center;
        padding:
            calc(8px + env(safe-area-inset-top))
            8px
            calc(8px + env(safe-area-inset-bottom));
    }
    #verify-user-backdrop .vum-modal {
        max-width: none;                   /* overrides desktop .vum-modal max-width */
        width: calc(100vw - 16px);
        max-height: calc(100vh  - 16px - env(safe-area-inset-top) - env(safe-area-inset-bottom));
        max-height: calc(100dvh - 16px - env(safe-area-inset-top) - env(safe-area-inset-bottom));
        margin: 0;
        border-radius: var(--radius-lg);
    }

    /* Compact header */
    #verify-user-backdrop .modal-header {
        padding: 12px 14px;
    }
    #verify-user-backdrop .modal-title {
        font-size: 1.05rem;
        gap: var(--space-xs) !important;
    }
    #verify-user-backdrop .modal-close {
        font-size: 1.6rem;
        padding: 4px 8px;
    }

    /* Body: vertical scroll only, tightened padding */
    #verify-user-backdrop .modal-body {
        padding: 12px 14px;
        overflow-x: hidden;
    }
    #verify-user-backdrop #vum-instructions {
        font-size: 0.82rem;
        margin-bottom: 10px !important;
    }

    /* Stacked label/value rows replacing the table */
    #verify-user-backdrop .vum-table {
        display: block;
        width: 100%;
        border: 1px solid var(--color-border);
        border-radius: var(--radius);
        overflow: hidden;
        margin-bottom: 0;
    }
    #verify-user-backdrop .vum-table tbody,
    #verify-user-backdrop .vum-table tr {
        display: block;
        width: 100%;
    }
    #verify-user-backdrop .vum-table tr {
        padding: 10px 12px;
        border-bottom: 1px solid var(--color-border);
    }
    #verify-user-backdrop .vum-table tr:last-child {
        border-bottom: none;
    }
    #verify-user-backdrop .vum-table th,
    #verify-user-backdrop .vum-table td {
        display: block;
        width: 100%;
        padding: 0;
        border-bottom: none;
        background: transparent !important;
        text-align: left;
    }
    #verify-user-backdrop .vum-table th {
        font-size: 0.7rem;
        font-weight: 500;
        letter-spacing: 0.04em;
        text-transform: uppercase;
        color: var(--color-text-muted);
        margin-bottom: 2px;
        white-space: normal;
    }
    #verify-user-backdrop .vum-table td {
        font-size: 0.95rem;
        line-height: 1.35;
        color: var(--color-text);
        word-break: break-word;
    }
    /* Disable row hover highlight on mobile (no hover, looks broken on tap) */
    #verify-user-backdrop .vum-table tr:hover > th,
    #verify-user-backdrop .vum-table tr:hover > td {
        background: transparent;
    }

    /* Warning summary tightened */
    #verify-user-backdrop .vum-missing-summary {
        margin-top: 12px;
        padding: 10px 12px;
        font-size: 0.85rem;
    }

    /* Sticky two-row footer */
    #verify-user-backdrop .modal-footer {
        padding: 10px 12px;
        background: var(--color-surface);
        position: sticky;
        bottom: 0;
    }
    #verify-user-backdrop .vum-footer {
        flex-direction: column;
        align-items: stretch;
        gap: 8px;
    }
    #verify-user-backdrop .vum-utility,
    #verify-user-backdrop .vum-actions {
        display: flex;
        margin-left: 0;
        gap: 8px;
        flex-wrap: nowrap;
        width: 100%;
    }
    #verify-user-backdrop .vum-utility .btn,
    #verify-user-backdrop .vum-actions .btn {
        flex: 1 1 0;
        min-height: 44px;
        padding: 10px 12px;
        font-size: 0.92rem;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
}


/* ── Schedule module ─────────────────────────────────────────── */

/* ── Toolbar ─────────────────────────────────────────────────── */
.sched-toolbar {
    display: flex;
    flex-direction: column;
    gap: 0;
    padding: var(--space-sm) var(--space-md);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    margin-bottom: var(--space-md);
}

/* Row 1: nav (left) + actions (right) */
.sched-toolbar-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
}

.sched-toolbar-nav {
    display: flex;
    align-items: center;
    gap: 4px;
    flex-shrink: 0;
}

.sched-week-label,
.sched-range-label {
    font-weight: 600;
    font-size: var(--font-size-sm);
    min-width: 130px;
    text-align: left;
    white-space: nowrap;
    padding: 0 var(--space-xs);
    color: var(--color-text-muted);
}

/* Native date input inside the toolbar */
.sched-anchor-picker {
    width: auto;
    min-width: 140px;
    max-width: 170px;
    font-size: var(--font-size-sm);
    padding: 4px 8px;
}

/* "Jump back to today" pill — distinct from the view switcher */
.sched-today-btn {
    font-weight: 600;
}

@media (max-width: 640px) {
    .sched-range-label { display: none; } /* date picker already shows it */
    .sched-anchor-picker { min-width: 130px; max-width: 150px; }
}

.sched-nav-btn {
    font-size: 1.1rem;
    padding: 0 8px;
    line-height: 1.8;
}

.sched-toolbar-actions {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    flex-shrink: 0;
}

/* View-mode switcher (Today / 3 Days / Week) */
.sched-view-switch {
    display: inline-flex;
    align-items: stretch;
    background: var(--color-surface-alt, #f4f4f4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    overflow: hidden;
    flex-shrink: 0;
}

.sched-view-btn {
    padding: 6px 14px;
    font-size: var(--font-size-sm);
    font-weight: 600;
    color: var(--color-text-muted);
    text-decoration: none;
    background: transparent;
    border: 0;
    border-right: 1px solid var(--color-border);
    line-height: 1.4;
    transition: background 0.15s ease, color 0.15s ease;
    white-space: nowrap;
}

.sched-view-btn:last-child { border-right: 0; }

.sched-view-btn:hover {
    background: rgba(0,0,0,0.03);
    color: var(--color-text);
}

.sched-view-btn.is-active {
    background: #ebebeb;
    color: var(--color-text);
    font-weight: 700;
    border-color: var(--color-border-strong);
}

/* ── Async user picker (combobox) ─────────────────────────────
   Used for shift-assignment flows; markup carries [data-user-picker].
   JS bootstrap lives in /layout/js/user-picker.js.

   One visible slot: either the search input OR the selected-user chip,
   never both. Visibility is driven by the `.is-selected` class on the
   container, not the `[hidden]` attribute, so class-based overrides win
   predictably and there's no CSS-specificity race. */
.hrc-picker {
    position: relative;
    display: block;
    width: 100%;
}

/* Default (empty) state: show input, hide chip */
.hrc-picker .hrc-picker-chip  { display: none; }
.hrc-picker .hrc-picker-input { display: block; }

/* Selected state: replace input with chip */
.hrc-picker.is-selected .hrc-picker-input { display: none; }
.hrc-picker.is-selected .hrc-picker-chip {
    display: flex;
    width: 100%;
}

.hrc-picker-input {
    width: 100%;
    min-height: 40px; /* comfortable touch target */
}

.hrc-picker-input:focus {
    outline: none;
    border-color: var(--color-border-strong);
    box-shadow: 0 0 0 3px rgba(0,0,0,0.08);
}

.hrc-picker-chip {
    align-items: center;
    gap: 8px;
    padding: 6px 8px 6px 12px;
    background: var(--color-surface-alt, #f4f4f4);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    font-size: var(--font-size-sm);
    min-height: 40px;
}

.hrc-picker-chip-label {
    font-weight: 600;
    flex: 1 1 auto;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.hrc-picker-chip-label .hrc-chip-meta {
    font-weight: 400;
    color: var(--color-text-muted);
    margin-left: 6px;
    font-size: 0.9em;
}

.hrc-picker-chip-clear {
    background: transparent;
    border: 0;
    font-size: 1.2rem;
    line-height: 1;
    padding: 4px 6px;
    cursor: pointer;
    color: var(--color-text-muted);
    border-radius: var(--radius-sm);
}

.hrc-picker-chip-clear:hover { color: var(--color-danger); background: rgba(0,0,0,0.05); }

.hrc-picker-dd {
    /* position/top/left/width/maxHeight are set by JS at open time so
       the dropdown escapes ancestor overflow:hidden. */
    position: fixed;
    z-index: 1200;
    overflow-y: auto;
    background: var(--color-surface);
    border: 1px solid var(--color-border-strong);
    border-radius: var(--radius-md);
    box-shadow: 0 8px 24px rgba(0,0,0,0.12);
    padding: 4px;
}

.hrc-picker-row {
    padding: 8px 10px;
    border-radius: var(--radius-sm);
    cursor: pointer;
    line-height: 1.35;
}

.hrc-picker-row + .hrc-picker-row { margin-top: 2px; }

.hrc-picker-row:hover,
.hrc-picker-row.is-active {
    background: #ebebeb;
}

.hrc-picker-row-top {
    display: flex;
    align-items: baseline;
    gap: 8px;
    flex-wrap: wrap;
}

.hrc-picker-row-name {
    font-weight: 600;
    color: var(--color-text);
}

.hrc-picker-row-id {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

.hrc-picker-row-group {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}
.hrc-picker-row-group::before { content: '·'; margin-right: 6px; color: var(--color-text-muted); }

.hrc-picker-row-meta {
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
    margin-top: 2px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ────────────────────────────────────────────────────────────────
   Role default-items picker (migration 225)

   Always-inline results panel. Earlier attempts used an absolute
   popover (everywhere, then via @media as a "desktop nicety") but
   both leaked under the modal's sticky footer because the popover's
   max-height extends below the modal-body's overflow box — the
   footer then visually covers the bottom of the panel.

   Inline solves this structurally: results live INSIDE the modal
   body's scroll area, in normal flow under the search input. The
   modal body scrolls naturally when content exceeds its height, and
   the footer (a flex sibling, not an absolute element) never overlaps
   anything inside that scroll area.

   No JS layout measurement, no z-index gymnastics, no responsive
   mode switch.
   ──────────────────────────────────────────────────────────────── */
.role-items-picker {
    margin-bottom: var(--space-lg);
}

/* Compact × on each selected role-default item row — matches the
   participant/chip remove pattern used in Risk Assessments and Incidents.
   Visually quiet by default, danger-coloured + light tint on hover/focus
   so the destructive action remains discoverable. Touch target is a
   28px square (centered glyph + padding) — meets a comfortable tap
   size without inflating the row. */
.role-items-remove {
    background: transparent;
    border: 0;
    cursor: pointer;
    color: var(--color-text-muted);
    font-size: 1.1rem;
    line-height: 1;
    padding: 6px 8px;
    min-width: 28px;
    min-height: 28px;
    border-radius: var(--radius-sm);
    transition: background-color 120ms, color 120ms;
}
.role-items-remove:hover,
.role-items-remove:focus-visible {
    color: var(--color-danger);
    background: rgba(0, 0, 0, 0.05);
}
.role-items-remove:focus-visible {
    outline: 2px solid var(--color-danger);
    outline-offset: 1px;
}

.role-items-picker .hrc-picker {
    position: static;
}

.role-items-picker .hrc-picker-dd {
    /* Override the global .hrc-picker-dd's position:fixed (used by
       the standalone typeahead that appends to <body>). */
    position: static;
    margin-top: 4px;
    /* Cap height so a huge result set scrolls internally instead of
       pushing the modal body to absurd heights. The modal body will
       still scroll outside this cap if the picker itself ends up
       below the body's visible viewport. */
    max-height: 320px;
    overflow-y: auto;
}

/* Single-line operational-path row — used by operational-location-picker.
   Parent chain renders muted inline before the strong leaf so the row
   stays compact ("Amfi • Scene • FOH" on one line, scannable). */
.hrc-picker-row-top--inline {
    flex-wrap: nowrap;
    min-width: 0;
}
.hrc-picker-row-path {
    display: inline;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.hrc-picker-row-path-prefix {
    color: var(--color-text-muted);
    font-weight: 400;
}

.hrc-picker-state {
    padding: 10px 12px;
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

.hrc-picker-state--error { color: var(--color-danger); }

/* Entity-type group heading inside the global-search dropdown */
.hrc-picker-group-label {
    padding: 8px 12px 3px;
    font-size: 0.6875rem; /* literal: --font-size-xs is not a declared token */
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

.hrc-picker-group-label:not(:first-child) {
    border-top: 1px solid var(--color-border);
    margin-top: 4px;
}

/* Give the global-search dropdown a sane width — it anchors to a
   compact topbar input, so match-input-width would be too narrow.
   340px fits a typical "email · +47 xxx xx xxx" meta line; the vw
   clamp keeps it inside a 320px viewport (no horizontal scroll). */
.topbar-search-dd {
    min-width: min(340px, calc(100vw - 24px));
    max-width: min(420px, calc(100vw - 24px));
}

/* The shared row-meta declares var(--font-size-xs), which is an
   UNDECLARED token — it falls back to the inherited 1rem and a
   typical "email · +47 xxx xx xxx" then overflows 340px. Scoped
   here rather than fixed globally so other pickers keep their
   current (long-shipped) rendering. */
.topbar-search-dd .hrc-picker-row-meta {
    font-size: var(--font-size-sm);
}

/* Small screens: pin the dropdown to the viewport instead of the
   input's left edge — picker-dd anchors at rect.left (inline style,
   hence the !important), and with the hamburger to the input's left
   an anchored 296px panel can overflow a 320px viewport. */
@media (max-width: 768px) {
    .topbar-search-dd {
        left: 12px !important;
        width: calc(100vw - 24px) !important;
        min-width: 0;
        max-width: none;
    }
}

/* Custom-fallback row (data-allow-custom="1" surfaces only) — visually
   distinct from saved-location rows so the user can tell at a glance
   "this is not a saved official location, but it's still a valid
   selectable operational area." */
.hrc-picker-row--custom {
    background: rgba(0,0,0,.02);
    border-top: 1px dashed var(--color-border, #d0d0d0);
    margin-top: 6px;
    padding-top: 10px;
    color: var(--color-text-muted);
    font-style: italic;
}
.hrc-picker-row--custom:hover,
.hrc-picker-row--custom.is-active {
    background: rgba(0,0,0,.06);
    color: var(--color-text);
}
.hrc-picker-row--custom .hrc-picker-row-name {
    font-weight: 500;
    color: inherit;
}
.hrc-picker-row--custom .hrc-picker-row-name::before {
    content: "✎";
    margin-right: 6px;
    opacity: .6;
    font-style: normal;
}

/* Custom-selected chip — matches the picker-row visual cue (small
   ✎ marker + lighter background) so the selected state echoes the
   row the user clicked. */
.hrc-picker.is-selected-custom .hrc-picker-chip {
    background: rgba(0,0,0,.03);
    font-style: italic;
}
.hrc-picker.is-selected-custom .hrc-picker-chip-label::before {
    content: "✎";
    margin-right: 6px;
    opacity: .6;
    font-style: normal;
}

@media (max-width: 640px) {
    .hrc-picker { max-width: none; }
    .hrc-picker-dd {
        max-height: 60vh;
    }
    .hrc-picker-row { padding: 10px 12px; } /* larger tap targets on mobile */
}

/* Row 2: filters — lighter, more breathing room */
.sched-toolbar-filters {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    flex-wrap: wrap;
    margin-top: var(--space-sm);
    padding-top: var(--space-sm);
    border-top: 1px solid var(--color-border);
}

.sched-filter-select {
    width: auto;
    min-width: 110px;
    max-width: 180px;
}

.sched-search-input {
    width: auto;
    flex: 1;
    min-width: 140px;
}

@media (max-width: 768px) {
    .sched-toolbar          { padding: var(--space-xs) var(--space-sm); }
    .sched-toolbar-row      { flex-wrap: wrap; justify-content: flex-start; gap: var(--space-xs); }
    .sched-toolbar-actions  { flex-wrap: wrap; }
    .sched-filter-select    { flex: 1; min-width: 90px; max-width: none; }
    .sched-search-input     { max-width: none; }
}

/* ── Starting soon section ───────────────────────────────────── */
.sched-section {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    margin-bottom: var(--space-md);
    overflow: hidden;
}

.sched-section--soon {
    border-color: var(--color-schedule-warning);
}

.sched-section-hd {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px var(--space-md);
    background: var(--color-warning-bg);
    border-bottom: 1px solid var(--color-warning-border);
}

.sched-section-title {
    font-size: var(--font-size-sm);
    font-weight: 600;
    color: var(--color-warning-text);
}

.sched-section-count {
    font-size: var(--font-size-sm);
    color: var(--color-schedule-warning-text);
}

/* ── "Starter snart og pågår" — operational attention summary ──
   Caps at 10 group rows desktop / 5 mobile (CSS presentation of
   business-approved rows; hidden_groups live in a <template> and
   never enter the live tree until [Vis alle] is clicked). */
.sched-section--soon .sched-rows {
    display: flex;
    flex-direction: column;
}

.sched-section--soon.is-expanded .sched-rows {
    max-height: 30vh;
    overflow-y: auto;
}

@media (max-width: 1023px) {
    .sched-section--soon:not(.is-expanded) .sched-rows .sched-row--top:nth-child(n+6) {
        display: none;
    }
    .sched-section--soon.is-expanded .sched-rows { max-height: 35vh; }
}

/* Aggregate group row — not an <a>, uses an explicit expand button.
   Member rows render with `hidden` until the chevron is clicked. */
.sched-row--group {
    cursor: default;
}
.sched-row--group:hover { background: transparent; }

.sched-row-expand {
    appearance: none;
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    width: 28px;
    height: 28px;
    line-height: 1;
    font-size: 1.1rem;
    color: var(--color-text-light);
    cursor: pointer;
    padding: 0;
    margin-left: 4px;
    flex-shrink: 0;
    transition: transform 0.15s ease, background 0.12s, color 0.12s;
}
.sched-row-expand:hover {
    background: var(--color-bg);
    color: var(--color-text);
}
.sched-row-expand:focus-visible {
    outline: 2px solid var(--color-border-strong);
    outline-offset: -2px;
}
.sched-row-expand[aria-expanded="true"] {
    transform: rotate(90deg);
    color: var(--color-text);
}

/* Member row (revealed when group is expanded) */
.sched-row--member {
    padding-left: calc(var(--space-md) + 16px);
    background: var(--color-bg);
    font-size: 0.82rem;
}
.sched-row--member .sched-row-time { font-size: 0.75rem; }

/* Overflow footer — `{N} vises • +{M} flere [Vis alle]` */
.sched-section-overflow {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    padding: 6px var(--space-md);
    background: var(--color-warning-bg);
    border-top: 1px solid var(--color-warning-border);
    font-size: var(--font-size-sm);
    color: var(--color-schedule-warning-text);
}

.sched-section-overflow-text {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--color-text-muted);
}

.sched-section-overflow-sep {
    color: var(--color-text-light);
}

.sched-section-overflow-btn {
    appearance: none;
    background: transparent;
    border: 1px solid var(--color-warning-border);
    border-radius: var(--radius-sm);
    padding: 4px 10px;
    font-size: var(--font-size-sm);
    font-weight: 600;
    color: var(--color-warning-text);
    cursor: pointer;
    transition: background 0.12s;
}
.sched-section-overflow-btn:hover {
    background: var(--color-surface);
}
.sched-section-overflow-btn:focus-visible {
    outline: 2px solid var(--color-border-strong);
    outline-offset: -2px;
}

.sched-section--soon.is-expanded .sched-section-overflow {
    display: none;
}

/* ── Day block ───────────────────────────────────────────────── */
.sched-day {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    margin-bottom: var(--space-sm);
    overflow: hidden;
}

/* Day header */
.sched-day-header {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    width: 100%;
    padding: var(--space-sm) var(--space-md);
    background: none;
    border: none;
    cursor: pointer;
    text-align: left;
    font-size: var(--font-size-base);
    color: var(--color-text);
    transition: background 0.12s;
    min-height: 40px;
}

.sched-day-header:hover { background: var(--color-bg); }

.sched-day-label {
    flex: 1;
    font-weight: 600;
}

.sched-day-meta {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

.sched-day-open {
    color: var(--color-schedule-warning-text);
    font-weight: 500;
}

.sched-day-chevron {
    font-size: 1.1rem;
    color: var(--color-text-light);
    transition: transform 0.15s;
    transform: rotate(90deg);
    display: inline-block;
}

.sched-day-header.is-open .sched-day-chevron { transform: rotate(270deg); }

/* Day body */
.sched-day-body { border-top: 1px solid var(--color-border); }

/* ── Day board: default stacked layout ──────────────────────────
   .sched-day-board is a plain block container by default — used
   at narrow widths for every view (1-Day, 3-Day, Week). Desktop
   breakpoints below switch the views into their lane/card grids:
   1-Day and 3-Day share the same lane system at ≥1024px (column
   count driven by --sched-day-cols); Week has its own at ≥1280px. */
.sched-day-board { display: block; }

/* Short-form day label/meta are hidden by default; the column-mode
   media queries below swap them in for 3-col and 7-col boards. */
.sched-day-label-short,
.sched-day-meta-short { display: none; }

/* ── Lane mode: 1-Day + 3-Days share at ≥1024px ────────────────
   Both views use the same lane/card system. Column count is
   driven by --sched-day-cols (set inline on the board element from
   $day_cols in PHP) so the grid handles 1 or 3 columns from the
   same rule. 1-Day gets a small breathing-room tuning block after
   for slightly larger fonts / padding — execution-workflow read
   density rather than planning-board scan density. */
@media (min-width: 1024px) {
    .sched-day-board--cols-1,
    .sched-day-board--cols-3 {
        display: grid;
        grid-template-columns: repeat(var(--sched-day-cols), minmax(0, 1fr));
        gap: var(--space-sm);
        /* `stretch` plus min-height pulls every lane to a common
           bottom edge so the per-lane vertical guide rails extend
           to the same line regardless of shift count. Lane content
           still flows from the top (flex column with no flex-grow
           on header/body), so cards continue to pile up under the
           date header rather than centering vertically. */
        align-items: stretch;
        min-height: 480px;
    }

    /* Day = lane: open surface, no fill, no chrome. The lane carries
       a single ultra-subtle vertical guide rail via `::after` —
       anchored to the lane container itself, NOT the card stack, so
       empty days still get the same temporal anchor. The rail
       starts just below the day header and extends to the bottom of
       the lane's natural height. No min-height, no equal-height
       columns — lanes still flow independently. */
    .sched-day-board--cols-1 .sched-day,
    .sched-day-board--cols-3 .sched-day {
        margin-bottom: 0;
        display: flex;
        flex-direction: column;
        min-width: 0;
        background: transparent;
        border: none;
        border-radius: 0;
        overflow: visible;
        position: relative;
    }
    .sched-day-board--cols-1 .sched-day::after,
    .sched-day-board--cols-3 .sched-day::after {
        content: '';
        position: absolute;
        top: 32px; /* approximate offset past the day header */
        bottom: 0;
        left: 0;
        width: 1px;
        background: rgba(0, 0, 0, 0.05);
        pointer-events: none;
    }

    /* Compact lane header: short date + short meta on one inline row. */
    .sched-day-board--cols-1 .sched-day-header,
    .sched-day-board--cols-3 .sched-day-header {
        cursor: default;
        pointer-events: none;
        padding: 2px var(--space-xs) 6px;
        min-height: 0;
        gap: var(--space-xs);
        font-size: var(--font-size-sm);
        background: none;
    }
    .sched-day-board--cols-1 .sched-day-header:hover,
    .sched-day-board--cols-3 .sched-day-header:hover { background: none; }
    .sched-day-board--cols-1 .sched-day-label-long,
    .sched-day-board--cols-1 .sched-day-meta-long,
    .sched-day-board--cols-3 .sched-day-label-long,
    .sched-day-board--cols-3 .sched-day-meta-long  { display: none; }
    .sched-day-board--cols-1 .sched-day-label-short,
    .sched-day-board--cols-1 .sched-day-meta-short,
    .sched-day-board--cols-3 .sched-day-label-short,
    .sched-day-board--cols-3 .sched-day-meta-short { display: inline; }
    .sched-day-board--cols-1 .sched-day-label,
    .sched-day-board--cols-3 .sched-day-label { font-weight: 600; }
    .sched-day-board--cols-1 .sched-day-meta,
    .sched-day-board--cols-3 .sched-day-meta {
        font-size: 0.75rem;
        color: var(--color-text-muted);
    }
    .sched-day-board--cols-1 .sched-day-chevron,
    .sched-day-board--cols-3 .sched-day-chevron { display: none; }
}

/* ════════════════════════════════════════════════════════════
   ROW-CARD LAYOUT — desktop lanes AND phones, never in between.
   ════════════════════════════════════════════════════════════

   The card composition below (time / title / staff+status stacked)
   was written for a NARROW lane, which is what a desktop day column
   is. A phone is the same problem: not enough horizontal room to put
   time, title, staffing and status on one line.

   Below 480px the base horizontal .sched-row starves the title. The
   arithmetic is fixed: time (100px) + staffing (70px) + padding and
   gaps (~121px) consume ~291px before the title gets anything, so
   .sched-row-main = viewport − 291px. Measured:

       320px → main  14px, title clipped, min-age badge overflows
       375px → main  69px, title clipped
       390px → main  84px, first width that works
       480px+       comfortable

   Hence max-width: 479px. Wider phones/tablets (480–1023px) keep the
   horizontal row, where it already works — this deliberately does NOT
   touch that band.

   The comma is an OR: these rules apply at BOTH ends and nowhere in
   between. Do not merge this back into the min-width block, and do not
   duplicate it into a phone-only block — one copy, two conditions.

   Board/day-COLUMN layout stays min-width:1024 only (above). That
   separation is load-bearing: the multi-column
   `grid-template-columns: repeat(var(--sched-day-cols), …)` must never
   reach a phone, or three day lanes would render side by side at 320px.
   ════════════════════════════════════════════════════════════ */
@media (max-width: 479px), (min-width: 1024px) {

    /* Lane body: vertical stack with small gaps between shift cards.
       Cards are inset 8px from the lane's left edge so the rail
       (rendered on `.sched-day::after` — see lane block above)
       reads alongside them without colliding with card edges. */
    .sched-day-board--cols-1 .sched-day-body,
    .sched-day-board--cols-3 .sched-day-body {
        display: flex !important;
        flex-direction: column;
        gap: 6px;
        border-top: none;
        padding: 0 0 0 8px;
    }

    /* Shift = compact card. Soft surface, no row dividers; the left
       accent bar (.sched-row-wrap::before) carries staffing colour.
       Wrap is `position: relative` already (legacy CSS) — we use that
       to pin the kebab to the corner. */
    .sched-day-board--cols-1 .sched-row-wrap,
    .sched-day-board--cols-3 .sched-row-wrap {
        border-bottom: none;
        background: var(--color-surface);
        border-radius: var(--radius-sm);
        overflow: hidden;
    }
    .sched-day-board--cols-1 .sched-row-wrap:last-child,
    .sched-day-board--cols-3 .sched-row-wrap:last-child { border-bottom: none; }

    /* Card grid:
         row 1: time   (kebab overlays top-right via absolute)
         row 2: title  (full width)
         row 3: staff + status  → one "card state" line
       Gives the card a native vertical composition and pairs the
       staffing count with the status pill so they read as a single
       operational indicator. */
    .sched-day-board--cols-1 .sched-row,
    .sched-day-board--cols-3 .sched-row {
        display: grid;
        grid-template-columns: 1fr auto;
        grid-template-areas:
            "time   time"
            "title  title"
            "staff  status";
        column-gap: var(--space-xs);
        row-gap: 2px;
        padding: var(--space-xs) var(--space-sm) 6px;
        padding-right: 28px; /* reserve the kebab corner */
        align-items: baseline;
    }
    .sched-day-board--cols-1 .sched-row-time,
    .sched-day-board--cols-3 .sched-row-time     { grid-area: time;   font-weight: 600; }
    .sched-day-board--cols-1 .sched-row-main,
    .sched-day-board--cols-3 .sched-row-main     { grid-area: title;  min-width: 0; }
    .sched-day-board--cols-1 .sched-row-main .sched-row-sub,
    .sched-day-board--cols-3 .sched-row-main .sched-row-sub {
        display: block;
        font-size: 0.75rem;
        color: var(--color-text-muted);
    }
    .sched-day-board--cols-1 .sched-row-staffing,
    .sched-day-board--cols-3 .sched-row-staffing { grid-area: staff;  justify-self: start; }
    .sched-day-board--cols-1 .sched-row-status,
    .sched-day-board--cols-3 .sched-row-status   { grid-area: status; justify-self: end; }
    .sched-day-board--cols-1 .sched-row-chevron,
    .sched-day-board--cols-3 .sched-row-chevron  { display: none; }

    /* Staffing number — let it use natural width and align with status
       on the bottom line. Drop the legacy fixed 36px width. */
    .sched-day-board--cols-1 .sched-bar-label,
    .sched-day-board--cols-3 .sched-bar-label {
        width: auto;
        text-align: left;
        line-height: 1.2;
    }

    /* Status pill — keep the colour but shrink to match the staffing
       indicator size so the two read as a paired state line. */
    .sched-day-board--cols-1 .sched-row-status .badge,
    .sched-day-board--cols-3 .sched-row-status .badge {
        font-size: 0.7rem;
        padding: 1px 6px;
        line-height: 1.3;
    }

    /* Kebab — absolute top-right corner, slim and muted. Removes the
       horizontal "tension column" from the right edge of the card. */
    .sched-day-board--cols-1 .sched-row-actions,
    .sched-day-board--cols-3 .sched-row-actions {
        position: absolute;
        top: 2px;
        right: 2px;
        padding: 0;
        border-left: none;
        background: transparent;
    }
    .sched-day-board--cols-1 .sched-kebab-btn,
    .sched-day-board--cols-3 .sched-kebab-btn {
        font-size: 0.85rem;
        padding: 2px 4px;
        color: var(--color-text-light);
        opacity: 0.7;
    }
    .sched-day-board--cols-1 .sched-kebab-btn:hover,
    .sched-day-board--cols-3 .sched-kebab-btn:hover { opacity: 1; }

    /* Softer empty-day placeholder. */
    .sched-day-board--cols-1 .sched-day-body:empty::before,
    .sched-day-board--cols-3 .sched-day-body:empty::before {
        content: attr(data-empty-label);
        display: block;
        padding: var(--space-md) 0;
        color: var(--color-text-light);
        font-size: var(--font-size-sm);
        font-style: italic;
        text-align: center;
    }
}

/* Per-view tuning stays DESKTOP-ONLY. These adjust type scale and
   padding for a 1-lane vs 3-lane desktop board; a phone renders one
   full-width lane and uses the base card sizing above. */
@media (min-width: 1024px) {

    /* ── 1-Day tuning: more horizontal room + slightly more
       breathing room than 3-Days. Same composition, looser
       padding + slightly larger type for execution readability.
       Stops short of legacy "wide list row" density. */
    .sched-day-board--cols-1 .sched-day-header {
        padding: 4px var(--space-sm) 8px;
        gap: var(--space-sm);
        font-size: var(--font-size-base);
    }
    .sched-day-board--cols-1 .sched-day-meta {
        font-size: var(--font-size-sm);
    }
    .sched-day-board--cols-1 .sched-day-body { gap: 8px; }
    .sched-day-board--cols-1 .sched-row {
        column-gap: var(--space-sm);
        row-gap: 3px;
        padding: var(--space-sm) var(--space-md) var(--space-sm);
        padding-right: 36px;
    }
    .sched-day-board--cols-1 .sched-row-time {
        font-size: var(--font-size-base);
    }
    .sched-day-board--cols-1 .sched-row-title {
        font-size: var(--font-size-base);
    }
    .sched-day-board--cols-1 .sched-row-main .sched-row-sub {
        font-size: var(--font-size-sm);
    }
    .sched-day-board--cols-1 .sched-row-status .badge {
        font-size: 0.75rem;
        padding: 1px 7px;
    }
    .sched-day-board--cols-1 .sched-kebab-btn {
        font-size: 0.95rem;
        padding: 2px 5px;
    }

    /* ── 3-Days tuning: closer to 1-Day visual scale (~90-95%)
       than the shared cols-1/cols-3 base, while staying tighter
       so the columns still read as a lane rather than a list.
       Bumps title/time/sub typography and adds vertical
       breathing room (body gap, row padding-y, row-gap). */
    .sched-day-board--cols-3 .sched-day-body {
        gap: 7px;
    }
    .sched-day-board--cols-3 .sched-row {
        column-gap: var(--space-sm);
        row-gap: 3px;
        padding: 6px 12px 8px;
        padding-right: 32px;
    }
    .sched-day-board--cols-3 .sched-row-time {
        font-size: 0.95rem; /* 15.2px — ~95% of 1-day's base */
    }
    .sched-day-board--cols-3 .sched-row-title {
        font-size: 0.95rem; /* 15.2px */
    }
    .sched-day-board--cols-3 .sched-row-main .sched-row-sub {
        font-size: 0.8125rem; /* 13px — between base 12 and 1-day's 14 */
    }
}

/* ── Worker "My Shifts" cards (/me/shifts) ───────────────────
   Worker cards ARE schedule rows: .sched-row.sched-row--worker inside the
   normal .sched-row-wrap, so they inherit the Schedule board's typography,
   padding, density and per-view (1/3/7-day) scaling verbatim — a filtered
   view of Schedule, not a separate component. Only three things differ for
   workers (Schedule/admin row semantics are untouched):
     1. a full-width action row so the Sign up / Request / Withdraw button
        never overflows a narrow column,
     2. the left accent bar communicates the worker's RELATIONSHIP to the
        shift (green = my confirmed, amber = my pending, neutral = available)
        instead of staffing level,
     3. a greyed "locked" state for a held shift that doesn't match the
        active role filter. */

/* Relationship accent on the shared 3px left bar (worker rows only).
   Order matters: the neutral rule also matches confirmed/requested rows
   (they carry .sched-row--worker too), so the specific colours follow it. */
.sched-row-wrap:has(.sched-row--worker)::before           { background: var(--color-border); }   /* neutral = available */
.sched-row-wrap:has(.sched-row--worker-confirmed)::before { background: var(--color-success); }   /* green  = my confirmed shift */
.sched-row-wrap:has(.sched-row--worker-requested)::before { background: var(--color-warning); }   /* amber  = my pending request */

/* Full-width action line. In the grid views it auto-places onto its own
   implicit full-width row only when present (confirmed cards have none, so
   no empty gap); in the mobile/stacked flex layout it wraps to full width. */
.sched-row--worker { flex-wrap: wrap; }
.sched-row--worker .sched-row-action {
    grid-column: 1 / -1;   /* grid views (1/3/7-day) */
    flex: 1 0 100%;        /* mobile/stacked flex */
    margin-top: 4px;
}
.sched-row--worker .sched-row-action form { margin: 0; }
.sched-row--worker .sched-row-action .btn { width: 100%; }

/* Off-filter held shift: greyed/locked, still clearly the worker's own. */
.sched-row-wrap.is-locked { opacity: 0.55; }
.sched-row-wrap.is-locked .sched-row { cursor: default; }

/* Read-only details modal — definition list rows. */
.me-shift-details { margin: 0; }
.me-shift-detail-row {
    display: flex;
    gap: var(--space-md);
    padding: 6px 0;
    border-bottom: 1px solid var(--color-border);
}
.me-shift-detail-row:last-child { border-bottom: none; }
.me-shift-detail-label {
    flex: 0 0 38%;
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
}
.me-shift-detail-value {
    flex: 1 1 auto;
    overflow-wrap: anywhere;
}
/* Long free-text fields (description) stack label-over-value, full width. */
.me-shift-detail-row--block { flex-direction: column; gap: 2px; }
.me-shift-detail-row--block .me-shift-detail-value { white-space: pre-line; }

/* ── Lane mode: Week view (7 cols) activates at 1280px ─────────
   Below 1280px, .sched-day-board--cols-7 falls through to the
   stacked block layout (same as mobile). Week mode is denser than
   3-Days: smaller typography, tighter padding, less metadata noise. */
@media (min-width: 1280px) {
    .sched-day-board--cols-7 {
        display: grid;
        grid-template-columns: repeat(7, minmax(0, 1fr));
        gap: var(--space-xs);
        /* Same stretch + min-height treatment as 3-Days so all 7
           lane rails extend to a common bottom in week view. */
        align-items: stretch;
        min-height: 480px;
    }

    .sched-day-board--cols-7 .sched-day {
        margin-bottom: 0;
        display: flex;
        flex-direction: column;
        min-width: 0;
        background: transparent;
        border: none;
        border-radius: 0;
        overflow: visible;
        position: relative;
    }
    .sched-day-board--cols-7 .sched-day::after {
        content: '';
        position: absolute;
        top: 26px; /* approximate offset past the denser week-mode header */
        bottom: 0;
        left: 0;
        width: 1px;
        background: rgba(0, 0, 0, 0.05);
        pointer-events: none;
    }

    .sched-day-board--cols-7 .sched-day-header {
        cursor: default;
        pointer-events: none;
        padding: 2px var(--space-xs) 4px;
        min-height: 0;
        gap: 4px;
        font-size: 0.8rem;
        background: none;
    }
    .sched-day-board--cols-7 .sched-day-header:hover { background: none; }
    .sched-day-board--cols-7 .sched-day-label-long,
    .sched-day-board--cols-7 .sched-day-meta-long  { display: none; }
    .sched-day-board--cols-7 .sched-day-label-short,
    .sched-day-board--cols-7 .sched-day-meta-short { display: inline; }
    .sched-day-board--cols-7 .sched-day-label { font-weight: 600; }
    .sched-day-board--cols-7 .sched-day-meta {
        font-size: 0.7rem;
        color: var(--color-text-muted);
    }
    .sched-day-board--cols-7 .sched-day-chevron { display: none; }
}

/* ════════════════════════════════════════════════════════════
   WEEK-VIEW ROW-CARD LAYOUT — wide desktop AND phones.
   ════════════════════════════════════════════════════════════

   Same split, and the same reason, as the cols-1/cols-3 block above:
   the card composition belongs wherever the LANE is narrow, and the
   viewport query is only a proxy for lane width.

   For a 7-lane board that means TWO ends, and they are not the same
   ends as cols-1/cols-3:

     >= 1280px  board becomes 7 columns -> each lane ~117px  -> cards
     480-1279   board is ONE full-width lane -> main 327-582px -> row
     <= 479px   lane is the whole phone -> main 14px          -> cards

   The middle band is genuinely healthy and is deliberately left alone.
   Measured on the deployed build before this change: at 1024/1100/1279
   main was 327/403/582px with no clipping and no age overflow, so
   folding cols-7 into the cols-1/3 block (whose lower end is 1024px)
   would have converted a working band into stacked cards. That is why
   this is a separate block with a 1280px upper end rather than an
   extra selector over there.

   At 320px before this: main 14px, title clipped, and 6 of 6 age
   badges overflowing their container — the same defect the cols-1/3
   split fixed, left behind because the narrow block never listed
   cols-7.

   Board-level rules stay at min-width:1280 (above). The 7-column
   `grid-template-columns` must never reach a phone.
   ════════════════════════════════════════════════════════════ */
@media (max-width: 479px), (min-width: 1280px) {

    .sched-day-board--cols-7 .sched-day-body {
        display: flex !important;
        flex-direction: column;
        gap: 5px;
        border-top: none;
        padding: 0 0 0 6px;
    }

    .sched-day-board--cols-7 .sched-row-wrap {
        border-bottom: none;
        background: var(--color-surface);
        border-radius: var(--radius-sm);
        overflow: hidden;
    }
    .sched-day-board--cols-7 .sched-row-wrap:last-child { border-bottom: none; }

    /* Card grid (week): same composition as 3-Days but denser type
       and tighter padding. Kebab pinned to corner. Typography sits
       at ~85-88% of 1-Day visual scale — clearly compact, still
       legible, preserves information density across 7 columns. */
    .sched-day-board--cols-7 .sched-row {
        display: grid;
        /* minmax(0,1fr): the flexible column may shrink below its content so the
           card grid never exceeds the narrow 7-day lane (was 1fr → its
           min-content floor + the auto status column overflowed the lane and
           clipped the status badge bottom-right). Applies to BOTH the manager
           Schedule and worker /me/shifts cards. */
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas:
            "time   time"
            "title  title"
            "staff  status";
        column-gap: 5px;
        row-gap: 2px;
        /* Symmetric padding — the kebab is absolutely positioned at the top-
           right corner (over the empty space beside the left-aligned time), so
           the row needs no right reserve. Dropping it lets the footer status
           (Published / Confirmed) align cleanly to the card's right edge
           instead of sitting ~16px in. */
        padding: 4px 8px 6px;
        align-items: baseline;
    }
    .sched-day-board--cols-7 .sched-row-time {
        grid-area: time;
        font-weight: 600;
        font-size: 0.85rem; /* 13.6px — ~85% of 1-day's base */
    }
    /* overflow:hidden so the single-line (nowrap) sub clips with its ellipsis
       instead of forcing the card grid to its full text width — the other half
       of the lane-fit fix, shared by manager + worker cards. */
    .sched-day-board--cols-7 .sched-row-main     { grid-area: title;  min-width: 0; overflow: hidden; }
    /* Title may wrap to 2 lines before truncating — uses the lane's vertical
       space instead of cutting titles short at one line. */
    .sched-day-board--cols-7 .sched-row-title    {
        font-size: 0.85rem;
        line-height: 1.3;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
        white-space: normal;   /* override base .sched-row-title nowrap so it can wrap to 2 lines */
        text-overflow: clip;
    }
    /* Location / role stays on a single muted line below the title. */
    .sched-day-board--cols-7 .sched-row-main .sched-row-sub {
        display: block;
        font-size: 0.75rem; /* 12px — bumped from 0.65rem (10.4px) for legibility */
        color: var(--color-text-muted);
        line-height: 1.3;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    /* Footer row: capacity (left) + status badge / action (right), aligned. */
    .sched-day-board--cols-7 .sched-row-staffing { grid-area: staff;  justify-self: start; align-self: center; }
    .sched-day-board--cols-7 .sched-row-status   { grid-area: status; justify-self: end;   align-self: center; }
    .sched-day-board--cols-7 .sched-row-chevron  { display: none; }

    .sched-day-board--cols-7 .sched-bar-label {
        width: auto;
        text-align: left;
        line-height: 1.2;
        font-size: 0.75rem; /* 12px — was 0.7rem (11.2px) */
    }
    .sched-day-board--cols-7 .sched-row-status .badge {
        font-size: 0.7rem; /* 11.2px — was 0.65rem (10.4px) */
        padding: 1px 6px;
        line-height: 1.3;
    }

    .sched-day-board--cols-7 .sched-row-actions {
        position: absolute;
        top: 1px;
        right: 1px;
        padding: 0;
        border-left: none;
        background: transparent;
    }
    .sched-day-board--cols-7 .sched-kebab-btn {
        font-size: 0.8rem;
        padding: 1px 3px;
        color: var(--color-text-light);
        opacity: 0.6;
    }
    .sched-day-board--cols-7 .sched-kebab-btn:hover { opacity: 1; }

    .sched-day-board--cols-7 .sched-day-body:empty::before {
        content: attr(data-empty-label);
        display: block;
        padding: var(--space-sm) 0;
        color: var(--color-text-light);
        font-size: 0.75rem;
        font-style: italic;
        text-align: center;
    }

    /* Week view, WORKER cards only. The grid-shrink (minmax(0,1fr)) and the
       sub-clip (.sched-row-main overflow:hidden) now live on the shared base
       cols-7 rules above, so both boards fit the lane. Worker cards add: no
       kebab → reclaim the right padding; badges never exceed the card. */
    .sched-day-board--cols-7 .sched-row--worker { padding-right: var(--space-sm); }
    /* Status badges (Confirmed / Requested) stay on the capacity footer row
       (capacity left, badge right) — they never use the full-width layout. */
    .sched-day-board--cols-7 .sched-row--worker .sched-row-status .badge { max-width: 100%; }
    /* ONLY the actionable button (Sign up / Request) goes full-width, on its
       own final row below capacity — reliable in narrow lanes. */
    .sched-day-board--cols-7 .sched-row--worker .sched-row-action--select {
        grid-column: 1 / -1;
        justify-self: stretch;
        align-self: auto;
        margin-top: 4px;
        min-width: 0;
    }
    .sched-day-board--cols-7 .sched-row--worker .sched-row-action--select .btn { width: 100%; max-width: 100%; }
    /* Withdraw (pending) stays in the details modal to keep the lane clean. */
    .sched-day-board--cols-7 .sched-row--worker .sched-row-action--withdraw { display: none; }
}

/* ── Ultra-wide: 7-day view matches 3-day typography ───────────
   At ≥1800px the 7 columns have enough horizontal room that the
   compact pass from the 1280px block above becomes unnecessarily
   dense. Restore 3-day-equivalent type, padding and gaps. The
   7-column grid template (repeat(7, …)) stays from the 1280px
   block — only typography/spacing is restored, day count is
   unchanged. Mirrors values from the cols-3 block (lines 5607–
   5717) so the two views read at parity on wide displays. */
@media (min-width: 1800px) {
    .sched-day-board--cols-7 .sched-day-header {
        padding: var(--space-xs) var(--space-sm) 6px;
        font-size: var(--font-size-base);
    }
    .sched-day-board--cols-7 .sched-day-meta {
        font-size: var(--font-size-sm);
    }
    .sched-day-board--cols-7 .sched-day::after {
        top: 28px;
    }

    .sched-day-board--cols-7 .sched-day-body {
        gap: 7px;
        padding: 0 0 0 8px;
    }

    .sched-day-board--cols-7 .sched-row {
        column-gap: var(--space-sm);
        row-gap: 3px;
        /* Symmetric padding (see 1280px block) — no kebab right-reserve, so the
           footer status aligns cleanly to the card edge at ultra-wide too. */
        padding: 6px 12px 8px;
    }
    /* Mirror the cols-3 tuning block — at ≥1800px the 7 columns
       read at parity with the 3-Day view. Values must track the
       cols-3 tuning above (search "3-Days tuning") so the two
       views stay aligned when 3-day spec changes. */
    .sched-day-board--cols-7 .sched-row-time {
        font-size: 0.95rem; /* matches cols-3 .sched-row-time */
    }
    .sched-day-board--cols-7 .sched-row-title {
        font-size: 0.95rem; /* matches cols-3 .sched-row-title */
        line-height: inherit;
    }
    .sched-day-board--cols-7 .sched-row-main .sched-row-sub {
        font-size: 0.8125rem; /* matches cols-3 .sched-row-sub */
        line-height: 1.4;
    }

    .sched-day-board--cols-7 .sched-bar-label {
        font-size: 0.75rem; /* matches base .sched-bar-label @ line 6207 (cols-3 doesn't override) */
    }
    .sched-day-board--cols-7 .sched-row-status .badge {
        font-size: 0.7rem;
        padding: 1px 6px;
        line-height: 1.3;
    }

    .sched-day-board--cols-7 .sched-row-actions {
        top: 2px;
        right: 2px;
    }
    .sched-day-board--cols-7 .sched-kebab-btn {
        font-size: 0.85rem;
        padding: 2px 4px;
    }

    .sched-day-board--cols-7 .sched-day-body:empty::before {
        padding: var(--space-md) 0;
        font-size: var(--font-size-sm);
    }
}

/* ── Shift detail modal ─────────────────────────────────────────
   Each shift card has a sibling .modal-overlay using the global
   modal system (centered workspace, calm backdrop, ESC + close
   button, mobile-responsive sizing). This file only adds the
   small structural pieces specific to the shift surface:
     • header layout: time + status row above the title,
       location/role/project meta line below
     • body section dividers (Assigned ↔ Staffing)
     • inline section label ("Staffing")
   Modal sizing is set via inline style="max-width:880px" on the
   .modal element — comfortably wider than the global 520px
   default so the staffing editor has horizontal room.

   Reuses .modal-overlay, .modal, .modal-header, .modal-body,
   .modal-footer, .modal-close, .modal-title — see the global
   modal block earlier in this file for shell behaviour. */

.sched-shift-modal .modal-header {
    align-items: flex-start;
    gap: var(--space-md);
}
.sched-shift-modal-title-block {
    flex: 1;
    min-width: 0;
}
.sched-shift-modal-meta-top {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    margin-bottom: 6px;
    flex-wrap: wrap;
}
.sched-shift-modal-time {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.sched-shift-modal .modal-title {
    line-height: 1.25;
    word-wrap: break-word;
}
.sched-shift-modal-meta-sub {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    margin-top: 4px;
    line-height: 1.35;
}

/* Section dividers between Assigned and Staffing inside the body.
   Each direct child after the first gets a top rule + matching
   padding so the sections feel like distinct operational blocks. */
.sched-shift-modal .modal-body > * + * {
    margin-top: var(--space-lg);
    padding-top: var(--space-lg);
    border-top: 1px solid var(--color-border);
}

/* "Staffing" section label — small caps-like anchor for the
   need-text + assign picker block. */
.sched-shift-modal .sched-panel-section-title {
    display: block;
    font-size: var(--font-size-sm);
    font-weight: 600;
    color: var(--color-text);
    margin-bottom: 6px;
}
.sched-shift-modal .sched-staffing-need {
    margin-bottom: var(--space-sm);
}

/* Per-worker group picker under the assign control. Sits below the
   picker+button row so the manager sets who first, then where they
   land. Full-width select on all viewports. */
.sched-assign-group-row {
    margin-top: var(--space-sm);
}
.sched-assign-group-row .form-label {
    display: block;
    margin-bottom: 4px;
    color: var(--color-text-muted);
}
.sched-assign-group-row .sched-assign-group-select {
    width: 100%;
}

/* Group sub-heading */
.sched-group-head {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: 5px var(--space-md);
    background: var(--color-bg);
    border-bottom: 1px solid var(--color-border);
}

.sched-group-name {
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
    flex: 1;
}

.sched-group-count {
    font-size: 0.78rem;
    color: var(--color-text-light);
    background: var(--color-border);
    border-radius: 99px;
    padding: 0 6px;
    min-width: 20px;
    text-align: center;
}

/* ── Row wrapper — holds the <a> + actions button ────────────── */
.sched-row-wrap {
    display: flex;
    align-items: stretch;
    border-bottom: 1px solid var(--color-border);
    position: relative;
}

.sched-row-wrap:last-child { border-bottom: none; }

/* Left state accent */
.sched-row-wrap::before {
    content: '';
    display: block;
    width: 3px;
    flex-shrink: 0;
    background: transparent;
    border-radius: 0;
}

.sched-row-wrap:has(.sched-row--state-urgent)::before  { background: var(--color-schedule-urgent); }  /* 0–49% red */
.sched-row-wrap:has(.sched-row--state-partial)::before { background: var(--color-schedule-partial); }  /* 50–79% orange */
.sched-row-wrap:has(.sched-row--state-good)::before    { background: var(--color-border); }  /* 80–99% neutral */
.sched-row-wrap:has(.sched-row--state-full)::before    { background: var(--color-schedule-good); }  /* 100% green — fully staffed */
.sched-row-wrap:has(.sched-row--state-over)::before    { background: var(--color-schedule-urgent); }  /* overbooked — red emphasis */

/* Main row — now an <a> */
.sched-row {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding: 8px var(--space-sm) 8px var(--space-md);
    text-decoration: none;
    color: var(--color-text);
    transition: background 0.1s;
    font-size: var(--font-size-sm);
    flex: 1;
    min-width: 0;
    /* button reset — rows are buttons now (clickable to expand) */
    background: transparent;
    border: 0;
    text-align: left;
    font-family: inherit;
    cursor: pointer;
    width: 100%;
}

/* REQUIRED - see the note on .flash[hidden]. Grouped rows are collapsed by
   setting `hidden` on each .sched-row--member, and the author `display`
   above made that inert: a collapsed group still rendered every member.
   Do not drop this rule while .sched-row sets display. */
.sched-row[hidden] { display: none; }

.sched-row:hover { background: var(--color-bg); text-decoration: none; }
.sched-row:focus-visible {
    outline: 2px solid var(--color-border-strong);
    outline-offset: -2px;
}

/* Chevron flips when the row is expanded */
.sched-row[aria-expanded="true"] .sched-row-chevron {
    transform: rotate(180deg);
}
.sched-row-chevron { transition: transform 0.15s ease; }
/* Expanded row keeps the row itself white — the chevron rotation is enough
   visual signal that it's open. */
.sched-row[aria-expanded="true"] { background: var(--color-surface); }

/* Also used directly in "starting soon" section (no wrapper) */
.sched-section .sched-row {
    border-bottom: 1px solid var(--color-warning-border);
}
.sched-section .sched-row:last-child { border-bottom: none; }

.sched-row-time {
    width: 116px;
    flex-shrink: 0;
    white-space: nowrap;
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
    font-size: 0.8rem;
    display: flex;
    align-items: center;
    gap: 5px;
}

.sched-row-main {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.sched-row-title {
    font-weight: 500;
    color: var(--color-text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.sched-row-sub {
    font-size: 0.78rem;
    color: var(--color-text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ── Shift minimum-age indicator ──────────────────────────────
   The meta line and the age share one row, but they are NOT peers:
   location/role describe the shift, min_age constrains WHO MAY WORK IT.
   So the age is a sibling of the meta text, never joined into the
   ' · ' string — that keeps it out of the ellipsis. The text may
   shrink to nothing; the age never shrinks.

       [ location · role ……………… ][20+]
          flex: 0 1 auto            flex-shrink: 0

   Deliberately NOT a pill: .sched-bar-label (staffing X/Y) is already
   the pill on this row, and a second pill vocabulary would make "pill"
   stop meaning anything. Weight + full-strength colour against muted
   neighbours is what makes it scannable. Deliberately NOT colour-coded
   by value either — 18 and 20 are both legitimate, neither is "worse",
   and a colour scale breaks the moment a 16 or 21 appears.

   Two texts, one rule. The sketch above is the OPERATOR board. Me → Shifts
   reuses this exact markup but renders the spelled-out worker suffix from
   format_min_age_suffix() — [ location · role …… ][(age 20+)] — which is
   wider, so the meta text ellipsizes sooner there. Intended: flex-shrink:0
   means the constraint is never the thing that gets cut. */
.sched-row-subline {
    display: flex;
    align-items: baseline;
    gap: 6px;
    min-width: 0;
}

/* min-width:0 is what actually permits shrinking below content size
   inside a flex parent — without it the ellipsis never engages and the
   row overflows instead. Scoped to the wrapper so the three other
   .sched-row-sub call sites keep their existing block behaviour. */
.sched-row-subline > .sched-row-sub {
    flex: 0 1 auto;
    min-width: 0;
}

.sched-row-age {
    flex-shrink: 0;
    white-space: nowrap;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--color-text);
}

/* Me -> Shifts puts the age on the TITLE line (owner, 2026-08-24). The
   spelled-out worker suffix is wide, and in the week view's ~145px lane
   it was ellipsizing the meta strip to roughly half its text while the
   title line sat nearly empty. Beside the title it spends space that was
   going unused, and the meta gets its full width back.

   Same flex mechanics as .sched-row-subline, and the same priority: the
   age never shrinks, its NEIGHBOUR ellipsizes. Only the neighbour
   changed - the meta strip before, the title now.

   The OPERATOR board is untouched. It keeps the terse "20+" in the
   subline, so it never gets this wrapper. The two surfaces are now
   deliberately different in structure; do not "align" them. */
.sched-row-titleline {
    display: flex;
    align-items: baseline;
    gap: 6px;
    min-width: 0;
}
/* min-width:0 is what permits shrinking below content size inside a flex
   parent - without it the title's ellipsis never engages and the lane
   overflows instead. Same reason it is set on .sched-row-subline's sub. */
.sched-row-titleline > .sched-row-title {
    flex: 0 1 auto;
    min-width: 0;
}

/* The week board's 7 lanes are ~139px, and there the 6px gap is the 2px
   that decides whether a 9-character single-word title clips: the cols-7
   title is white-space: normal / text-overflow: clip, so a title with no
   break opportunity cannot wrap and is shaved instead of ellipsized.
   Tightened only here, the same way cols-7 already scales its own type.
   Measured alternative: flex-wrap on the title line also removes the
   clip, but drops the age to its own line and costs 23px of height on
   EVERY age-restricted row in the densest view. Not worth 2px. */
.sched-day-board--cols-7 .sched-row-titleline { gap: 4px; }

/* ── Staffing bar ────────────────────────────────────────────── */
.sched-row-staffing {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
    width: 100px;
}

.sched-bar {
    flex: 1;
    height: 5px;
    background: var(--color-border);
    border-radius: 99px;
    overflow: hidden;
}

.sched-bar-fill {
    height: 100%;
    border-radius: 99px;
    transition: width 0.2s;
}

.sched-bar-fill--full    { background: var(--color-schedule-good); }
.sched-bar-fill--partial { background: var(--color-schedule-partial); }
.sched-bar-fill--urgent  { background: var(--color-schedule-urgent); }
.sched-bar-fill--no_cap  { background: var(--color-border); }
.sched-bar-fill--cancelled,
.sched-bar-fill--completed { background: var(--color-border); }

.sched-bar-label {
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    width: 36px;
    text-align: right;
    flex-shrink: 0;
}

/* Ratio text is neutral for every live staffing state — color is reserved
   for the one anomaly (overstaffed >100%). Staffing status is carried by the
   card border, not the count. Scan borders for gaps; red text = exception. */
.sched-bar-label--urgent,
.sched-bar-label--partial,
.sched-bar-label--good,
.sched-bar-label--full    { color: var(--color-text); }
.sched-bar-label--over    { color: var(--color-schedule-urgent-text); font-weight: 700; }  /* overbooked — red emphasis */
.sched-bar-label--no_cap,
.sched-bar-label--cancelled,
.sched-bar-label--completed { color: var(--color-text-muted); }  /* n/a — de-emphasized */

/* Held seats (pending registrations / shift requests, migration 446).
   Distinct from the X/Y on purpose: X stays "people actually on the
   shift"; the chip is capacity promised but not yet assigned. */
.sched-held-chip {
    display: inline-block;
    font-size: 0.7rem;
    line-height: 1.4;
    padding: 0 5px;
    border: 1px dashed var(--color-border-strong, var(--color-border));
    border-radius: 999px;
    color: var(--color-text-muted);
    white-space: nowrap;
    flex-shrink: 0;
}

/* ── Status chip ─────────────────────────────────────────────── */
.sched-row-status {
    flex-shrink: 0;
    width: 80px;
    text-align: right;
}

/* ── Chevron ─────────────────────────────────────────────────── */
.sched-row-chevron {
    color: var(--color-text-light);
    font-size: 1rem;
    flex-shrink: 0;
    opacity: 0.5;
}

.sched-row:hover .sched-row-chevron { opacity: 1; }

/* ── Row actions (expand btn + kebab) ────────────────────────── */
.sched-row-actions {
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 0 var(--space-xs);
    flex-shrink: 0;
    border-left: 1px solid var(--color-border);
}

.sched-expand-btn {
    background: none;
    border: none;
    padding: 4px 6px;
    cursor: pointer;
    font-size: 0.75rem;
    color: var(--color-text-muted);
    line-height: 1;
    border-radius: var(--radius-sm);
}
.sched-expand-btn:hover { color: var(--color-text); background: var(--color-bg); }

/* ── Kebab menu ──────────────────────────────────────────────── */
.sched-kebab-wrap { position: relative; }

.sched-kebab-btn {
    background: none;
    border: none;
    padding: 4px 6px;
    cursor: pointer;
    font-size: 1rem;
    color: var(--color-text-muted);
    line-height: 1;
    border-radius: var(--radius-sm);
    letter-spacing: 0.05em;
}
.sched-kebab-btn:hover { color: var(--color-text); background: var(--color-bg); }

.sched-kebab-menu {
    display: none;
    position: fixed; /* overridden per-open by JS — kept here as fallback */
    right: 0;
    top: calc(100% + 4px);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    z-index: 1000;
    min-width: 140px;
    overflow: hidden;
}

.sched-kebab-menu.open { display: block; }

.sched-kebab-item,
.sched-kebab-menu form button.sched-kebab-item {
    display: block;
    width: 100%;
    padding: 8px var(--space-md);
    font-size: var(--font-size-sm);
    color: var(--color-text);
    text-decoration: none;
    background: none;
    border: none;
    text-align: left;
    cursor: pointer;
    transition: background 0.1s;
}
.sched-kebab-item:hover { background: var(--color-bg); text-decoration: none; }
.sched-kebab-menu form { margin: 0; }

/* ── Overnight badge (+1) ────────────────────────────────────── */
.overnight-badge {
    font-size: 9px;
    font-weight: 600;
    color: var(--color-text-muted);
    vertical-align: super;
    margin-left: 1px;
    letter-spacing: 0;
}

/* ── Live dot (ongoing) ──────────────────────────────────────── */
.sched-live-dot {
    display: inline-block;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--color-schedule-urgent);
    animation: sched-pulse 1.5s infinite;
    flex-shrink: 0;
}

@keyframes sched-pulse {
    0%, 100% { opacity: 1; }
    50%       { opacity: 0.35; }
}

/* Legacy `.sched-expand-body` inline-expand base styles and the
   90px row-title alignment indent were removed when the unified
   detail-surface (mobile bottom sheet + desktop right-side
   inspector) replaced inline accordion expansion in every
   viewport. All structural rules for .sched-expand-body and its
   inner sections now live in the "Shift detail surface" block
   earlier in this file. */

/* Assigned section — label + list. */
.sched-expand-section {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

/* Softer label — sentence case, no caps/letter-spacing. Feels like
   editorial caption rather than a banner heading. */
.sched-expand-label {
    font-size: 0.78rem;
    color: var(--color-text-muted);
    font-weight: 500;
}

/* Assignee list — structured grid rows, full width of the expand-body
   content column so rows feel like part of the same layout as the shift
   row above them (not loose form fields dropped under it). */
.sched-assignees {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    width: 100%;
}
/* Base row — 4 columns: name (caps at content width), group select (fixed),
   1fr spacer, then the action. The name + select sit close together on the
   left (middle anchor), spacer absorbs the free space, action pins to the
   right edge. No row background by default — subtle hover tint only. */
.sched-assignee-row {
    display: grid;
    grid-template-columns: minmax(220px, max-content) 280px 1fr auto;
    gap: 12px;
    align-items: center;
    min-height: 40px;
    padding: 4px 0;
    margin: 0;
    border-bottom: 1px solid var(--color-border);
    border-radius: 0;
    background: transparent;
    transition: background 0.15s ease;
}
.sched-assignee-row:last-child { border-bottom: none; }
.sched-assignee-row:first-child { border-top: 0; }
.sched-assignee-row:hover {
    background: color-mix(in srgb, var(--color-text) 2%, transparent);
}
/* Remove button on the main page is a bare <button> (3rd grid child) — force
   it to the last column so col 3 stays an empty 1fr spacer. */
.sched-assignee-row > .sched-assignee-remove {
    grid-column-start: 4;
    justify-self: end;
}
.sched-assignee-main {
    min-width: 0;
    display: flex;
    align-items: baseline;
    gap: 8px;
    flex-wrap: wrap;
    justify-self: start;
}
.sched-assignee-group {
    min-width: 0;
    width: 100%;
    display: flex;
    align-items: center;
}
.sched-assignee-status {
    display: flex;
    align-items: center;
    justify-content: center;
}
.sched-assignee-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    justify-self: end;
}
.sched-assignee-name {
    font-weight: 600;
    font-size: 0.9rem;
    color: var(--color-text);
    text-decoration: none;
    transition: color 0.15s ease;
}
.sched-assignee-name:hover {
    text-decoration: underline;
}
.sched-assignee-meta {
    font-size: 0.78rem;
    color: var(--color-text-muted);
}
/* Inline "group for this shift" editor — fills the group column. */
.sched-assignee-group-form {
    margin: 0;
    display: inline-flex;
    align-items: center;
    line-height: 1.4;
    width: 100%;
}
.sched-assignee-group-select {
    font-size: 0.8rem;
    font-family: inherit;
    color: var(--color-text);
    padding: 5px 28px 5px 10px;
    border: 1px solid var(--color-border);
    background-color: var(--color-surface-alt, var(--color-surface));
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' stroke='%23666' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>");
    background-repeat: no-repeat;
    background-position: right 8px center;
    border-radius: var(--radius-md);
    cursor: pointer;
    width: 100%;
    line-height: 1.3;
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
    appearance: none;
    -webkit-appearance: none;
    transition: border-color 0.15s ease, background-color 0.15s ease;
}
.sched-assignee-group-select:hover {
    border-color: var(--color-border-strong);
}
.sched-assignee-group-select:focus {
    border-color: var(--color-border-strong);
    background-color: var(--color-surface);
    outline: none;
}
.sched-assignee-group-select option[disabled] {
    color: var(--color-text-muted);
}

/* Detail-page variant — same left-anchored pattern, extra columns on the
   right for status (90px), the QR action wrapper, and the remove button.
   The remove button is now a direct grid child (not nested inside actions)
   so mobile can split it out onto the name row. 6 cols: name, group,
   spacer(1fr), status, actions, remove. Remove button is always visible
   since it's a first-class action on this view. */
.sched-assignees-detail .sched-assignee-row {
    grid-template-columns: minmax(220px, max-content) 280px 1fr 90px auto auto;
    min-height: 44px;
    padding: 4px 0;
}
.sched-assignees-detail .sched-assignee-status {
    grid-column-start: 4;
    justify-self: center;
}
.sched-assignees-detail .sched-assignee-actions {
    grid-column-start: 5;
    justify-self: end;
}
.sched-assignees-detail .sched-assignee-row > .sched-assignee-remove {
    grid-column-start: 6;
    justify-self: end;
    opacity: 1;
}

/* Top-of-card QR action buttons (detail page). Inline on desktop, stacked to
   a full-width 2-col grid on mobile so neither button gets cramped. */
.sched-qr-actions {
    display: flex;
    gap: 6px;
}
.sched-qr-actions > form { margin: 0; }
.sched-qr-actions > form > .btn,
.sched-qr-actions > .btn {
    white-space: nowrap;
}

/* Mobile — 2-row stacked layout on the main schedule: name + remove on
   row 1 (× reads clearly as a row-level action on this person, not as a
   panel collapse), group select full-width on row 2. Detail page keeps
   its 3-row layout with status + actions at the bottom. */
@media (max-width: 768px) {
    /* Main schedule: 2 rows — [name | ×] / [select spanning]. */
    .sched-assignees:not(.sched-assignees-detail) .sched-assignee-row {
        grid-template-columns: 1fr auto;
        grid-template-areas:
            "name   actions"
            "group  group";
        gap: 4px 8px;
        padding: 10px 0;
        min-height: 0;
    }
    .sched-assignees:not(.sched-assignees-detail) .sched-assignee-main {
        grid-area: name;
        justify-self: start;
        align-self: center;
    }
    .sched-assignees:not(.sched-assignees-detail) .sched-assignee-group {
        grid-area: group;
        justify-self: stretch;
    }
    .sched-assignees:not(.sched-assignees-detail) .sched-assignee-row > .sched-assignee-remove {
        grid-area: actions;
        justify-self: end;
        align-self: center;
    }

    /* Detail page: 3 rows —
       [name | ×] destructive action sits with the identity row
       [select spanning]
       [status | QR] informational badge + utility action together. */
    .sched-assignees-detail .sched-assignee-row {
        grid-template-columns: 1fr auto;
        grid-template-areas:
            "name   remove"
            "group  group"
            "status qr";
        gap: 6px 8px;
        padding: 10px 0;
        min-height: 0;
    }
    .sched-assignees-detail .sched-assignee-main {
        grid-area: name;
        justify-self: start;
        align-self: center;
    }
    .sched-assignees-detail .sched-assignee-group {
        grid-area: group;
        justify-self: stretch;
    }
    .sched-assignees-detail .sched-assignee-status {
        grid-area: status;
        justify-self: start;
        align-self: center;
    }
    .sched-assignees-detail .sched-assignee-actions {
        grid-area: qr;
        justify-self: end;
        align-self: center;
        gap: 6px;
    }
    .sched-assignees-detail .sched-assignee-row > .sched-assignee-remove {
        grid-area: remove;
        grid-column-start: auto; /* reset desktop col 6 */
        justify-self: end;
        align-self: center;
    }

    /* Shared — full-width select and always-visible remove on touch. */
    .sched-assignees .sched-assignee-group-select {
        max-width: 100%;
    }
    .sched-assignees .sched-assignee-remove {
        opacity: 1;
    }

    /* Top-of-card QR action buttons stack full-width into a 2-col grid. */
    .sched-qr-actions {
        display: grid;
        grid-template-columns: 1fr 1fr;
        width: 100%;
    }
    .sched-qr-actions > form,
    .sched-qr-actions > .btn {
        width: 100%;
    }
    .sched-qr-actions > form > .btn {
        width: 100%;
    }
}
.sched-assignee-remove {
    flex-shrink: 0;
    opacity: 0;
    width: 20px;
    height: 20px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--color-text-muted);
    font-size: 0.75rem;
    line-height: 1;
    cursor: pointer;
    transition: opacity 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.sched-assignee-row:hover .sched-assignee-remove,
.sched-assignee-row:focus-within .sched-assignee-remove {
    opacity: 1;
}
.sched-assignee-remove:hover,
.sched-assignee-remove:focus-visible {
    color: var(--color-danger);
    background: var(--color-danger-bg);
    outline: none;
}

/* "Need 1 more person" — muted caption above the actions row. */
.sched-staffing-need {
    font-size: 0.82rem;
    color: var(--color-text-muted);
}

/* Legacy `.sched-expand-actions` / `.sched-expand-footer` /
   `.sched-expand-footer-link` were removed when the shift detail
   surface moved to the global modal system. Footer actions now
   live in `.modal-footer` using `.btn .btn-secondary`. The 90/60px
   indent media queries on .sched-expand-section et al. are
   likewise obsolete — the modal-body's own padding handles
   horizontal rhythm. Touch-device `.sched-assignee-remove
   { opacity: 1 }` is still set at ≤768px inside the existing
   assignees block earlier in this file. */

/* ── Assign conflict modal ───────────────────────────────────── */
.hrc-conflict-modal { max-width: 520px; }

/* ── Approvals assign modal ──────────────────────────────────── */
.approval-assign-modal {
    max-width: 560px;
}
.approval-assign-modal .aa-summary {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-md);
    margin-bottom: var(--space-md);
    padding-bottom: var(--space-md);
    border-bottom: 1px solid var(--color-border);
}
.approval-assign-modal .aa-summary-main {
    min-width: 0;
    flex: 1 1 auto;
}
.approval-assign-modal .aa-shift-title {
    display: block;
    font-size: var(--font-size-lg, 1.1rem);
    margin-bottom: 2px;
}
.approval-assign-modal .aa-shift-meta {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}
.approval-assign-modal .aa-dot {
    display: inline-block;
    width: 3px;
    height: 3px;
    border-radius: 50%;
    background: var(--color-text-muted);
    opacity: 0.6;
}
.approval-assign-modal .aa-summary-staffing {
    flex-shrink: 0;
    text-align: right;
    white-space: nowrap;
}
.approval-assign-modal .aa-form {
    margin: 0;
}
.approval-assign-modal .aa-full-notice {
    margin: var(--space-md) 0 0;
    padding: 10px 12px;
    background: var(--color-success-bg, #eefbf0);
    border: 1px solid var(--color-success-border, #cfead6);
    border-radius: var(--radius-md);
    color: var(--color-success-text, #1d7a3a);
}
.approval-assign-modal .modal-footer {
    display: flex;
    justify-content: space-between;
    gap: var(--space-xs);
    padding: var(--space-md);
    border-top: 1px solid var(--color-border);
}
.hrc-conflict-overlay .modal-title { font-weight: 700; }
.hrc-conflict-reason {
    margin: 0 0 var(--space-md);
    color: var(--color-text);
}
.hrc-conflict-block {
    margin-top: var(--space-md);
    padding-top: var(--space-sm);
    border-top: 1px solid var(--color-border);
}
.hrc-conflict-label {
    font-size: var(--font-size-xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-text-muted);
    margin-bottom: 4px;
}
.hrc-conflict-shift {
    padding: 6px 0;
}
.hrc-conflict-shift + .hrc-conflict-shift {
    border-top: 1px dashed var(--color-border);
}
.hrc-conflict-shift-title {
    font-weight: 600;
}
.hrc-conflict-shift-meta {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    margin-top: 2px;
}
.hrc-conflict-actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-xs);
}

/* Shared "assign control": picker + submit button merged into one component.
   Used by both the list-view Quick-assign and the detail-page Assign user flow.
   Visually: a single rounded bar where the button is attached to the picker
   (no gap, shared focus ring). Stacks on narrow screens. */
.hrc-assign-control {
    display: flex;
    align-items: stretch;
    flex-wrap: nowrap;
    min-width: 0;
    border-radius: var(--radius-md);
    transition: box-shadow 0.15s ease;
}
.hrc-assign-control:focus-within {
    box-shadow: 0 0 0 3px rgba(0,0,0,0.08);
}
.hrc-assign-control .hrc-picker {
    flex: 1 1 220px;
    min-width: 180px;
    max-width: none;
}
.hrc-assign-control .hrc-picker-input,
.hrc-assign-control .hrc-picker-chip {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
    border-right: 0;
}
.hrc-assign-control:focus-within .hrc-picker-input,
.hrc-assign-control:focus-within .hrc-picker-chip {
    box-shadow: none; /* outer control shows the ring, not the child */
}
.hrc-assign-control .hrc-assign-btn {
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
    align-self: stretch;
    white-space: nowrap;
    padding-left: 16px;
    padding-right: 16px;
}
/* Disabled assign button — intentional muted look, not the global 0.5 opacity.
   Override here so it reads as "disabled by design" rather than "broken". */
.hrc-assign-control .hrc-assign-btn:disabled,
.hrc-assign-control .hrc-assign-btn[disabled] {
    opacity: 1;
    background: var(--color-surface-alt, #f4f4f4);
    color: var(--color-text-muted);
    border-color: var(--color-border);
}
/* Suppress the button's own focus ring — the wrapper's :focus-within ring is
   the only visible focus treatment for the combined control. */
.hrc-assign-control .hrc-assign-btn:focus-visible {
    outline: none;
}
@media (max-width: 640px) {
    .hrc-assign-control {
        flex-wrap: wrap;
        gap: 6px; /* Breathing room between stacked picker and button */
    }
    .hrc-assign-control .hrc-picker-input,
    .hrc-assign-control .hrc-picker-chip {
        border-top-right-radius: var(--radius-md);
        border-bottom-right-radius: var(--radius-md);
        border-right: 1px solid var(--color-border);
    }
    .hrc-assign-control .hrc-assign-btn {
        border-radius: var(--radius-md);
        width: 100%;
    }
}

/* Quickfill form sits next to the "N Empty slot" label as one cohesive
   staffing row: label → gap → picker+button stretched to fill remaining
   width (capped). No `margin-left: auto`: the form should feel anchored to
   the staffing content, not floating at the far edge. */
/* Extra-small — compact table controls only, never for primary actions */
.btn-xs {
    font-size: 0.75rem;
    padding: 0 8px;
    min-height: 26px;
}

/* ── Mobile ──────────────────────────────────────────────────── */
@media (max-width: 640px) {
    .sched-row-status   { display: none; }
    .sched-row-time     { width: 100px; }
    .sched-row-staffing { width: 70px; }
    .sched-bar-label    { display: none; }
    .sched-row-chevron  { display: none; }
}

/* Day heading row inside schedule day cards (legacy, keep for other uses) */
.schedule-day-heading {
    display: flex;
    align-items: baseline;
    gap: var(--space-sm);
    justify-content: space-between;
}


/* ── Add shift form ──────────────────────────────────────────── */
.add-shift-card {
    margin-bottom: 0;
}

/* Compact card variant — tighter internal padding */
.add-shift-card--compact {
    padding: var(--space-md);
}

.add-shift-card--compact .card-title {
    margin-bottom: var(--space-sm);
}

/* Inline label+input rows */
.asf-group {
    display: flex;
    flex-direction: column;
    gap: 3px;
    margin-bottom: var(--space-sm);
}

.asf-group:last-child { margin-bottom: 0; }

.asf-group label {
    font-size: 0.78rem;
    font-weight: 500;
    color: var(--color-text-muted);
    margin: 0;
}

.asf-group--sm { flex: 0 0 80px; }

/* Side-by-side field pair */
.asf-row {
    display: flex;
    gap: var(--space-sm);
    align-items: flex-end;
}

.asf-row .asf-group { flex: 1; margin-bottom: 0; }
.asf-row .asf-group--sm { flex: 0 0 80px; }

/* Time slot row — single flat line */
.time-slot-cols {
    display: flex;
    align-items: center;
    gap: 6px;
    padding-bottom: 2px;
}
.time-slot-col-label {
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.03em;
}
.time-slot-col-label:nth-child(1) { width: 100px; }
.time-slot-col-label:nth-child(3) { width: 100px; }
.time-slot-col-label:nth-child(4) { width: 80px; text-align: center; }
.time-slot-cols > span:nth-child(2) { width: 18px; } /* arrow spacer */

.time-slot-row {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 5px 0;
    border-bottom: 1px solid var(--color-border);
}
.time-slot-row:last-child { border-bottom: none; }

.time-slot-row .form-control {
    width: 100px;
}
.time-slot-row .slot-capacity {
    width: 80px;
    text-align: center;
}

.time-slot-sep {
    color: var(--color-text-muted);
    flex-shrink: 0;
    width: 18px;
    text-align: center;
}


.time-slot-duration {
    font-size: 0.78rem;
    color: var(--color-text-muted);
    min-width: 50px;
    flex-shrink: 0;
}

.time-slot-remove {
    margin-left: auto;
    flex-shrink: 0;
}

/* Full-width card (stacked layout) */
.asf-full-card {
    margin-bottom: var(--space-md);
}
.asf-full-card:last-of-type {
    margin-bottom: 0;
}

/* Context card — auto-fill grid */
.asf-context-grid {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: var(--space-sm) var(--space-md);
    align-items: end;
}
.asf-context-grid .asf-group {
    margin-bottom: 0;
}

/* Times card header */
.asf-times-header {
    display: flex;
    align-items: baseline;
    gap: var(--space-sm);
    margin-bottom: var(--space-sm);
}

/* Section label */
.asf-section-label {
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* Slot action buttons row */
.asf-slot-actions {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    margin-top: var(--space-sm);
    flex-wrap: wrap;
}

@media (max-width: 1024px) {
    .asf-context-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 768px) {
    .asf-context-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 480px) {
    .asf-context-grid { grid-template-columns: 1fr; }
    .time-slot-row .form-control { width: 90px; }
}
@media (max-width: 400px) {
    /* Two 90px time fields + capacity + duration need ~352px — at 320px
       (page content 296px, shift-editor modal body 304px) the row was
       clipped, hiding the duration and part of the capacity field. Let
       the row wrap: times stay on line one, capacity + duration drop to
       a second line instead of being cut off. */
    .time-slot-row { flex-wrap: wrap; row-gap: 4px; }
    .time-slot-duration { min-width: 0; }
}



/* ── Inline notice / alert ───────────────────────────────────── */

.alert {
    padding: var(--space-sm) var(--space-md);
    border-radius: var(--radius-md);
    margin-bottom: var(--space-md);
    border: 1px solid transparent;
}

.alert-info {
    background: var(--color-info-bg);
    border-color: var(--color-info-border);
    color: var(--color-info-text);
}

.alert-warning {
    background: var(--color-warning-bg);
    border-color: var(--color-warning-border);
    color: var(--color-warning-text);
}

.alert-success {
    background: var(--color-success-bg);
    border-color: var(--color-success-border);
    color: var(--color-success-text);
}

.alert-danger {
    background: var(--color-danger-bg);
    border-color: var(--color-danger-border);
    color: var(--color-danger-text);
}


/* ── Notice banner (project override, etc.) ──────────────────── */

.notice-banner {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    padding: var(--space-md) var(--space-lg);
    border-radius: var(--radius-md);
    margin-bottom: var(--space-lg);
}

.notice-banner-warning {
    background: var(--color-warning-bg);
    border: 1px solid var(--color-warning-border);
    color: var(--color-warning-text);
}

.notice-banner-info {
    background: var(--color-info-bg);
    border: 1px solid var(--color-info-border);
    color: var(--color-info-text);
}

.notice-banner-icon {
    font-size: 1.2rem;
    flex-shrink: 0;
}

.notice-banner-text {
    flex: 1;
    font-size: var(--font-size-base);
}


/* ── 25. Public pages (signup, feedback, etc.) ───────────────── */

body.public-page {
    background: var(--color-bg);
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: var(--space-xl) var(--space-md);
}

.public-card-wrap {
    width: 100%;
    max-width: 680px;
}

.public-card {
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--space-xl);
}

.public-card--wide {
    max-width: 100%;
}

.public-card-brand {
    font-size: var(--font-size-sm);
    font-weight: 600;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    margin-bottom: var(--space-md);
}

.public-card-title {
    font-size: 1.5rem;
    font-weight: 700;
    color: var(--color-text);
    margin-bottom: var(--space-lg);
    line-height: 1.2;
}

.public-section-title {
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin: var(--space-xl) 0 var(--space-md);
    padding-bottom: var(--space-xs);
    border-bottom: 1px solid var(--color-border);
}

.public-section-title:first-of-type {
    margin-top: 0;
}

.public-empty {
    color: var(--color-text-muted);
    font-size: var(--font-size-base);
    padding: var(--space-lg) 0;
}

/* ── Public confirmation / completion state ─────────────────── */
.public-confirm {
    text-align: center;
    padding: var(--space-xl) 0 var(--space-lg);
}
.public-confirm-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 52px;
    height: 52px;
    border-radius: 50%;
    background: var(--color-success-bg);
    margin: 0 auto var(--space-md);
}
.public-confirm-icon svg {
    width: 26px;
    height: 26px;
    stroke: var(--color-success);
    stroke-width: 2.5;
    fill: none;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.public-confirm-heading {
    font-size: 1.2rem;
    font-weight: 700;
    margin: 0 0 var(--space-sm);
    color: var(--text);
}
.public-confirm-body {
    font-size: 0.9375rem;
    color: var(--color-text-muted);
    margin: 0 auto;
    max-width: 360px;
}

/* ── Signup shift list ─────── */

/* Role filter above the shift list — reuses the Me → Shifts toolbar select,
   but this page is a single-column card (not a multi-control toolbar), so the
   select fills the card width rather than being capped. Scoped to signup only. */
.signup-filter-bar {
    margin-bottom: var(--space-md);
}
.signup-filter-bar .hrc-toolbar-control {
    width: 100%;
}

.signup-shifts-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    margin-bottom: var(--space-md);
}

/* ── Date group headers within shift list ── */
.signup-date-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-sm);
    font-size: var(--font-size-base);
    font-weight: 600;
    color: var(--color-text);
    margin: var(--space-sm) 0 0 0;
    padding: 0;
}
.signup-date-header:first-child {
    margin-top: 0;
}
.signup-date-label {
    min-width: 0;
}

/* Per-day unavailable toggle. Picking a shift auto-hides the rows it
   conflicts with (temporal rule only — a compatible same-day row stays
   selectable); this control on the day header names the hidden count
   and brings them back. Rendered only while that day actually holds
   locked rows, so an undecided day shows no dead chrome. Muted and
   underlined rather than a button shape: the day header is a label,
   not an action row, and the primary action on this page stays the
   Continue button. */
.signup-date-toggle {
    flex-shrink: 0;
    background: none;
    border: 0;
    /* Vertical padding keeps a usable tap target on phones; the matching
       negative margin keeps it out of the flex line's height, so a day that
       gains the toggle does not grow taller than one not yet picked.
       Measured: without it the header grows 5px and the first card drops 3px
       at the moment of selection. */
    padding: 4px 0;
    margin-block: -4px;
    font-family: inherit;
    font-size: var(--font-size-sm);
    font-weight: 500;
    color: var(--color-text-muted);
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 2px;
}
.signup-date-toggle:hover {
    color: var(--color-text);
}
.signup-date-toggle[hidden] {
    display: none;
}

/* Me → Shifts: unavailable candidates (temporal conflict with a
   held/requested shift) are rendered but hidden by default — the
   per-day expander chip below the cards names the count and toggles
   .is-unavail-open on the day body. CSS-default hiding on purpose:
   no script has to run before first paint for the default to hold. */
.sched-day-body:not(.is-unavail-open) > .sched-row-wrap.is-unavailable {
    display: none;
}
.sched-unavail-toggle {
    display: block;
    width: fit-content;
    background: none;
    border: 0;
    padding: 4px 0;
    font-family: inherit;
    font-size: var(--font-size-sm);
    font-weight: 500;
    color: var(--color-text-muted);
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 2px;
}
.sched-unavail-toggle:hover {
    color: var(--color-text);
}

/* ── Disabled shift row (temporal conflict with a selected/held shift) ── */
.signup-shift-row.is-disabled {
    opacity: 0.4;
    pointer-events: none;
}

.signup-shift-row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-md);
    padding: var(--space-md);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: border-color 0.15s, background 0.15s;
    background: var(--color-surface);
}

.signup-shift-row:hover {
    border-color: var(--color-border-strong);
    background: var(--color-bg);
}

.signup-shift-row input[type="checkbox"] {
    margin-top: 3px;
    flex-shrink: 0;
    width: 18px;
    height: 18px;
    accent-color: var(--color-accent);
    cursor: pointer;
}

.signup-shift-row:has(input:checked) {
    border-color: var(--color-accent);
    background: var(--color-bg);
}

/* ── Already-assigned row (returning worker) ──
   Read-only: no checkbox is rendered, so :has(input:checked) and hover
   affordances never apply. Full-strength text — this is a confirmation
   ("you're signed up"), not a disabled state, which is why it does NOT
   reuse .is-disabled's washed-out opacity. */
.signup-shift-row.is-assigned {
    cursor: default;
    background: var(--color-bg);
}

.signup-shift-row.is-assigned:hover {
    border-color: var(--color-border);
    background: var(--color-bg);
}

.signup-shift-assigned {
    flex-shrink: 0;
    white-space: nowrap;
    font-size: var(--font-size-sm);
    font-weight: 600;
    color: var(--color-success);
}

.signup-shift-row--full {
    opacity: 0.5;
    cursor: not-allowed;
}

.signup-shift-row--full:hover {
    border-color: var(--color-border);
    background: var(--color-surface);
}

.signup-shift-info {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1;
    min-width: 0;
}

.signup-shift-title {
    font-weight: 600;
    font-size: var(--font-size-base);
    color: var(--color-text);
}

.signup-shift-role {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

/* Minimum age on the public signup picker.
   ─────────────────────────────────────────
   Sits on the TITLE line, not the role line: it is an eligibility
   constraint, so it belongs on the visually primary line rather than in
   the muted secondary text. For anonymous visitors nothing is filtered
   by age, so this is the only eligibility signal on the card.

       Bar Red Bull (age 20+)   <- .signup-shift-titleline
       15:00➜21:30 · …          <- .signup-shift-meta

   Title, role and age share ONE line (2026-08-24). The role used to sit
   on its own line below; it moved up once the duplicate-role case was
   suppressed in PHP (signup_show_role_line()), which is what stops the
   line reading "Bar Bar".

   STYLING is IDENTICAL to .sched-row-age on the operator schedule list —
   semibold, full-strength colour, never a pill, never colour-coded by
   value. Do not re-badge one of them without changing the other.

   The WORDING is not shared, and that is deliberate. The operator board
   renders the compact "20+"; worker-facing surfaces (this one and
   Me → Shifts) render the spelled-out suffix from
   format_min_age_suffix(), because "Bar 20+" reads as the name of the bar
   to somebody meeting the product through a public link. Same visual
   vocabulary, different reader. */
.signup-shift-titleline {
    display: flex;
    align-items: baseline;
    gap: 6px;
    min-width: 0;
}

/* Title and role may truncate; the age never does. Same priority rule as
   the schedule row — a long shift title must not push a legal constraint
   off the card. min-width:0 is what permits shrinking below content size
   inside a flex parent, so BOTH text spans need it: with the role on this
   line too, leaving it unshrinkable would just move the overflow problem
   onto the role and push the age out again. */
.signup-shift-titleline > .signup-shift-title,
.signup-shift-titleline > .signup-shift-role {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.signup-shift-age {
    flex-shrink: 0;
    white-space: nowrap;
    font-size: var(--font-size-sm);
    font-weight: 600;
    color: var(--color-text);
}

.signup-shift-meta {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

.signup-shift-slots {
    font-size: var(--font-size-sm);
    font-weight: 500;
    color: var(--color-text-muted);
    margin-top: 2px;
}

.signup-shift-slots--full {
    color: var(--color-danger);
}

/* ── Or divider ─────────────── */

.signup-or-divider {
    text-align: center;
    color: var(--color-text-light);
    font-size: var(--font-size-sm);
    margin: var(--space-md) 0;
}

/* ── Form actions ───────────── */

.form-actions {
    margin-top: var(--space-xl);
    display: flex;
    gap: var(--space-md);
    align-items: center;
}

/* ── Identity step — register vs login cards ─ */

.signup-identity-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-md);
    margin-bottom: var(--space-lg);
}

.signup-identity-grid--single {
    grid-template-columns: 1fr;
    max-width: 320px;
}

.signup-identity-card {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-xs);
    padding: var(--space-lg);
    border: 2px solid var(--color-border);
    border-radius: var(--radius-md);
    text-decoration: none;
    color: var(--color-text);
    transition: border-color 0.15s, background 0.15s;
}

.signup-identity-card:hover {
    border-color: var(--color-accent);
    background: var(--color-bg);
    text-decoration: none;
}

.signup-identity-icon {
    font-size: 1.5rem;
    margin-bottom: var(--space-xs);
}

.signup-identity-title {
    font-weight: 600;
    font-size: 1rem;
}

.signup-identity-desc {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    line-height: 1.4;
}

@media (max-width: 480px) {
    body.public-page {
        padding: var(--space-md) var(--space-sm);
    }

    .public-card {
        padding: var(--space-lg) var(--space-md);
    }

    .public-card-title {
        font-size: 1.25rem;
    }

    .form-actions .btn {
        width: 100%;
        justify-content: center;
    }

    .signup-identity-grid {
        grid-template-columns: 1fr;
    }
}


/* ── Cookie consent banner ─────────────────────────────────── */

.cookie-banner {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 9999;
    background: var(--color-surface);
    border-top: 1px solid var(--color-border);
    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
    padding: var(--space-md) var(--space-lg);
}

.cookie-banner.hidden { display: none; }

.cookie-banner-inner {
    max-width: 960px;
    margin: 0 auto;
    display: flex;
    align-items: center;
    gap: var(--space-md);
    flex-wrap: wrap;
}

.cookie-banner-text {
    flex: 1 1 300px;
    font-size: var(--font-size-sm);
    color: var(--color-text);
    margin: 0;
    line-height: 1.5;
}

.cookie-banner-actions {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    flex-shrink: 0;
    flex-wrap: wrap;
}

.cookie-banner-actions .btn-link {
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    cursor: pointer;
    padding: var(--space-xs) var(--space-md);
    text-decoration: none;
}

.cookie-banner-actions .btn-link:hover {
    background: var(--color-border);
    color: var(--color-text);
}


/* ── Cookie preferences modal ──────────────────────────────── */

.cookie-modal {
    position: fixed;
    inset: 0;
    z-index: 10000;
    display: flex;
    align-items: center;
    justify-content: center;
}

.cookie-modal.hidden { display: none; }

.cookie-modal-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.4);
}

.cookie-modal-content {
    position: relative;
    background: var(--color-surface);
    border-radius: var(--radius-md);
    padding: var(--space-lg);
    max-width: 520px;
    width: calc(100% - 2rem);
    max-height: 90vh;
    overflow-y: auto;
    box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
}

.cookie-modal-content h2 {
    font-size: var(--font-size-lg);
    margin: 0 0 var(--space-md);
}

.cookie-category {
    padding: var(--space-sm) 0;
    border-bottom: 1px solid var(--color-border);
}

.cookie-category:last-of-type {
    border-bottom: none;
}

.cookie-category-header {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    cursor: pointer;
    font-size: var(--font-size-base);
    /* 44px minimum tap target (WCAG 2.5.8 / iOS HIG). The LABEL is the
       interactive target - it wraps the checkbox, so a tap anywhere on the
       row toggles it - which is why the height belongs here and not on the
       18px input. Enlarging the input instead would also break
       .cookie-category-desc, whose left margin is calc(18px + gap) so the
       description lines up under the label text. */
    min-height: 44px;
}

.cookie-category-header input[type="checkbox"] {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
}

.cookie-always-on {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    margin-left: auto;
}

.cookie-category-desc {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    margin: var(--space-xs) 0 0 calc(18px + var(--space-sm));
    line-height: 1.4;
}

.cookie-modal-actions {
    display: flex;
    gap: var(--space-sm);
    margin-top: var(--space-md);
    justify-content: flex-end;
}

/* 44px minimum tap target for the consent controls (WCAG 2.5.8 / iOS HIG).
   SCOPED to the consent surfaces deliberately: .btn-sm is shared by ~107
   files, so raising its 32px globally would resize buttons across the whole
   app. Consent is a one-shot decision on a bar that overlays the page and is
   usually thumb-reached at the bottom of a phone, so it needs a full target.
   Both selectors outrank .btn-sm on specificity, so source order is safe. */
.cookie-banner-actions .btn,
.cookie-modal-actions .btn {
    min-height: 44px;
}

/* .btn-link is not a .btn - it carries no flex box of its own. */
.cookie-banner-actions .btn-link {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/* ── Cookie banner mobile ──────────────────────────────────── */

@media (max-width: 640px) {
    .cookie-banner {
        padding: var(--space-sm) var(--space-md);
        padding-bottom: calc(var(--space-sm) + env(safe-area-inset-bottom));
    }

    .cookie-banner-inner {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-sm);
    }

    .cookie-banner-text {
        flex: none;
    }

    .cookie-banner-actions {
        justify-content: stretch;
    }

    .cookie-banner-actions .btn,
    .cookie-banner-actions .btn-link {
        flex: 1;
        text-align: center;
    }
}


/* ── Live poll: "Last updated" strip + change fade ─────────── */
.live-strip {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    font-size: 0.8rem;
    color: var(--color-text-muted);
    margin-left: auto;
}
.live-strip .live-refresh-btn {
    background: none;
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: 2px 8px;
    font-size: 0.8rem;
    color: var(--color-text-muted);
    cursor: pointer;
    line-height: 1.4;
}
.live-strip .live-refresh-btn:hover {
    color: var(--color-text);
    border-color: var(--color-text-muted);
}
.live-strip .live-ts { font-variant-numeric: tabular-nums; }

/* Failing state — subtle warning tint on the strip when the most recent
   poll(s) errored. Recovers automatically on the next successful poll. */
.live-strip.live-strip--failing .live-ts {
    color: var(--color-warning, #c07000);
}
.live-strip.live-strip--failing .live-refresh-btn {
    color: var(--color-warning, #c07000);
    border-color: var(--color-warning, #c07000);
}
.live-strip.live-strip--failing .live-warn {
    display: inline;
}
.live-strip .live-warn {
    display: none;
    color: var(--color-warning, #c07000);
    font-size: 0.8rem;
    margin-left: 4px;
}

[data-live-region].live-updated {
    animation: live-updated-fade 380ms ease-out;
}
@keyframes live-updated-fade {
    0%   { background-color: rgba(255, 214, 10, 0.18); }
    100% { background-color: transparent; }
}

/* Contract typography — applied wherever a contract template is rendered:
   the workspace template detail page, the contract detail page, AND the
   Quill editor itself. The editor view should match the rendered preview
   and the final PDF so authors see what the contract will look like.

   Templates use:
     <h2> document title
     <h3> top section ("1. Employer", "2. Employee", ...)
     <h4> sub-section ("5.1 Working hours", "6.4 Compensation", ...)
     <p>  one per line of content (data line OR prose paragraph)

   Margins are tuned so consecutive <p>s feel grouped (data lines stack
   tightly), while <h3>/<h4> open real space between sections. */
/* Constrain contract-preview prose width for comfortable reading.
   Editor view (.ql-editor) stays full width — authors need every
   pixel while editing. Read-only preview gets a typographic measure
   (~720px ≈ 75ch at base size) and centers within its container. */
.contract-preview {
    max-width: 720px;
    margin-left: auto;
    margin-right: auto;
}

/* Merge-field tokens — {{name}}, {{employer}} etc. — appear as
   literal text in the *template* preview (the contract preview
   resolves them to real values). Render them slightly lighter so
   they read as "to be filled" without disrupting the prose flow. */
.contract-preview .merge-token {
    color: var(--color-text-light);
}

/* Zero <p> margin so consecutive data lines stack tight, the way the
   original v2 <br>-only structure rendered. All visual breaks come
   from the heading margins below. Multi-paragraph prose stacks tight
   too — that's the trade-off this contract style accepts. */
.contract-preview p,
.ql-editor p                  { margin: 0; }

.contract-preview h2,
.ql-editor h2                 { margin: 0 0 0.7em 0; font-size: 1.65em; }
.contract-preview h3,
.ql-editor h3                 { margin: 1.2em 0 0.25em 0; font-size: 1.15em; font-weight: 700; }
.contract-preview h4,
.ql-editor h4                 { margin: 0.9em 0 0.15em 0; font-size: 1em; font-weight: 700; }

.contract-preview h2:first-child,
.contract-preview h3:first-child,
.ql-editor h2:first-child,
.ql-editor h3:first-child     { margin-top: 0; }


/* ============================================================
   PHASE A — DESIGN SYSTEM PRIMITIVES
   ------------------------------------------------------------
   Additive only. No existing rules above are edited. These
   classes are opt-in: existing markup keeps rendering exactly
   as before until templates are migrated in Phase B.

   Conventions encoded here:
   - Benchmark patterns (Users, Clockwork, Settings) promoted
     to canonical class names, side by side with their original
     bespoke names (`.settings-nav-btn`, `.users-tab-count`,
     `.cw-sessions-table`).
   - Time-range glyph: ➜ (no spaces in tables, spaces allowed
     in detail/card views — handled in PHP/JS helpers, not CSS).
   ============================================================ */


/* ── Card structural sub-elements ──────────────────────────────
   Optional sub-structure that replaces the card flush-edge
   negative-margin hack. Use `.card.card-structured` to drop
   the outer padding so `.card-header / .card-body / .card-footer`
   each own their own padding and any table inside `.card-body`
   sits flush to the card edges naturally. */
.card.card-structured {
    padding: 0;
}

.card-header {
    padding: var(--space-md) var(--space-lg);
    border-bottom: 1px solid var(--color-border);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    flex-wrap: wrap;
}

.card-header-title {
    font-size: var(--font-size-base);
    font-weight: 600;
    margin: 0;
    color: var(--color-text);
}

.card-header-actions {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    flex-wrap: wrap;
}

.card-body {
    padding: var(--space-lg);
}

/* Body that contains a full-width child like a table. */
.card-body-flush {
    padding: 0;
}

.card-footer {
    padding: var(--space-md) var(--space-lg);
    border-top: 1px solid var(--color-border);
    background: var(--color-bg);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    flex-wrap: wrap;
}


/* ── Modal size modifiers ──────────────────────────────────────
   Same `.modal` structure (header / body / footer, mobile slide-up,
   safe-area handling), just wider variants. `.doc-viewer` stays
   for now — `.modal.modal-lg` is for forms/flows that need more
   width without the document-preview chrome. */
.modal.modal-md { max-width: 640px; }
.modal.modal-lg { max-width: 960px; }
/* .modal-xl (1100px) was removed 2026-09-01: it shipped with zero
   consumers and a second large size is exactly the drift the shared
   shell exists to stop. One large variant — .modal-lg. .modal-md (640)
   was promoted from the ~8 inline style="max-width: 640px" copies the
   audit found, starting with the vehicles detail modals. */


/* ── Toolbar (page header row) ─────────────────────────────────
   Title left, actions right, wraps cleanly on mobile.
   Functional alias for the existing `.page-header`; both render
   identically. New code uses `.toolbar`. */
.toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    flex-wrap: wrap;
    margin-bottom: var(--space-lg);
}

.toolbar-title {
    font-size: var(--font-size-2xl);
    font-weight: 700;
    line-height: 1.2;
    margin: 0;
}

.toolbar-actions {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    flex-wrap: wrap;
}


/* ── Search bar (above tables) ─────────────────────────────────
   Page-level toolbar: search input grows to fill, primary action
   pinned right. Distinct from `.filter-bar` which is the compact
   picker filter pattern. */
.search-bar {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    flex-wrap: wrap;
    margin-bottom: var(--space-md);
}

.search-bar-input {
    flex: 1 1 220px;
    min-width: 0;
}

/* Form-control children inside .search-bar (selects, button-likes) shrink
   to their natural width. Without this, `.form-control { width: 100% }`
   in section 12 wins and each control claims its own row. The growing
   search input keeps `.search-bar-input` as an explicit override. */
.search-bar > .form-control:not(.search-bar-input) {
    width: auto;
    flex: 0 0 auto;
}

.search-bar-actions {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    flex-wrap: wrap;
    margin-left: auto;
}


/* ── Bulk actions footer (below tables) ────────────────────────
   Lives inside `.card-footer` typically. `is-active` toggles
   visibility once one or more rows are selected. */
.bulk-actions {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    flex-wrap: wrap;
}

.bulk-actions-summary {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    font-weight: 500;
}

.bulk-actions-buttons {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    flex-wrap: wrap;
    margin-left: auto;
}


/* ── Empty state ───────────────────────────────────────────────
   Centered message + optional icon + optional primary action.
   Use inside `.card-body`, `.card`, or a `<td colspan="...">`
   when a table has no rows. Replaces ad-hoc
   `<tr><td>No results</td></tr>` plaintext drops. */
.empty-state {
    padding: var(--space-2xl) var(--space-lg);
    text-align: center;
    color: var(--color-text-muted);
}

.empty-state-icon {
    font-size: var(--font-size-3xl);
    line-height: 1;
    margin-bottom: var(--space-md);
    opacity: 0.5;
}

.empty-state-title {
    font-size: var(--font-size-lg);
    font-weight: 600;
    color: var(--color-text);
    margin: 0 0 var(--space-xs);
}

.empty-state-body {
    font-size: var(--font-size-sm);
    margin: 0 auto var(--space-md);
    max-width: 420px;
    line-height: 1.5;
}

.empty-state-action {
    margin-top: var(--space-sm);
}


/* ── Skeleton loaders ──────────────────────────────────────────
   Pulse-shimmer placeholders for table rows and cards.
   Pure CSS, no images. Respects prefers-reduced-motion. */
@keyframes skeleton-pulse {
    0%, 100% { background-color: var(--color-bg); }
    50%      { background-color: #ececec; }
}

.skeleton {
    display: inline-block;
    border-radius: var(--radius-sm);
    background: var(--color-bg);
    animation: skeleton-pulse 1.4s ease-in-out infinite;
}

.skeleton-line {
    display: block;
    width: 100%;
    height: 0.875rem;
    margin: 0.25rem 0;
    border-radius: var(--radius-sm);
    background: var(--color-bg);
    animation: skeleton-pulse 1.4s ease-in-out infinite;
}

.skeleton-line.is-short  { width: 40%; }
.skeleton-line.is-medium { width: 65%; }

.skeleton-row td > .skeleton-line {
    margin: 0;
}

@media (prefers-reduced-motion: reduce) {
    .skeleton, .skeleton-line { animation: none; }
}


/* ── Pill (canonical, generalises .settings-nav-btn) ───────────
   `.pill-nav` is the segmented container; `.pill` is each item;
   `.pill.is-active` is the active state. Visual rules match the
   existing `.settings-nav` / `.settings-nav-btn-active` exactly,
   per `feedback_pill_tab_style.md` (#ebebeb bg, strong border,
   font-weight 700, no shadow). Both class systems coexist. */
.pill-nav {
    display: inline-flex;
    flex-wrap: wrap;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: 3px;
    gap: 2px;
}

/* Horizontal-scroll variant for tab bars that exceed viewport
   width (e.g. Users page with 8 tabs at 320px). Matches the
   inline `.users-tab-nav` rules in users/index.php. */
.pill-nav-scrollable {
    overflow-x: auto;
    flex-wrap: nowrap;
    max-width: 100%;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}

.pill-nav-scrollable::-webkit-scrollbar { height: 0; display: none; }
.pill-nav-scrollable .pill,
.pill-nav-scrollable .settings-nav-btn { white-space: nowrap; flex-shrink: 0; }

.pill {
    display: inline-block;
    padding: 5px 14px;
    font-size: var(--font-size-sm);
    font-weight: 500;
    letter-spacing: 0.03em;
    color: var(--color-text-muted);
    text-decoration: none;
    border: 1px solid transparent;
    border-radius: calc(var(--radius-lg) - 3px);
    transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
    cursor: pointer;
    background: transparent;
}

.pill:hover {
    color: var(--color-text);
    text-decoration: none;
}

.pill.is-active {
    color: var(--color-text);
    font-weight: 700;
    background: #ebebeb;
    border-color: var(--color-border-strong);
    box-shadow: none;
}

/* Horizontal-scroll pill nav for narrow viewports — required when
   the pill row carries more entries than the viewport width can
   hold (Contracts: 5 pills, Users: 8+ tabs). Promotes the inline
   styles that previously lived only in workspace/users/index.php so
   any module adopting `.settings-nav.users-tab-nav` gets the same
   behaviour. */
.users-tab-nav {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    flex-wrap: nowrap;
    max-width: 100%;
}
.users-tab-nav::-webkit-scrollbar { height: 0; display: none; }
.users-tab-nav .settings-nav-btn { white-space: nowrap; flex-shrink: 0; }


/* ── Workspace toolbar — pill nav + trailing actions ───────────
   Generic page-chrome container for module list views that need
   module-level actions alongside the subnav pills (Suppliers,
   future Items / Schedule / Locations passes) instead of stacked
   on a separate row below the filter form.

   Generalises Power Analytics' .ar-toolbar with a module-neutral
   name. Migrating .ar-toolbar itself to .ws-toolbar is a deferred
   follow-up — kept separate to bound the change surface per page.

   The pill nav inside MUST NOT carry an inline style="margin-bottom"
   — defeat it at the call site (e.g. an $inside_toolbar parameter
   on the render helper, as suppliers_render_subnav does), not with
   !important here. Inline-style specificity would otherwise win.

   Layout: pills left, actions right, space-between on desktop.
   At narrow viewports the toolbar wraps so the pill nav claims its
   own row (and its mobile horizontal-scroll behaviour works against
   the full container width) instead of competing with the actions. */
.ws-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-md);
    flex-wrap: wrap;
    margin-bottom: var(--space-md);
}
.ws-toolbar > nav.settings-nav { margin-bottom: 0; min-width: 0; }
.ws-toolbar-trailing {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    flex-wrap: wrap;
}

@media (max-width: 640px) {
    .ws-toolbar > nav.settings-nav { flex: 0 0 100%; max-width: 100%; }
    .ws-toolbar-trailing           { flex: 0 0 100%; }
}


/* ── Mobile pill bars: single-row horizontal scroll ────────────
   At ≤640px, every `.settings-nav` (except the segmented modifier
   which fills its container by design) becomes a single
   horizontally-scrollable row. No wrapping, hidden scrollbar.
   The desktop "segmented panel" look is preserved unchanged —
   container bg + border + 3px padding, pills inside the panel,
   active pill subtly tinted. Matches the Contracts/Incidents
   benchmark exactly across all viewports.

   Active pill is centered into view on load by the JS hook in
   main.js. Desktop is unaffected — only modules that already opt
   in via `.users-tab-nav` get scroll behaviour above 640px. */
@media (max-width: 640px) {
    .settings-nav:not(.settings-nav-segmented) {
        display: flex;
        flex-wrap: nowrap;
        /* Setting overflow-x:auto makes the computed overflow-y `auto`
           too (CSS spec: a non-visible value on one axis promotes the
           other from `visible` to `auto`), so this row is also a
           vertical scrollport. With the flex default align-items:stretch
           the flex line rounds up ~1px past the content box on mobile
           WebKit and clips the TOP of the pills. Centering the pills on
           the cross axis and giving it a couple of px of vertical slack
           leaves nothing to clip. Container-level only — pill styling is
           unchanged. */
        align-items: center;
        overflow-x: auto;
        max-width: 100%;
        padding-top: 5px;
        padding-bottom: 5px;
        scrollbar-width: none;
        -ms-overflow-style: none;
        -webkit-overflow-scrolling: touch;
    }
    .settings-nav:not(.settings-nav-segmented)::-webkit-scrollbar {
        height: 0;
        display: none;
    }
    .settings-nav:not(.settings-nav-segmented) > .settings-nav-btn,
    .settings-nav:not(.settings-nav-segmented) > .pill {
        white-space: nowrap;
        flex-shrink: 0;
    }
}

/* The legacy right-edge fade mask that lived here previously was
   removed when explicit chevron control zones (`< |` and `| >`)
   replaced the gradient affordance. The mask applied a transparent
   gradient to the entire nav box including its right border, so
   the rounded outer edge would visibly fade to nothing whenever
   `.is-overflowing` was active. The control zones now provide the
   "more content" cue; the shell border stays intact on every side.
   Don't reintroduce a mask here. */


/* ── Pill-nav row shell (caller-side) ──────────────────────────
   Optional structural shell used by `_detail_tabs.php` to bound
   the tab nav inside a flex parent (e.g. user detail page-header
   shares a row with the back button). Lives between the flex
   parent and the nav so the JS-injected `.pill-nav-wrap` and the
   nav inside both inherit a known-good width.

   - Desktop:  shell is `flex:1 1 0; min-width:0` — joins back
               button on the same row, claims remaining width.
   - Mobile (≤640px): `flex-basis:100%` forces shell onto its own
               row at full viewport width. The nav inside scrolls
               horizontally within that bounded width — guarantees
               the page itself never scrolls sideways.

   Driven by a class (not inline styles) so a media query can flip
   sizing without fighting inline-style specificity, and so the
   sizing is independent of whether main.js has run yet. */
.pill-nav-row {
    flex: 1 1 0;
    min-width: 0;
    max-width: 100%;
}

.pill-nav-row > .settings-nav,
.pill-nav-row > .pill-nav-wrap {
    width: 100%;
    max-width: 100%;
    min-width: 0;
}

/* Break the min-content chain on EVERY ancestor that contains
   the row. Without this, an unstyled `<div style="display:flex;
   flex-wrap:wrap">` ancestor inherits `min-width: auto`, which
   resolves to the children's min-content. With `flex-wrap:nowrap`
   on the nav inside, that min-content equals the sum of all 8
   pills (~700px) — so the parent locks at content width, and the
   `flex-basis:100%` rule below has nothing smaller to measure
   against.

   The bare `:has(.pill-nav-row)` matcher (descendant, not direct
   child) is required because some routes wrap the back-button +
   tabs row in an extra flex layer (e.g. competence pairs it with
   an action form via `justify-content:space-between`). The
   chain-breaker must reach every flex ancestor of the row, no
   matter the depth.

   Safe to apply broadly: `min-width: 0` is a no-op on elements
   that aren't flex/grid items, and `max-width: 100%` matches the
   default block-element behaviour. */
:has(.pill-nav-row) {
    min-width: 0;
    max-width: 100%;
}

@media (max-width: 640px) {
    .pill-nav-row {
        flex-basis: 100%;
        width: 100%;
    }
}


/* ── Pill-nav wrapper, viewport, + left/right control zones ────
   Two-mode component:

   FLAT MODE (no overflow — pills fit in the available width):
     `.pill-nav-wrap` and `.pill-nav-viewport` are both
     `display:contents`. They have no boxes of their own — the
     `.settings-nav` inside renders exactly as if no wrapper
     existed at all, with the original `.settings-nav` panel
     chrome (bg, border, radius, padding, gap). Identical to
     Contracts/Incidents. Zero added vertical rhythm.

   OVERFLOW MODE (pills don't fit — JS sets `.has-overflow`):
     The wrap promotes itself to the visible bordered panel,
     the viewport becomes the clipping scroll container, the
     nav becomes a transparent inner track, and the chevron
     siblings become real flex columns flanking the viewport:

       .pill-nav-wrap.has-overflow   ← panel
         .pill-nav-scroll-left       (column, divider on right)
         .pill-nav-viewport          (flex:1; overflow-x:auto)
           .settings-nav             (transparent track)
         .pill-nav-scroll-right      (column, divider on left)

   The chevron control zones are NOT absolute overlays. They are
   real flex columns; pills physically cannot render in the
   chevron column, so no glyph fragments can bleed past the
   divider. The wrap owns the outer rounded border, continuous
   on every edge. */

/* Default — wrapper is layout-transparent. Nav inside renders
   exactly like a raw `.settings-nav`. */
.pill-nav-wrap,
.pill-nav-viewport {
    display: contents;
}

/* Overflow active — promote wrap to the bordered panel, viewport
   to the clipping scroll container. Padding distribution mirrors
   the Contracts/Incidents single-panel `.settings-nav` exactly:
     - shell    → border + bg only, NO padding
     - viewport → horizontal clipping only, NO padding
     - track    → 3px padding (the panel's inner breathing room)
     - pills    → 5/14 padding (defines active pill height)
   Only one layer provides vertical padding (the track). With
   `align-items: stretch` the chevron columns rise to the track
   height — no empty gray space above/below the glyph. */
.pill-nav-wrap.has-overflow {
    display: inline-flex;
    flex-wrap: nowrap;
    align-items: stretch;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: 0;
    min-width: 0;
    max-width: 100%;
    /* Clip child corners to the rounded shell so the chevron
       columns and viewport don't extend past the rounded edges. */
    overflow: hidden;
}

.pill-nav-wrap.has-overflow > .pill-nav-viewport {
    display: block;
    flex: 1 1 0;
    min-width: 0;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -ms-overflow-style: none;
    -webkit-overflow-scrolling: touch;
    /* No padding — horizontal clipping only. The track inside
       owns the inner breathing room. */
    padding: 0;
}
.pill-nav-wrap.has-overflow > .pill-nav-viewport::-webkit-scrollbar { height: 0; display: none; }

/* Track inside the viewport: keeps the 3px segmented-panel inner
   padding that `.settings-nav` carries in flat mode, so the
   visual rhythm matches Contracts/Incidents exactly. Strips the
   rest of the panel chrome (bg/border/radius) — those live on
   the wrap now.

   `margin: 0 !important` is intentional: the nav becomes
   inline-flex inside a block viewport here, so any inline margin
   set on the nav (e.g. an old `style="margin-bottom:..."`) would
   be trapped inside the panel and inflate the line-box height —
   making the panel render visibly taller than the pill row. The
   margin belongs on a wrapper around the wrap, not on the nav
   itself. !important defeats that inline specificity. */
.pill-nav-wrap.has-overflow > .pill-nav-viewport > .settings-nav {
    display: inline-flex;
    flex-wrap: nowrap;
    background: transparent;
    border: 0;
    border-radius: 0;
    padding: 3px;
    margin: 0 !important;
    gap: 2px;
    max-width: none;
    min-width: 0;
    overflow: visible;
}

/* Control zones — real flex columns, not overlays. Solid
   panel-bg face, 1px divider on the inner edge using
   --color-border.

   `align-self: center` + explicit `height: 30px` is the visual
   compact-look fix: the chevron's visible block (its solid bg
   *and* the 1px divider line) matches active pill height —
   never the full 35.52px track height. The control vertically
   centres inside the shell with ~2-3px of wrap-bg showing above
   and below it, exactly like the active pill sits inside the
   3px segmented-panel padding on Contracts/Incidents. The 1px
   divider is therefore 30px tall, so the right side of the
   shell reads as compact pill-row height rather than tall
   panel-edge height. */
.pill-nav-scroll-left,
.pill-nav-scroll-right {
    flex: 0 0 34px;
    align-self: center;
    height: 30px;
    min-height: 0;
    border: 0;
    cursor: pointer;
    color: var(--color-text-muted);
    font-size: 1.4rem;
    line-height: 1;
    font-family: inherit;
    font-weight: 600;
    display: none;
    align-items: center;
    justify-content: center;
    padding: 0;
    background: var(--color-bg);
    transition: color 0.15s ease;
}

.pill-nav-scroll-left  { order: 0; border-right: 1px solid var(--color-border); }
.pill-nav-viewport     { order: 1; }
.pill-nav-scroll-right { order: 2; border-left:  1px solid var(--color-border); }

/* Chevrons only render when the wrap is in overflow mode AND
   the matching direction class is set — never in flat mode,
   even transiently during class updates. */
.pill-nav-wrap.has-overflow.is-overflowing-left  > .pill-nav-scroll-left  { display: flex; }
.pill-nav-wrap.has-overflow.is-overflowing-right > .pill-nav-scroll-right { display: flex; }

.pill-nav-scroll-left:hover,
.pill-nav-scroll-right:hover {
    color: var(--color-text);
}

.pill-nav-scroll-left:focus-visible,
.pill-nav-scroll-right:focus-visible {
    outline: 2px solid var(--color-text);
    outline-offset: -2px;
}


/* Pill count badge — canonical name `.users-tab-count`, with
   `.pill-count` kept as an alias for the Phase A primitive
   vocabulary. Visual treatment matches Users page exactly:
   white-bordered pill with muted text, inverts to subtle-gray
   on the active pill. Source of truth for contracts and any
   future module that adopts the pill-nav pattern. */
.users-tab-count,
.pill-count {
    display: inline-block;
    margin-left: 6px;
    padding: 0 6px;
    min-width: 18px;
    height: 18px;
    line-height: 18px;
    font-size: 0.72rem;
    font-weight: 500;
    text-align: center;
    background: var(--color-white);
    border: 1px solid var(--color-border);
    border-radius: 9px;
    color: var(--color-text-muted);
}

.pill.is-active .pill-count,
.pill.is-active .users-tab-count,
.settings-nav-btn-active .pill-count,
.settings-nav-btn-active .users-tab-count {
    background: var(--color-bg);
    color: var(--color-text);
}

/* On narrow viewports the count badge is the first thing to drop:
   the pill row already has limited horizontal budget, and the tab
   label is the actionable signal. Numbers stay visible on desktop
   where they fit comfortably. */
@media (max-width: 640px) {
    .users-tab-count,
    .pill-count {
        display: none;
    }
}


/* ── Table modifiers ───────────────────────────────────────────
   `.table-tight`         — compact row padding for dense lists
                            (matches the inline overrides in
                            clockwork's `.cw-sessions-table`).
   `.table-cards-mobile`  — generic mobile card transform with
                            `.col-check / .col-name / .col-meta /
                            .col-time / .col-dur / .col-actions`
                            columns. Render-identical to the
                            existing inline rules in clockwork
                            and dashboard. Phase B converts call
                            sites to use these names. */
.table-tight th,
.table-tight td {
    padding: 6px var(--space-sm);
}

.table-tight td.col-actions { text-align: right; white-space: nowrap; }

@media (max-width: 640px) {
    .table-cards-mobile,
    .table-cards-mobile tbody { display: block; }
    .table-cards-mobile thead { display: none; }

    .table-cards-mobile tbody tr {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 0;
        padding: var(--space-sm) var(--space-md);
        border-bottom: 1px solid var(--color-border);
    }

    /* Invisible spacer forces a wrap between row 1 and row 2. */
    .table-cards-mobile tbody tr::after {
        content: '';
        flex-basis: 100%;
        height: 0;
        order: 50;
    }

    .table-cards-mobile td {
        display: block;
        border: none;
        padding: 0;
        white-space: nowrap;
    }

    .table-cards-mobile td.col-check {
        flex: 0 0 28px;
        order: 1;
    }
    .table-cards-mobile td.col-check input[type="checkbox"] {
        margin: 0;
    }
    .table-cards-mobile td.col-name {
        flex: 1;
        order: 2;
        font-weight: 500;
        min-width: 0;
        white-space: normal;
    }
    .table-cards-mobile td.col-actions {
        flex: 0 0 auto;
        order: 3;
        text-align: right;
    }
    .table-cards-mobile td.col-meta,
    .table-cards-mobile td.col-time,
    .table-cards-mobile td.col-dur {
        flex: 0 0 auto;
        order: 60;
        font-size: var(--font-size-sm);
        color: var(--color-text-muted);
        margin-top: 2px;
        margin-right: 8px;
    }

    /* Indent meta line under name when there's a checkbox column,
       so it aligns with the name column above (28px col + 8px gap). */
    .table-cards-mobile tbody tr:has(> td.col-check) td.col-meta,
    .table-cards-mobile tbody tr:has(> td.col-check) td.col-time {
        margin-left: 36px;
    }
}


/* ── Destructive confirm modal ─────────────────────────────────
   Visual treatment for confirm-destructive flows. Add
   `.modal-confirm-danger` to the `.modal` element. Subtle
   danger framing — never neutralises the destructive intent
   but doesn't shout either. */
.modal-confirm-danger .modal-body {
    padding-top: var(--space-xl);
}

.modal-confirm-danger .confirm-icon {
    display: block;
    width: 48px;
    height: 48px;
    margin: 0 auto var(--space-md);
    border-radius: 50%;
    background: var(--color-danger-bg);
    color: var(--color-danger);
    font-size: 1.5rem;
    line-height: 48px;
    text-align: center;
    font-weight: 700;
}

.modal-confirm-danger .confirm-title {
    font-size: var(--font-size-lg);
    font-weight: 600;
    text-align: center;
    margin: 0 0 var(--space-xs);
}

.modal-confirm-danger .confirm-body {
    text-align: center;
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    line-height: 1.5;
    margin: 0 auto;
    max-width: 360px;
}


/* ── Layout utilities ──────────────────────────────────────────
   Canonical flex/spacing primitives so templates can stop
   carrying inline `style="display:flex;gap:8px"` blocks.

   Note: `.flex`, `.items-center`, `.gap-sm`, `.gap-md`, `.gap-lg`,
   `.mt-md`, `.hide-mobile`, `.show-mobile` already exist above
   in section 17 and section 16 respectively (768px breakpoint).
   Phase A does NOT redefine them — it only adds the missing
   primitives below. */
.stack            { display: flex; flex-direction: column; }
.stack-xs         { display: flex; flex-direction: column; gap: var(--space-xs); }
.stack-sm         { display: flex; flex-direction: column; gap: var(--space-sm); }
.stack-md         { display: flex; flex-direction: column; gap: var(--space-md); }
.stack-lg         { display: flex; flex-direction: column; gap: var(--space-lg); }

.row              { display: flex; align-items: center; }
.row-wrap         { display: flex; flex-wrap: wrap; align-items: center; }
.row-baseline     { display: flex; align-items: baseline; }
.row-top          { display: flex; align-items: flex-start; }

.gap-xs           { gap: var(--space-xs); }

.flex-1           { flex: 1; min-width: 0; }
.flex-auto        { flex: 0 0 auto; }
.ml-auto          { margin-left: auto; }
.mr-auto          { margin-right: auto; }


/* ── Card section heading (sentence case) ──────────────────────
   Distinct from `.card-title` which is uppercase + muted (legacy
   admin-list flavour). Use `.card-section-title` for operational
   workspaces — incident detail, contract sub-sections, anywhere
   readability beats label-stamping. */
.card-section-title {
    font-size: var(--font-size-base);
    font-weight: 600;
    margin: 0 0 var(--space-md);
    color: var(--color-text);
}


/* ── Vertical timeline (audit log, event history) ──────────────
   Used for any chronological event stream — incident events,
   contract lifecycle, future audit logs. Each `.timeline-item`
   draws a left-border guide-line; the dot sits on that line via
   ::before. Last item drops the line so the column ends cleanly. */
.timeline {
    list-style: none;
    margin: 0;
    padding: 0;
}

.timeline-item {
    position: relative;
    padding: 0 0 var(--space-md) var(--space-lg);
    border-left: 2px solid var(--color-border);
    margin-left: 6px;
}

.timeline-item:last-child {
    padding-bottom: 0;
    border-left-color: transparent;
}

.timeline-item::before {
    content: '';
    position: absolute;
    left: -7px;
    top: 4px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--color-surface);
    border: 2px solid var(--color-text-muted);
}

.timeline-meta {
    font-size: 0.8rem;
    color: var(--color-text-muted);
    margin-bottom: 2px;
}

.timeline-body {
    font-size: 0.875rem;
    line-height: 1.4;
}


/* ── Attachment grid + cards (thumbnails for images, file
   cards for other types). Generic: any module that surfaces
   attachments — incidents, contracts, forms — uses this. */
.attachment-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
    gap: var(--space-sm);
}

.attachment-card {
    display: block;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--color-surface);
    color: var(--color-text);
    text-decoration: none;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.attachment-card:hover {
    border-color: var(--color-text-muted);
    box-shadow: var(--shadow-sm);
    text-decoration: none;
}

.attachment-thumb {
    display: block;
    width: 100%;
    height: 120px;
    object-fit: cover;
    background: var(--color-bg);
}

.attachment-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 80px;
    font-size: 1.4rem;
    color: var(--color-text-muted);
    background: var(--color-bg);
}

.attachment-meta {
    padding: var(--space-xs) var(--space-sm);
}

.attachment-name {
    font-size: 0.8125rem;
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.attachment-size {
    font-size: 0.75rem;
    color: var(--color-text-muted);
    margin-top: 2px;
}


/* ── <button> elements used as .settings-nav-btn need browser-
   default chrome reset so they render identically to the <a>
   variant. Used for segmented form-submit controls (incident
   status workflow, future filter rows). */
button.settings-nav-btn {
    background: none;
    cursor: pointer;
    font-family: inherit;
    font-size: inherit;
}


/* ── Workflow stack — vertical full-width state buttons ───────
   Use when 3–5 states need a clear list-style selector inside a
   narrow card (sidebar status panel, modal form section). Each
   button is a full-width row with the active state visually
   selected. Distinct from the segmented pill bar (horizontal
   row, equal columns) — pick the stack when labels are workflow
   states that benefit from prominence and don't compete for
   horizontal space. */
.workflow-stack {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
}

.workflow-stack-btn {
    display: block;
    width: 100%;
    padding: 9px 14px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
    color: var(--color-text-muted);
    cursor: pointer;
    font-family: inherit;
    font-size: var(--font-size-sm);
    font-weight: 500;
    text-align: left;
    transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}

.workflow-stack-btn:hover:not(.is-active) {
    border-color: var(--color-text-muted);
    color: var(--color-text);
}

.workflow-stack-btn.is-active {
    background: #ebebeb;
    border-color: var(--color-border-strong);
    color: var(--color-text);
    font-weight: 600;
    cursor: default;
}


/* ── Full-width segmented pill bar ─────────────────────────────
   Modifier on `.settings-nav` that turns the nav into a single-row
   segmented control with equal-width children. Use for narrow
   controls that should fill their container — sidebar status
   cards, modal sub-toolbars, narrow form sections — where 3–5
   short options claim equal space.

   NOT for wide list-page filter rows: stretching 5 pills across
   a 1200px row produces "five stretched columns" (per user
   feedback). Wide filter rows use plain `.settings-nav`
   (natural-width, grouped, content-sized). */
.settings-nav.settings-nav-segmented {
    display: flex;
    flex-wrap: nowrap;       /* override parent's wrap — one row, always */
    width: 100%;
}

.settings-nav.settings-nav-segmented > .settings-nav-btn {
    flex: 1 1 0;             /* equal share of the row */
    min-width: 0;            /* allow shrinkage so siblings don't push out */
    padding: 5px 8px;        /* tighter than default 5px 14px for narrow fits */
    text-align: center;
    justify-content: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}


/* ── Time-range glyph helper class ─────────────────────────────
   Wrap a time/date span in `.time-range` when it should never
   wrap mid-glyph (e.g. inside narrow table columns). */
.time-range {
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}


/* ── Incident case workspace ───────────────────────────────────
   Single-workspace layout: page banner → tab nav → one card per tab.
   No competing surfaces. Reusable for SJA / deviations / safety
   observations.                                                  */

/* Priority pills */
.pill-priority {
    display: inline-flex;
    align-items: center;
    padding: 3px 10px;
    border-radius: var(--radius-md);
    font-size: 0.8125rem;
    font-weight: 600;
    line-height: 1.3;
}
.pill-priority-low      { background: var(--color-badge-neutral-bg); color: #555; }
.pill-priority-medium   { background: var(--color-badge-warning-bg); color: var(--color-warning); }
.pill-priority-high     { background: var(--color-badge-danger-bg);  color: var(--color-danger); }
.pill-priority-critical { background: #fee2e2; color: #991b1b; }
.pill-priority-unset {
    background: transparent;
    color: var(--color-text-muted);
    border: 1px dashed var(--color-border);
}

/* Owner chip */
.owner-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 2px 10px 2px 2px;
    border-radius: 999px;
    background: var(--color-bg);
    border: 1px solid var(--color-border);
    max-width: 100%;
}
.owner-chip-avatar {
    width: 22px; height: 22px;
    border-radius: 50%;
    background: var(--color-avatar-bg);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.7rem;
    font-weight: 700;
    flex-shrink: 0;
}
.owner-chip-name {
    font-size: 0.85rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
.owner-chip-unassigned {
    color: var(--color-text-muted);
    font-style: italic;
    padding: 2px 12px;
}

/* Unassigned + clickable — reads as a clear "+ Assign owner" CTA
   instead of a passive italic placeholder. Drop the italic, drop the
   caret, lead with a `+` glyph. */
.owner-chip-cta {
    padding: 2px 12px 2px 6px;
    color: var(--color-text);
    font-weight: 500;
    font-style: normal;
    background: var(--color-bg);
    border-color: var(--color-border-strong);
}
.owner-chip-cta:hover {
    background: var(--color-accent-subtle);
    color: var(--color-text);
}
.owner-chip-plus {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--color-accent);
    color: #fff;
    font-size: 0.85rem;
    font-weight: 600;
    line-height: 1;
    flex-shrink: 0;
}
.owner-chip-cta-text {
    font-size: 0.85rem;
    line-height: 1.2;
}

/* Interactive chips — click to open the matching modal. Buttons reset
   so they look like the existing chips, plus a hover bg + caret hint. */
button.owner-chip,
button.pill-priority {
    font: inherit;
    cursor: pointer;
    transition: background 0.12s ease, border-color 0.12s ease;
}
.owner-chip-button {
    padding-right: 8px;            /* room for the caret */
}
.pill-priority-button {
    padding-right: 8px;
    gap: 6px;
}
.owner-chip-button:hover,
.pill-priority-button:hover {
    border-color: var(--color-border-strong);
}
.owner-chip-button:focus-visible,
.pill-priority-button:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: 2px;
}
.chip-caret {
    display: inline-block;
    margin-left: 4px;
    font-size: 0.7em;
    line-height: 1;
    opacity: 0.6;
    transition: opacity 0.12s ease, transform 0.12s ease;
}
.owner-chip-button:hover .chip-caret,
.pill-priority-button:hover .chip-caret {
    opacity: 1;
}

/* ── Page banner (no card chrome) ──────────────────────────────
   Holds the tab nav (left) and action buttons (right) on the
   same row. No bottom border — the bordered .case-content card
   below provides the visual page-content boundary. */
.case-banner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: var(--space-md);
}
.case-banner-main {
    flex: 1 1 auto;
    min-width: 0;
}
.case-title {
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.25;
    margin: 0 0 2px;             /* tightened from 4px */
    color: var(--color-text);
    word-wrap: break-word;
}
.case-meta { margin: 0 0 10px; }  /* breathes between meta block and chip strip */
/* Secondary meta line ("Last updated …") — same family, slightly lighter
   so it reads as supplementary, not equal weight to the Submitted line. */
.case-meta-secondary { color: var(--color-text-light); }

.case-status-strip {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;                     /* tightened from var(--space-sm) */
    font-size: 0.85rem;
}
/* Unify the three chip sizes (Status badge, Priority pill, Owner chip)
   so the strip reads as one row of equal-weight indicators. */
.case-status-strip .badge {
    padding: 3px 10px;
    font-size: 0.8125rem;
    font-weight: 600;
    line-height: 1.3;
    border-radius: var(--radius-md);
}

/* ── Chip popovers (priority + owner) ─────────────────────────
   Anchored dropdown menus. Replace heavyweight modals for inline
   metadata edits — selecting an option fires POST and reloads. */
.chip-anchor {
    position: relative;
    display: inline-flex;
    align-items: center;
}
.chip-popover {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: 0 8px 28px rgba(0,0,0,0.12);
    padding: 4px;
    z-index: 30;
    min-width: 200px;
    max-width: calc(100vw - 16px);
}
.chip-popover-wide {
    min-width: 320px;
    padding: var(--space-sm);
}
.chip-popover[hidden] { display: none; }

.chip-popover-item {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 6px 8px;
    background: transparent;
    border: none;
    text-align: left;
    border-radius: calc(var(--radius-md) - 2px);
    cursor: pointer;
    font: inherit;
    color: var(--color-text);
    transition: background 0.1s ease;
}
.chip-popover-item:hover { background: var(--color-bg); }
.chip-popover-item[aria-current="true"] {
    background: var(--color-bg);
}
.chip-popover-item[aria-current="true"]::after {
    content: '✓';
    margin-left: auto;
    color: var(--color-text-muted);
    font-size: 0.85rem;
}
.chip-popover-sep {
    border: 0;
    border-top: 1px solid var(--color-border);
    margin: 4px 0;
}

/* Wide popover header (owner): title + Clear assignment */
.chip-popover-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    padding: 4px 4px 8px;
    border-bottom: 1px solid var(--color-border);
    margin-bottom: 8px;
}
.chip-popover-title {
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
    font-weight: 600;
}
.chip-popover-clear {
    background: transparent;
    border: none;
    color: var(--color-danger);
    font-size: 0.78rem;
    font-weight: 500;
    cursor: pointer;
    padding: 2px 6px;
    border-radius: var(--radius-sm);
}
.chip-popover-clear:hover {
    background: var(--color-danger-ghost-bg);
}
.chip-popover-body { padding: 0 4px 4px; }

.case-banner-actions {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    flex-wrap: wrap;
    flex-shrink: 0;
}

/* More-actions overflow (native <details>) */
.case-more-menu { position: relative; }
.case-more-menu summary { list-style: none; cursor: pointer; }
.case-more-menu summary::-webkit-details-marker { display: none; }
.case-more-menu[open] summary { background: var(--color-bg); }
.case-more-panel {
    position: absolute;
    top: calc(100% + 4px);
    right: 0;
    min-width: 220px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: 0 6px 24px rgba(0,0,0,0.10);
    padding: 4px;
    z-index: 20;
}
.case-more-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: 8px 10px;
    background: transparent;
    border: none;
    text-align: left;
    font-size: 0.875rem;
    color: var(--color-text);
    text-decoration: none;     /* normalise <a> variant */
    border-radius: calc(var(--radius-md) - 2px);
    cursor: pointer;
    gap: var(--space-sm);
}
.case-more-item:hover:not(:disabled) { background: var(--color-bg); }
.case-more-item:disabled {
    color: var(--color-text-muted);
    cursor: not-allowed;
}
/* Destructive action (e.g. soft delete) — danger text, danger-tinted hover. */
.case-more-item-danger { color: var(--color-danger); }
.case-more-item-danger:hover:not(:disabled) { background: #fef3f2; }
/* Disabled-with-reason variant rendered as a <span> (no :disabled pseudo). */
.case-more-item-disabled {
    color: var(--color-text-muted);
    cursor: not-allowed;
}
.case-more-item-disabled:hover { background: transparent; }
.case-more-tag {
    font-size: 0.65rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-text-muted);
    background: var(--color-bg);
    padding: 2px 6px;
    border-radius: var(--radius-sm);
    font-weight: 600;
}

/* ── Content card — wraps tab nav + all panels ───────────────────
   One card; tabs sit at the top edge so they feel attached to the
   panel below. Card padding is 0; the nav and panels handle their
   own internal padding. */
.case-content { padding: 0; overflow: visible; }

/* Tab nav — uses HR Cluster .pill-nav. `.case-tab-nav` lives as
   the LEFT flex child of `.case-banner`, sharing the same row as
   the right-side action buttons. No own margin or flex grow —
   .pill-nav is `display: inline-flex` so the nav stays compact
   (shrink-to-fit content, matches the /workspace/contracts
   pattern). The banner's justify-content: space-between handles
   the left/right alignment. */
.case-tab-nav {
    margin: 0;
}

/* Pill counts inside the case-workspace tab nav — polished variant
   of the global .pill-count. Borderless, slightly heavier, with a
   solid-dark active state for clear contrast. Scoped so we don't
   override the source-of-truth used by Users/Contracts. */
.case-tab-nav .pill-count {
    border: none;
    background: var(--color-bg);
    color: var(--color-text-muted);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    min-width: 20px;
    padding: 0 7px;
    margin-left: 7px;
}
.case-tab-nav .pill.is-active .pill-count {
    background: var(--color-accent);
    color: #fff;
}

/* ── Tab panels — sit inside .case-content, separated from the
   nav by a hairline border so they read as one connected unit. */
.case-tab-panel {
    padding: var(--space-md) var(--space-lg) var(--space-lg);
    border-top: 1px solid var(--color-border);
    margin-top: var(--space-md);
}
.case-tab-panel[hidden] { display: none; }
/* When a panel is the first visible child after the nav (single
   tab open at any time) the border-top above acts as the divider. */

/* ── Top-of-card operational state row — split layout.
   LEFT  : live operational state (status badge + priority/risk
           chip + owner chip). Interactive controls.
   RIGHT : passive audit metadata (submitted/created timestamps,
           last-updated). Plain text, NOT pills.
   Collapses to a single column at <640px so the pills sit above
   the timestamps on phones. */
.case-state-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--space-md);
    align-items: center;
    padding: var(--space-sm) var(--space-lg);
    margin-top: var(--space-md);
    border-top: 1px solid var(--color-border);
}
.case-meta-inline {
    font-size: 0.85rem;
    color: var(--color-text-muted);
    line-height: 1.5;
    text-align: right;
}
@media (max-width: 640px) {
    .case-state-row {
        grid-template-columns: 1fr;
    }
    .case-meta-inline {
        text-align: left;
    }
}

/* When .case-tab-nav lives OUTSIDE the .case-content card (standard
   HR Cluster detail-page layout), the state-row is the first child
   inside the card. The card's own border provides the top boundary,
   so the inner row's hairline border-top would be a redundant double
   divider. Drop the top border + margin when first inside the card,
   and on tab panels that follow the state-row. */
.case-content > .case-state-row:first-child {
    border-top: none;
    margin-top: 0;
}
.case-state-row ~ .case-tab-panel {
    border-top: none;
    margin-top: 0;
}

/* Detail table — fills the left column rail. The 70/30 case-detail-grid
   is what constrains the measure; no per-element max-width. Label column
   stays at 160px so values line up with the rest of the column rhythm. */
.case-tab-panel .detail-table {
    width: 100%;
    max-width: none;
    margin: 0 0 var(--space-md);
}
.case-tab-panel .detail-table th {
    width: 160px;
    color: var(--color-text-muted);
    font-weight: 500;
}
.case-tab-panel .detail-table td {
    text-align: left;
    color: var(--color-text);
}

/* "What happened" summary block — fills the left column rail with the
   same width as the meta rows / form fields below. Stronger background
   tone + larger body text mark it as a feature block, not just a
   tinted paragraph. */
.case-summary {
    background: #eef0f3;          /* cooler, slightly darker than --color-bg */
    padding: 18px 20px;            /* stronger padding (was 14/16) */
    border-radius: var(--radius-md);
    margin: 0 0 var(--space-md);
    width: 100%;
    max-width: none;
}
.case-summary-label {
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
    font-weight: 600;
    margin-bottom: 6px;            /* slightly more separation from body */
}
.case-summary-text {
    font-size: 1rem;               /* up from 0.95rem — featured size */
    line-height: 1.6;
    color: var(--color-text);
    white-space: pre-wrap;
    margin: 0;
}

/* Type/category pills — sit between summary and meta rows */
.case-pills {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 0 0 var(--space-md);
}
.case-pill {
    display: inline-flex;
    align-items: center;
    padding: 3px 10px;
    font-size: 0.78rem;
    font-weight: 500;
    background: var(--color-bg);
    color: var(--color-text);
    border-radius: 999px;
    border: 1px solid var(--color-border);
    white-space: nowrap;
}

/* In-tab section divider (Immediate action / Follow-up).
   Used by Incidents detail tab AND Risk Assessments detail tab —
   replaces card-within-card nesting with a flat hairline divider
   between sections. The first section in any tab panel (or column
   inside a panel grid) drops the divider so the page doesn't open
   with a stray border under the meta-row. */
.case-section {
    margin-top: var(--space-md);
    padding-top: var(--space-md);
    border-top: 1px solid var(--color-border);
}
.case-tab-panel > .case-section:first-child,
.case-tab-panel .ra-detail-col > .case-section:first-child {
    margin-top: 0;
    padding-top: 0;
    border-top: none;
}
.case-subhead {
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
    font-weight: 600;
    margin: 0 0 var(--space-sm);
}
.case-section-body {
    margin: 0;
    white-space: pre-wrap;
    font-size: 0.92rem;
    line-height: 1.55;
    color: var(--color-text);
}

/* ── Details tab — 70/30 internal grid ────────────────────────
   Left: narrative + form. Right: utility (owner / actions / activity / files).
   Stacks below 900px so the side moves below the main on tablets/phones. */
.case-detail-grid {
    display: grid;
    grid-template-columns: minmax(0, 7fr) minmax(0, 3fr);
    gap: var(--space-xl);
    align-items: start;
}
.case-detail-main { min-width: 0; }
.case-detail-side {
    min-width: 0;
    /* Sticky on desktop — keeps Owner / Quick actions / Recent activity
       visible while the user scrolls through the long left column. */
    position: sticky;
    top: var(--space-md);
    align-self: start;
}

@media (max-width: 900px) {
    .case-detail-side {
        position: static;
        top: auto;
    }
    .case-detail-grid {
        grid-template-columns: 1fr;
        gap: var(--space-lg);
    }
    /* On mobile the desktop Quick actions section is replaced by a
       prominent band at the top of the panel — hide the rail copy. */
    .case-side-section--actions { display: none; }
}

/* Mobile-only Quick actions band. Sits at the top of the Details
   tab panel so Close / Reopen aren't buried below the full form.
   Hidden on desktop (the side-rail Quick actions handles it there). */
.case-mobile-actions { display: none; }
@media (max-width: 900px) {
    .case-mobile-actions {
        display: block;
        margin: 0 0 var(--space-md);
        padding: 0 0 var(--space-md);
        border-bottom: 1px solid var(--color-border);
    }
    .case-mobile-actions-row {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-sm);
    }
    .case-mobile-action-btn {
        flex: 1 1 auto;
        min-width: 140px;
        justify-content: center;
    }
}

/* Side column — flat sub-sections divided by hairline rules.
   No nested cards (would create surface noise inside the parent card). */
.case-side-section + .case-side-section {
    margin-top: var(--space-md);
    padding-top: var(--space-md);
    border-top: 1px solid var(--color-border);
}
.case-side-label {
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
    font-weight: 600;
    margin: 0 0 var(--space-sm);
}
.case-side-link {
    display: inline-block;
    margin-top: var(--space-xs);
    font-size: 0.78rem;
    color: var(--color-text-muted);
}
.case-side-link:hover { color: var(--color-text); }
.case-side-btn {
    width: 100%;
    justify-content: center;
}

/* Owner summary in side column */
.case-side-owner {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    min-width: 0;
}
.case-side-avatar {
    width: 32px;
    height: 32px;
    font-size: 0.8rem;
    flex-shrink: 0;
}
.case-side-owner-text {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.case-side-owner-name {
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--color-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Quick actions — full-width stacked buttons */
.case-side-actions {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

/* Recent activity timeline — tighter than main timeline,
   fits the narrow side column */
.case-side-timeline { font-size: 0.85rem; }
.case-side-timeline .timeline-item { padding-bottom: 14px; }

.case-side-files-count {
    font-size: 0.9rem;
    font-weight: 500;
    color: var(--color-text);
    margin: 0;
}

/* Log tab — "Latest:" header row. Single-line summary of the most
   recent event; the full timeline follows directly below. */
.case-log-toprow {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 6px;
    margin: 0 0 var(--space-md);
    padding: 0 0 var(--space-md);
    border-bottom: 1px solid var(--color-border);
    font-size: 0.875rem;
    line-height: 1.4;
}
.case-log-latest-label { font-weight: 500; }
.case-log-latest-msg {
    color: var(--color-text);
    font-weight: 500;
    flex: 1 1 auto;
    min-width: 0;
}
.case-log-latest-time { white-space: nowrap; }

/* Timeline polish — scoped to case workspace.
   Quieter meta (lighter, smaller), stronger event body (slightly larger
   + heavier), more breathing room between entries. */
.case-tab-panel .timeline-item { padding-bottom: 18px; }   /* was 14px */
.case-tab-panel .timeline-meta {
    font-size: 0.7rem;                      /* was 0.72rem */
    color: var(--color-text-light);
    margin-bottom: 5px;                     /* was 4px */
    text-transform: none;
    letter-spacing: 0;
}
.case-tab-panel .timeline-body {
    font-size: 0.9rem;                      /* was 0.875rem */
    color: var(--color-text);
    font-weight: 500;
    line-height: 1.5;                       /* was 1.45 */
}
.case-tab-panel .timeline-body .text-muted { font-weight: 400; }

/* Attachments — proper thumbnail tile grid */
.case-tab-panel .attachment-grid {
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: var(--space-md);
}
.case-tab-panel .attachment-card {
    transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.case-tab-panel .attachment-card:hover {
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.case-tab-panel .attachment-thumb,
.case-tab-panel .attachment-icon { height: 140px; }
.case-tab-panel .attachment-icon { font-size: 1.75rem; }
.case-tab-panel .attachment-meta { padding: 10px var(--space-md) 12px; }
.case-tab-panel .attachment-name { font-size: 0.85rem; font-weight: 500; }
.case-tab-panel .attachment-size { font-size: 0.7rem; margin-top: 3px; }

/* Mobile-first responsive */
@media (max-width: 900px) {
    .case-banner { gap: var(--space-sm); }
    .case-banner-actions { width: 100%; }
}

@media (max-width: 640px) {
    .case-title { font-size: 1.25rem; }
    .case-status-strip { gap: var(--space-sm); }
    .case-tab-panel { padding: var(--space-md); }
    .case-tab { padding: 8px 10px; font-size: 0.85rem; }
    /* Counts hidden on mobile via .hide-mobile in markup */
}


/* End of Phase A primitives ───────────────────────────────── */


/* ════════════════════════════════════════════════════════════════
   List system — shared backend list alignment pattern.

   Used by Comms › Contacts, Comms › Channels, and (later) Items.
   Defines the primitive building blocks — fixed star slot, name
   cell, inline meta, plain compact number, supporting columns,
   row action — so future modules adopt the same look without
   reinventing or drifting.

   These are BACKEND-ONLY primitives (Workspace tables and cards).
   The public/output pages (e.g. /channels/{token}, /contacts/{token})
   use a different visual treatment by design — heavier badge for
   numbers, more spacious type — and do NOT use these classes.
════════════════════════════════════════════════════════════════ */

/* Fixed-width star slot. Always rendered (empty for non-pinned rows)
   so primary-name positions stay vertically aligned across the list. */
.list-star {
    display: inline-block;
    width: 12px;
    flex-shrink: 0;
    flex-basis: 12px;
    text-align: center;
    color: var(--color-text);
    font-weight: 700;
    line-height: 1;
}

/* Name cell — flex baseline row, 6px gap between primitives. Wraps a
   star slot, the primary-name content, and any inline meta. */
.list-name-cell {
    display: flex;
    align-items: baseline;
    gap: 6px;
    min-width: 0;
}
.list-name-content {
    min-width: 0;
    flex: 1 1 auto;
}

/* Inline meta — muted, smaller, non-wrapping technical/supporting text
   sitting after the primary name (e.g. "Repeater · Digital"). */
.list-meta {
    color: var(--color-text-muted);
    font-size: 0.85em;
    font-weight: 500;
    white-space: nowrap;
    flex-shrink: 0;
}

/* Plain compact number — visible scan target, but not dominant. Used
   where a number is structural (channel number) and the badge style
   would compete with the name. No background. */
.list-num-plain {
    display: inline-block;
    min-width: 22px;
    font-weight: 500;
    color: var(--color-text-muted);
    flex-shrink: 0;
}

/* Supporting columns — Title / Email / Phone / Notes etc. Apply this
   class to the <td> for any column that should read as data, not as
   the primary scan column. Slight opacity reduction so the eye lands
   on the primary column first. */
td.list-col-supporting { opacity: 0.85; }

/* Row action button (Edit, etc.) — repetitive low-priority action.
   Muted color + lighter border by default, restores on hover.
   Apply alongside .btn .btn-secondary on the action button itself. */
.btn.list-row-action {
    color: var(--color-text-muted);
    border-color: rgba(0,0,0,0.1);
}
.btn.list-row-action:hover {
    color: var(--color-text);
    border-color: var(--color-border-strong, rgba(0,0,0,0.2));
}

/* Row selection + bulk-action bar (Gmail pattern). Same visual as the
   Production Plan bulk bar (.pp-bulkbar) but kept as list-system
   primitives: these surfaces are plain PRG forms, not the plan's AJAX
   editor, and the pp-* rules carry plan-specific media behavior. */
.list-col-sel { width: 30px; text-align: center; }
.list-col-sel input[type="checkbox"] { margin: 0; vertical-align: middle; }
.list-bulkbar {
    display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
    padding: 8px 12px; margin-bottom: var(--space-sm);
    border: 1px solid var(--color-border); border-radius: var(--radius-md);
    background: var(--color-surface);
}
.list-bulkbar[hidden] { display: none; }
.list-bulkbar-count { font-weight: 600; font-size: .85rem; margin-right: 4px; white-space: nowrap; }
/* Mobile-card selection checkbox — floats right of the card's first
   line so the name still leads and no extra row is spent on it. */
.list-card-sel { float: right; margin: 2px 0 0 8px; }
@media (pointer: coarse) {
    .list-col-sel input[type="checkbox"],
    input[type="checkbox"].list-card-sel { width: 22px; height: 22px; }
    .list-bulkbar .btn { min-height: 38px; }
}

/* End of List system ─────────────────────────────────────── */


/* ── Submission review modal (Forms approvals) ───────────────
   Used at /{slug}/workspace/forms/{id}/submissions when a
   reviewer opens a submission. Wider than the standard 520px
   modal so submitted fields breathe; mobile collapses to one
   column with a sticky footer.

   Spacing rhythm: tight inside fields and sections (lets data
   feel grouped), looser between sections (separates intent).
   Modal height stays content-driven up to 90dvh — no forced
   minimum, no scrollbar for typical submissions. */
/* .modal-review (820px) retired 2026-09-01: the review modal now uses
   .modal-lg like every other shell consumer — ONE large scale (owner:
   the review modal must look like the global large modal). The
   .modal-footer-review and .submission-review-* content classes below
   remain in use. */

/* .modal-shell-head/-meta are the NEUTRAL names for these two rules,
   used by the shared full-modal shell (layout/components/
   modal_shell.php). The Forms review modal keeps its original class
   names — grouped selectors, not a copy, so the two cannot drift. */
.submission-review-head,
.modal-shell-head {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    min-width: 0;
}

.submission-review-meta,
.modal-shell-meta {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    flex-wrap: wrap;
}

/* Shared loading / error note used by hrcModal fragment loads (one
   implementation replacing the three per-page copies). */
.modal-shell-loading {
    padding: var(--space-md);
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    text-align: center;
}

.submission-review-date {
    color: var(--color-text-muted);
    font-size: 0.8125rem;
}

/* Sections: tighter inside, looser between. */
.submission-review-section + .submission-review-section {
    margin-top: var(--space-xl);
    padding-top: var(--space-lg);
    border-top: 1px solid var(--color-border);
}

.submission-review-section-title {
    display: block;
    margin: 0 0 var(--space-sm);
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--color-text-muted);
}

.submission-review-meta-line {
    color: var(--color-text-muted);
    font-size: 0.8125rem;
}

/* Fields layout — column flow, NOT row-synchronised grid.
   Real onboarding submissions have wildly uneven field heights
   (4-line addresses next to 1-line phone numbers, missing
   optional fields, etc.). A traditional grid forces row height
   to the tallest cell which makes neighbour columns look broken.
   Multi-column flow lets each field size to its own content and
   the columns balance naturally. */
.submission-review-compact {
    column-count: 3;
    column-gap: var(--space-lg);
    /* Column height is balanced from the available content; if the
       browser doesn't honour `column-fill: balance` (rare), columns
       still flow correctly — just unbalanced. */
    column-fill: balance;
}

.submission-review-blocks {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
    margin-top: var(--space-md);
    padding-top: var(--space-md);
    border-top: 1px dashed var(--color-border);
}

/* Compact-flow fields. Each is its own block so multi-column
   never splits a label from its value mid-column. */
.submission-review-field {
    break-inside: avoid;
    -webkit-column-break-inside: avoid;
    page-break-inside: avoid;
    min-width: 0;
    /* Margin (not gap) — column-count uses CSS-margins for vertical
       rhythm between items inside a column. */
    margin: 0 0 var(--space-sm);
}

.submission-review-field:last-child {
    margin-bottom: 0;
}

/* Block fields (address, free-text notes) live below the column
   flow because their height varies most and would imbalance the
   column heights or split awkwardly across columns. */
.submission-review-field-block {
    margin: 0;
}

.submission-review-label {
    font-size: 0.75rem;
    color: var(--color-text-muted);
    margin-bottom: 1px;
    line-height: 1.3;
}

.submission-review-value {
    font-size: 0.9375rem;
    color: var(--color-text);
    line-height: 1.4;
    word-break: break-word;
}

.submission-review-value a {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
}

.submission-review-value-mono {
    font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
    font-size: 0.875rem;
    letter-spacing: 0.02em;
}

.submission-review-value-multi {
    white-space: pre-line;
}

/* Subordinate value (e.g. ICE phone under ICE name). */
.submission-review-subvalue {
    color: var(--color-text-muted);
    font-size: 0.8125rem;
    margin-top: 1px;
}

/* Masked bank account: bullets are visually quieter than the
   real digits, so the trailing 4 stand out for verification. */
.submission-review-mask {
    font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
    font-size: 0.875rem;
    letter-spacing: 0.06em;
}

.submission-review-mask-tag {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--color-badge-neutral-bg, rgba(0,0,0,0.05));
    color: var(--color-text-muted);
    font-size: 0.6875rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    vertical-align: 1px;
}

/* Attachments — compact horizontal cards */
.submission-review-attachments {
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
}

.submission-review-attachment {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-xs) var(--space-sm);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    background: var(--color-surface);
}

.submission-review-attachment-thumb {
    flex-shrink: 0;
    width: 36px;
    height: 36px;
    border-radius: 6px;
    background: var(--color-bg);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}

.submission-review-attachment-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.submission-review-attachment-icon {
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    color: var(--color-text-muted);
}

.submission-review-attachment-meta {
    min-width: 0;
    flex: 1;
}

.submission-review-attachment-name {
    display: block;
    color: var(--color-text);
    font-size: 0.9375rem;
    font-weight: 500;
    text-decoration: none;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.submission-review-attachment-name:hover {
    text-decoration: underline;
    text-underline-offset: 2px;
}

.submission-review-attachment-size {
    font-size: 0.75rem;
    color: var(--color-text-muted);
    margin-top: 1px;
}

/* Reviewer note: supplementary, not the main event. Lighter
   border, smaller minimum height than the default form-control. */
.submission-review-note-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    margin-bottom: var(--space-sm);
}

.submission-review-note-head .submission-review-section-title {
    margin-bottom: 0;
}

.submission-review-note {
    border-color: var(--color-border);
    background: var(--color-bg);
    min-height: 60px;
    padding: var(--space-xs) var(--space-sm);
    font-size: 0.875rem;
}

.submission-review-note:focus {
    background: var(--color-surface);
}

/* Read-only state for decided submissions. Visually subdued so
   it reads as "this happened" rather than "edit me". */
.submission-review-note.submission-review-note-readonly {
    background: transparent;
    border-color: transparent;
    border-left: 2px solid var(--color-border);
    border-radius: 0;
    padding-left: var(--space-sm);
    color: var(--color-text);
    cursor: default;
    resize: none;
}

.submission-review-note.submission-review-note-readonly:focus {
    background: transparent;
    outline: none;
}

/* Footer with explicit action buttons. modal-footer-spacer pushes
   destructive/primary actions to the right while keeping Cancel
   on the left at desktop widths. */
.modal-footer-review {
    flex-wrap: wrap;
    gap: var(--space-xs);
}

.modal-footer-review .modal-footer-spacer {
    flex: 1;
}

@media (max-width: 900px) {
    .submission-review-compact {
        column-count: 2;
        column-gap: var(--space-md);
    }
}

@media (max-width: 640px) {
    .submission-review-compact {
        column-count: 1;
    }
    .modal-footer-review {
        gap: var(--space-xs);
    }
    .modal-footer-review .modal-footer-spacer {
        display: none;
    }
    .modal-footer-review .btn {
        flex: 1 1 calc(50% - var(--space-xs));
        min-width: 0;
    }
    /* Cancel takes a full row so the three review actions stay
       on one row at narrow widths. */
    .modal-footer-review .btn:first-child {
        flex-basis: 100%;
        order: -1;
    }
}


/* ============================================================
   Billing page — plan / addon / capacity-pack card buttons
   ============================================================
   .btn inherits body's line-height: 1.5 which puts the text box
   slightly below optical center inside the button's min-height.
   Collapsing line-height to 1 on buttons inside these cards lets
   the existing flex centering land glyphs dead-center without
   touching the global .btn rule.
*/
.plan-card .btn,
.addon-card .btn,
.capacity-pack-card .btn {
    line-height: 1;
    min-height: 40px;
}
.plan-card .btn-sm,
.addon-card .btn-sm,
.capacity-pack-card .btn-sm {
    min-height: 36px;
}

/* Capacity packs are utilitarian scaling — quieter than plans/Ops.
   Lighter background, slightly compressed spacing, no heavy borders.
   The .btn-secondary on the CTA already keeps them from competing
   with the primary plan/addon CTAs. */
.capacity-pack-card {
    background: color-mix(in srgb, var(--color-text) 2%, transparent);
}

/* ─────────────────────────────────────────────────────────────
   Global HR Cluster graph system
   Reusable card + SVG styling for operational dashboard graphs.
   Used by: dashboard staffing pressure (workspace/_dashboard_staffing.php)
   Future Ops/Workforce dashboard graphs should adopt these classes.
   ───────────────────────────────────────────────────────────── */
.hrc-graph-card {
    position: relative;
    background: var(--color-white);
    border: 1px solid rgba(0, 0, 0, 0.06);
    border-radius: var(--radius-md);
    padding: 10px 14px 8px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
}
.hrc-graph-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: var(--space-md);
    flex-wrap: wrap;
    margin-bottom: 0;
}
.hrc-graph-title {
    font-size: var(--font-size-sm);
    font-weight: 600;
    color: var(--color-text);
    letter-spacing: 0;
    text-transform: none;
    line-height: 1.2;
    margin: 0;
}
.hrc-graph-meta {
    font-size: 10px;
    color: var(--color-text-muted);
    line-height: 1.4;
}
.hrc-graph-title-block {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

/* Compact pill toggle (e.g. 15m / 60m time-window) — not wired by default */
.hrc-graph-toggle {
    display: inline-flex;
    background: rgba(0, 0, 0, 0.04);
    border-radius: 999px;
    padding: 2px;
    gap: 0;
}
.hrc-graph-toggle button {
    border: 0;
    background: transparent;
    padding: 4px 10px;
    border-radius: 999px;
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
    cursor: pointer;
    line-height: 1;
    font-weight: 500;
}
.hrc-graph-toggle button.is-active {
    background: var(--color-white);
    color: var(--color-text);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}

/* Controls row in the top-right — holds optional filters + legend.
   Wraps under the title block on narrow widths. */
.hrc-graph-controls {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
}

/* Location filter (and future contextual scopes) — compact muted pill */
.hrcg-filter-form {
    margin: 0;
    display: inline-flex;
}
.hrcg-filter-select {
    appearance: none;
    -webkit-appearance: none;
    font-family: inherit;
    font-size: 11px;
    color: var(--color-text-muted);
    background-color: var(--color-white);
    border: 1px solid rgba(0, 0, 0, 0.10);
    border-radius: 999px;
    padding: 3px 22px 3px 10px;
    cursor: pointer;
    line-height: 1.4;
    max-width: 180px;
    text-overflow: ellipsis;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%23999' stroke-width='1.5'><path d='M2 2 L6 6 L10 2'/></svg>");
    background-repeat: no-repeat;
    background-position: right 8px center;
    background-size: 8px;
    transition: border-color 0.12s ease, color 0.12s ease;
}
.hrcg-filter-select:hover {
    color: var(--color-text);
    border-color: rgba(0, 0, 0, 0.20);
}
.hrcg-filter-select:focus {
    outline: none;
    border-color: rgba(0, 0, 0, 0.32);
}

/* ── Vehicle category colour dot (Phase 1) ───────────────────────────
   Small inherited category indicator rendered next to a vehicle's type or
   plate on cards and the scan/gate result. Colour comes from the Vehicle
   Type (inline background); this class owns only the shape. It is NOT an
   access indicator — the green/red access verdict is a separate element. */
.veh-cat-dot {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 2px;
    flex: 0 0 auto;
    vertical-align: middle;
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
}

/* ── Ops desk toolbar (shared) ────────────────────────────────────────
   Used by /workspace/ops/accreditation/desk and /workspace/ops/vehicles/desk.
   Both pages render the same component, not two parallel layouts —
   change here, both desks move together.

   Visual size matches Users / Suppliers list controls: search input
   and time-window select both use .form-control (38px min-height,
   var(--radius-md), full-strength black border on focus). Schedule's
   smaller .hrc-toolbar-control was tried and rejected — the desk is
   a checkpoint surface, not an analytic toolbar; the form-control
   size carries more presence and reads cleaner alongside an empty
   results card.

   Row 1: [search input] [time-window select]
   Row 2: helper line ("Showing: Last 12h ➜ Next 12h") — bounded windows only.

   The search input is a native <input type="search"> + .form-control,
   which gets the global WebKit cancel-X (see locked rule above). No
   manual "Clear" link — the X clears the field; clicking it on a search
   input also fires `input`, so debounced auto-search re-runs naturally.
   ─────────────────────────────────────────────────────────────────── */
.desk-toolbar {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: 8px;
    /* Default: 8px to whatever follows (helper-or-results). When a
       non-empty helper sits right after the toolbar, the :has() rule
       below tightens this to 4px so the helper reads as a sub-label
       of the toolbar rather than its own block. When the helper is
       empty (or absent), the 8px default keeps the result list at a
       consistent visual distance from the toolbar across both desks. */
    margin-bottom: var(--space-sm);
}
.desk-toolbar:has(+ .desk-helper:not(:empty)) {
    margin-bottom: var(--space-xs);
}
.desk-search-form {
    display: flex; align-items: center;
    margin: 0;
    min-width: 0;
    /* Grow to fill the row — time-window select keeps its natural
       width and the search swallows the remainder. */
    flex: 1 1 220px;
}
.desk-search-form input.form-control[type="search"],
.desk-search-form input.form-control[type="text"] {
    width: 100%;
    min-width: 0;
}
/* Time-window select is a .form-control like the search input, but
   its natural width is the longest option string. Keep it auto-sized
   so it doesn't grow with the row — only the search does. */
.desk-toolbar > select.form-control {
    width: auto;
    flex: 0 0 auto;
}
.desk-helper {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    margin-bottom: var(--space-sm);
}
/* Collapse the helper element entirely when it has no content so the
   result list doesn't sit beneath an oversized dead zone. Accreditation
   defaults to 'all' (no helper text) and the desk renders the element
   empty until JS may fill it; vehicles renders empty when the resolver
   produces no message. In both cases, no text → no reserved space. */
.desk-helper:empty {
    display: none;
}
@media (max-width: 640px) {
    /* Stack to full-width controls so the toolbar reads cleanly on phones.
       Each control sits on its own line with consistent height. */
    .desk-toolbar { gap: 8px; }
    .desk-search-form { width: 100%; }
    .desk-search-form input.form-control[type="search"],
    .desk-search-form input.form-control[type="text"] {
        width: 100%;
        max-width: none;
        min-width: 0;
    }
    .desk-toolbar > select.form-control { width: 100%; }
}

/* ── Dashboard operational header ─────────────────────────────────────
   One row at the top of /workspace combining global scope filters (left)
   and live-refresh status (right). Wraps on narrow viewports so on mobile
   the scope pills sit above the live-refresh stamp.
   ─────────────────────────────────────────────────────────────────── */
.live-strip.dash-control-bar {
    margin-bottom: var(--space-md);
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: 12px 16px;
}
/* When the scope filter is rendered (form is the first child), we want it
   left-aligned and the live-strip-status pushed to the right. When there
   is no scope filter (form omitted by render helper), the status block is
   the only child and stays right-aligned via space-between. */
.live-strip.dash-control-bar .dash-scope-bar {
    margin-bottom: 0;
    flex: 0 1 auto;
}
.live-strip-status {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    margin-left: auto;
    color: var(--color-text-muted);
    font-size: 12px;
}

/* Global scope filter bar — slightly heavier than chart-local filters so
   readers can see it changes the whole page, not just one widget. */
.dash-scope-bar {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
}
.dash-scope-bar .hrcg-filter-select {
    min-width: 180px;
    max-width: 260px;
    font-size: 12px;
    color: var(--color-text);
    border-color: rgba(0, 0, 0, 0.20);
    padding: 5px 26px 5px 12px;
}
.dash-scope-bar .hrcg-filter-select:hover {
    border-color: rgba(0, 0, 0, 0.32);
}
.dash-scope-bar .hrcg-filter-select:focus {
    border-color: rgba(0, 0, 0, 0.48);
}
.dash-scope-reset {
    font-size: 11px;
    color: var(--color-text-muted);
    text-decoration: none;
    padding: 4px 10px;
    border-radius: 999px;
    line-height: 1.4;
}
.dash-scope-reset:hover {
    color: var(--color-text);
    background: rgba(0, 0, 0, 0.04);
}
/* Muted "account-wide" marker on non-scopable KPI cards while scope is set */
.dash-scope-account-wide-hint {
    font-style: italic;
    opacity: 0.72;
}

@media (max-width: 640px) {
    /* Stack scope filter above live-status on phones. flex-wrap already does
       the work; this just nudges the status block back to the left edge so
       both rows align visually. */
    .live-strip.dash-control-bar .live-strip-status {
        margin-left: 0;
    }
    .dash-scope-bar .hrcg-filter-select {
        min-width: 0;
        flex: 1 1 140px;
    }
}

/* ── Reusable toolbar control ──────────────────────────────────────────
   A shared rhythm for analytic-page filter toolbars where the row holds
   substantial form-like controls (selects + datetime inputs) above a
   chart or table.
   Sits intentionally between two existing patterns:
     - the small chart-local "filter pill" (.hrcg-filter-select) used
       above KPI grids
     - the legacy heavy "form-control" used inside data-entry forms
   Form-control-like (rounded corners, clear border, generous padding,
   consistent height) but lighter — never decorative, never pill-shaped.
   Use on <select>, <input type="date|datetime-local|number|text">.
   ─────────────────────────────────────────────────────────────────── */
.hrc-toolbar-control {
    appearance: none;
    -webkit-appearance: none;
    font-family: inherit;
    font-size: 13px;
    line-height: 1.4;
    color: var(--color-text);
    background-color: var(--color-white);
    border: 1px solid rgba(0, 0, 0, 0.18);
    border-radius: 8px;
    padding: 7px 12px;
    height: 36px;
    box-sizing: border-box;
    cursor: pointer;
    transition: border-color 0.12s ease;
}
.hrc-toolbar-control:hover  { border-color: rgba(0, 0, 0, 0.32); }
/* Active / selected input — same treatment as .form-control:focus
   (full-strength black border + subtle 2px outer ring). Locked as the
   global "this input has focus" signal across toolbars, lists, and
   forms. Do not weaken without auditing every consumer of
   .hrc-toolbar-control or .form-control. */
.hrc-toolbar-control:focus  {
    outline: none;
    border-color: var(--color-text);
    box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05);
}

/* Native WebKit search-clear X — locked global convention for
   <input type="search">. Required because .hrc-toolbar-control sets
   appearance:none which would otherwise strip the cancel button; we
   also style it on .form-control so a search input looks identical
   regardless of which form-control class it carries. The custom SVG
   keeps the X visually consistent across Chrome/Edge/Safari regardless
   of OS theme. Only renders when the input has a value (browser
   default for ::-webkit-search-cancel-button). */
input[type="search"].hrc-toolbar-control::-webkit-search-cancel-button,
input[type="search"].form-control::-webkit-search-cancel-button {
    -webkit-appearance: none;
    appearance: none;
    height: 14px;
    width: 14px;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23808080' stroke-width='2.5' stroke-linecap='round'><path d='M18 6L6 18M6 6l12 12'/></svg>");
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;
    cursor: pointer;
    margin-left: 6px;
    opacity: 0.6;
    transition: opacity 0.12s ease;
}
input[type="search"].hrc-toolbar-control::-webkit-search-cancel-button:hover,
input[type="search"].form-control::-webkit-search-cancel-button:hover {
    opacity: 1;
}

/* Select variant — native chevron is suppressed and replaced with a
   uniform 9px chevron so all selects look identical regardless of OS. */
select.hrc-toolbar-control {
    padding-right: 30px;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%23808080' stroke-width='1.5'><path d='M2 2 L6 6 L10 2'/></svg>");
    background-repeat: no-repeat;
    background-position: right 10px center;
    background-size: 9px;
}

input.hrc-toolbar-control { cursor: text; }
/* Datetime/date pickers stay clickable on the icon — keep pointer cursor
   so the calendar pop-up affordance is obvious. */
input.hrc-toolbar-control[type="date"],
input.hrc-toolbar-control[type="datetime-local"] {
    cursor: pointer;
}

/* Anchor variant — used for download/export actions that should sit at the
   same height + rhythm as the rest of the toolbar but read as a button.
   Hover uses the locked secondary-utility treatment (soft accent-subtle
   bg, stronger border, no underline) so it stays consistent with the
   .sched-stats-export-link chip below and the Power Analytics export
   chips — both are the same family of "utility, not primary" actions. */
a.hrc-toolbar-control {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-decoration: none;
    text-align: center;
    color: var(--color-text);
    padding-left: 14px;
    padding-right: 14px;
    transition: border-color 0.12s ease, color 0.12s ease,
                background-color 0.12s ease, box-shadow 0.12s ease;
}
a.hrc-toolbar-control:hover {
    color: var(--color-text);
    text-decoration: none;
    background-color: var(--color-accent-subtle);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}

/* ── Schedule → Statistics ─────────────────────────────────────────────
   Page-specific toolbar layout. Controls themselves use the reusable
   .hrc-toolbar-control style above.
   ─────────────────────────────────────────────────────────────────── */

/* Schedule tabs row trailing slot — used by the Statistics page to dock
   compact PDF / CSV links at the right edge of the pill nav row. The
   slot itself stays right-pinned via margin-left:auto inherited from the
   flex layout of .sched-tabs-row. Calm bordered chips matching the tab
   pill rhythm rather than toolbar-control sized rectangles. */
.sched-tabs-trailing {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
/* Locked secondary-utility chip — used by Schedule Statistics
   (PDF / CSV trailing slot) AND Power Analytics (PDF / CSV chips
   on the subnav row). Calm white-on-page button reserved for
   utility actions (export, print, email). Primary actions (Save,
   Publish, Approve, Delete) use .btn-primary instead.

   Hover: stronger border + soft accent-subtle bg shift + subtle
   shadow lift. NEVER underlines — utility chips are still
   buttons, not links. */
.sched-stats-export-link {
    display: inline-flex;
    align-items: center;
    height: 28px;
    padding: 0 12px;
    font-size: 12px;
    color: var(--color-text-muted);
    text-decoration: none;
    background: var(--color-white);
    border: 1px solid rgba(0, 0, 0, 0.15);
    border-radius: 6px;
    line-height: 1;
    transition: border-color 0.12s ease, color 0.12s ease,
                background-color 0.12s ease, box-shadow 0.12s ease;
}
.sched-stats-export-link:hover {
    color: var(--color-text);
    border-color: rgba(0, 0, 0, 0.32);
    background-color: var(--color-accent-subtle);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
    text-decoration: none;
}
.sched-stats-export-link:focus-visible {
    outline: 2px solid rgba(0, 0, 0, 0.32);
    outline-offset: 1px;
}

/* Mobile export pair lives inside the filter grid as the 4th cell —
   default hidden, mobile media query below toggles it on. */
.sched-stats-export-mobile { display: none; }

/* Same pattern for the Schedule listing's "Add shift" primary CTA: lives
   in the tabs row trailing slot on desktop, moves into the filter grid
   as a fill-the-empty-cell action on mobile. The desktop variant
   (.sched-tabs-add-shift) and mobile variant (.sched-stats-add-mobile)
   are toggled at the 1023px breakpoint below. */
.sched-stats-add-mobile { display: none; }

.sched-stats-controls {
    margin-bottom: var(--space-lg);
}
.sched-stats-row {
    display: flex;
    align-items: center;
    gap: 6px 10px;
    flex-wrap: wrap;
}
/* Three sibling control groups (filters | daterange | nav) so CSS can
   re-rank them on mobile without restructuring the markup. */
.sched-stats-group {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}

/* Desktop ≥1024: grid layout for the row. CSS Grid expresses "fill the
   remaining space, reserve natural width for the other two" cleanly —
   date range on the left, filters grow into the middle 1fr column,
   period nav stays compact on the right. Below this threshold the row
   doesn't have enough horizontal space without crushing the filter
   pills, so the mobile-style vertical stack takes over.

   Visual order is the DOM order on every form that uses this grid:
     • Schedule (#sched-filter-form):    daterange → filters → nav
     • Statistics (#sched-stats-form):   daterange → filters → nav
     • Crew-list (#cl-filter-form):      daterange (with prev/next
                                         arrows wrapping the date) →
                                         filters  (only 2 groups; the
                                         3rd column collapses to 0)
   Keeping placement implicit (no `grid-column: N` overrides) avoids
   edge cases where explicit column assignments interact unexpectedly
   with grid's implicit-row algorithm and split the toolbar onto two
   rows even when there's room for one. */
@media (min-width: 1024px) {
    .sched-stats-row {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto;
        gap: 10px;
        align-items: center;
    }
    /* Crew-list has only two groups (no period nav), so a 2-column
       track sized auto + 1fr is enough. The 3rd column from the
       default rule above would otherwise sit empty on the right. */
    #cl-filter-form .sched-stats-row {
        grid-template-columns: auto minmax(0, 1fr);
    }
    .sched-stats-group-filters {
        display: flex;
        align-items: center;
        gap: 6px;
        min-width: 0;
    }
    .sched-stats-group-filters .hrc-toolbar-control {
        flex: 1 1 0;
        min-width: 100px;
    }
}
.sched-stats-daterange {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
.sched-stats-daterange-label {
    font-size: 11px;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.sched-stats-arrow {
    color: var(--color-text-muted);
    font-size: 14px;
}

/* Period nav: ‹ Day Week Month › — arrows + preset chip group.
   Sits inline next to the date range so the whole toolbar reads as one
   row when the viewport allows. (No margin-left:auto — that greedily
   reserved horizontal space and forced the nav to wrap onto its own
   line at viewports where everything would otherwise fit.) */
.sched-stats-group-nav {
    gap: 4px;
    height: 36px;
}
.sched-stats-nav-arrow {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    font-size: 16px;
    color: var(--color-text-muted);
    text-decoration: none;
    background: var(--color-white);
    border: 1px solid rgba(0, 0, 0, 0.18);
    border-radius: 8px;
    line-height: 1;
    cursor: pointer;
    transition: border-color 0.12s ease, color 0.12s ease;
}
.sched-stats-nav-arrow:hover {
    color: var(--color-text);
    border-color: rgba(0, 0, 0, 0.32);
}
.sched-stats-presets {
    display: inline-flex;
    gap: 4px;
    padding: 3px;
    background: rgba(0, 0, 0, 0.04);
    border-radius: 8px;
    height: 32px;
    box-sizing: border-box;
}
.sched-stats-preset {
    font-size: 12px;
    color: var(--color-text-muted);
    text-decoration: none;
    padding: 0 12px;
    border-radius: 6px;
    line-height: 1;
    display: inline-flex;
    align-items: center;
}
.sched-stats-preset:hover {
    color: var(--color-text);
    background: rgba(0, 0, 0, 0.04);
}
.sched-stats-preset.is-active {
    color: var(--color-text);
    background: var(--color-white);
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.10);
    font-weight: 600;
}
.sched-stats-chart-wrap {
    margin-bottom: var(--space-md);
}
.sched-stats-table-card {
    margin-bottom: var(--space-md);
}
.sched-stats-table th.num,
.sched-stats-table td.num {
    text-align: right;
    white-space: nowrap;
}
.sched-stats-pct {
    color: var(--color-text-muted);
    font-size: 11px;
    margin-left: 2px;
}
/* ── Schedule Statistics: stacked row blocks (Phase 1.1) ──────────────
   Both Schedule Execution and Stats per Location render their rows as
   stacked blocks (not a wide table). Each block: bold title, inline
   Coverage + Clocked pair, then either an Accuracy sub-block (6-metric
   grid under a small heading) or a single "No completed sessions" line
   when the bucket / location produced zero closed sessions.

   Metrics grid uses auto-fit so the 6 cells flow as a single row on wide
   desktops, 2-3 columns on tablet, and 1 column on phones — without any
   explicit breakpoints. That's what makes the same markup serve both the
   per-row sub-block and the Shift Accuracy card's All / By role / By
   location subsections.

   Class system (kept independent of the old `.sched-stats-table` rules
   below so nothing about this block depends on table semantics):
     .sched-stats-row-list    — container for row blocks
     .sched-stats-row-block   — one row
     .sched-stats-row-title   — bold dimension label
     .sched-stats-pairs       — Coverage + Clocked container (inline)
     .sched-stats-pair        — single Label: value pair
     .sched-stats-row-accuracy — accuracy sub-block wrapper
     .sched-stats-accuracy-head — "Accuracy:" small heading
     .sched-stats-metrics     — 6-metric grid
     .sched-stats-metric      — single metric chip
   ─────────────────────────────────────────────────────────────────── */

.sched-stats-row-list {
    display: flex;
    flex-direction: column;
}
.sched-stats-row-block {
    padding: 12px 0;
    border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.sched-stats-row-block:first-child { padding-top: 4px; }
.sched-stats-row-block:last-child  { border-bottom: 0; padding-bottom: 4px; }

.sched-stats-row-title {
    font-weight: 600;
    font-size: 14px;
    color: var(--color-text);
    margin-bottom: 6px;
}

.sched-stats-pairs {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 24px;
    margin-bottom: 8px;
}
.sched-stats-pair {
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    line-height: 1.5;
}
.sched-stats-pair-label {
    color: var(--color-text-muted);
    margin-right: 4px;
}
.sched-stats-pair-value {
    color: var(--color-text);
}
.sched-stats-pair-value.status-danger  { color: var(--color-danger); }
.sched-stats-pair-value.status-warning { color: var(--color-warning); }
.sched-stats-pair-value.status-success { color: var(--color-success); }

.sched-stats-row-accuracy {
    margin-top: 2px;
}
.sched-stats-accuracy-head {
    font-size: 12px;
    color: var(--color-text-muted);
    margin-bottom: 4px;
}

/* Metrics grid — auto-fit means wide desktops get one row of 6, mobile
   collapses to a single column. minmax(110px) keeps each "Label  Value"
   pair from cramming, but lets it shrink to phone-comfortable widths. */
/* Metric grid — fixed 2-column layout so values stack vertically.
   For 6 metrics (Accuracy) lays out as 3 rows × 2; for 7 metrics
   (Variance All / Drift) the last odd-numbered tile spans both columns
   so N anchors the bottom without leaving an empty trailing cell. */
.sched-stats-metrics {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 6px;
}
.sched-stats-metric:last-child:nth-child(odd) {
    grid-column: span 2;
}
/* Tile chip — subtle background + tighter typography so the grid reads
   as a calm KPI block rather than a stretched table row. Label is
   uppercase microtype, value is bigger + semibold for vertical scanning. */
.sched-stats-metric {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 12px;
    padding: 6px 10px;
    background: rgba(0, 0, 0, 0.025);
    border-radius: 4px;
    font-variant-numeric: tabular-nums;
    line-height: 1.5;
}
.sched-stats-metric-label {
    color: var(--color-text-muted);
    font-size: 11px;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.sched-stats-metric-value {
    color: var(--color-text);
    font-size: 14px;
    font-weight: 600;
    /* Protect short numeric values (e.g. "31,9 pp", "+1,2h", "+14,3%")
       from wrapping mid-token on narrow chips. */
    white-space: nowrap;
}

/* Mobile chip layout (≤640px): keep the 2-column grid, but flip each
   chip from row to column so label sits above value (microtype label
   on its own line, semibold value below). Higher information density
   than a single-column stack — two metrics fit per row, still readable
   on narrow screens because labels and values are short. Background +
   label microtype + bold value all carry through from desktop styles. */
@media (max-width: 640px) {
    .sched-stats-metric {
        flex-direction: column;
        align-items: flex-start;
        gap: 2px;
    }
}

/* Idle bucket / location: compact "No shifts" line — replaces the
   full metric grid when planned = 0 (no shifts scheduled). Quiet,
   small, italic so a row of empty days reads as background. */
.sched-stats-no-shifts {
    font-size: 12px;
    color: var(--color-text-muted);
    font-style: italic;
}

/* ── Analytics help modal ─────────────────────────────────────────────
   Small ? icon in analytics card titles opens a shared modal explaining
   the percentile / AVG / STD / N concepts in plain language. One modal
   instance reused by Shift Accuracy / Variance / Drift (and future
   Arrival / Hotspots cards). Self-contained — does not depend on any
   other modal system in the codebase. */

.sched-stats-help-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    margin-left: 8px;
    padding: 0;
    border: 1px solid var(--color-border, rgba(0, 0, 0, 0.18));
    border-radius: 50%;
    background: transparent;
    color: var(--color-text-muted);
    font-size: 11px;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    vertical-align: middle;
    text-transform: none;
    letter-spacing: 0;
    transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.sched-stats-help-btn:hover,
.sched-stats-help-btn:focus-visible {
    background: var(--color-text);
    color: #fff;
    border-color: var(--color-text);
    outline: none;
}

.sched-stats-help-modal[hidden] { display: none; }
.sched-stats-help-modal {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: 5vh 16px;
    overflow-y: auto;
}
.sched-stats-help-modal__backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
}
.sched-stats-help-modal__panel {
    position: relative;
    z-index: 1;
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.25);
    max-width: 640px;
    width: 100%;
    max-height: 90vh;
    display: flex;
    flex-direction: column;
}
.sched-stats-help-modal__header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 14px 20px;
    border-bottom: 1px solid var(--color-border, rgba(0, 0, 0, 0.08));
    flex-shrink: 0;
}
.sched-stats-help-modal__title {
    font-size: 15px;
    font-weight: 600;
    margin: 0;
    color: var(--color-text);
}
.sched-stats-help-modal__close {
    background: transparent;
    border: 0;
    font-size: 24px;
    line-height: 1;
    cursor: pointer;
    padding: 0 4px;
    color: var(--color-text-muted);
    border-radius: 4px;
}
.sched-stats-help-modal__close:hover,
.sched-stats-help-modal__close:focus-visible {
    color: var(--color-text);
    background: rgba(0, 0, 0, 0.05);
    outline: none;
}
.sched-stats-help-modal__body {
    padding: 16px 20px 20px;
    overflow-y: auto;
}

/* Content blocks — one per metric. Light hairline between sections,
   compact typography so the reader can scan + close quickly. */
.sched-stats-help-section {
    padding: 14px 0;
    border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.sched-stats-help-section:first-child { padding-top: 0; }
.sched-stats-help-section:last-child  { border-bottom: 0; padding-bottom: 0; }
.sched-stats-help-section h3 {
    font-size: 14px;
    font-weight: 600;
    margin: 0 0 6px 0;
    color: var(--color-text);
}
.sched-stats-help-section p {
    font-size: 13px;
    line-height: 1.5;
    color: var(--color-text);
    margin: 4px 0;
}
.sched-stats-help-example-label {
    font-size: 11px;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin: 8px 0 4px;
}
.sched-stats-help-example {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px;
    line-height: 1.55;
    background: rgba(0, 0, 0, 0.03);
    border-radius: 4px;
    padding: 8px 10px;
    margin: 0 0 8px 0;
    color: var(--color-text);
    white-space: pre-wrap;
    word-break: break-word;
}
.sched-stats-help-subhead {
    font-size: 12px;
    color: var(--color-text-muted);
    margin: 10px 0 4px;
}
/* Group heading inside the modal body — sits above "How the numbers
   are calculated" and any future grouped sections. Visually distinct
   from per-metric h3 headings without competing with the modal title. */
.sched-stats-help-group-heading {
    font-size: 11px;
    font-weight: 700;
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin: 0 0 6px;
    padding-bottom: 6px;
    border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
.sched-stats-help-group-heading:not(:first-child) {
    margin-top: 18px;
}
/* Business formula line — the calculation in monospace so it reads as
   a formula, framed lightly so it doesn't get lost between heading
   and example block. */
.sched-stats-help-formula {
    margin: 6px 0 8px;
    padding: 6px 10px;
    background: rgba(0, 0, 0, 0.03);
    border-radius: 4px;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--color-text);
    font-weight: 500;
}
/* Compact definition list used by the Arrival + Hotspots help sections —
   short bullets, no examples / meaning blocks. The bold label is set
   inline in the <li> markup. */
.sched-stats-help-defs {
    margin: 6px 0 0;
    padding-left: 18px;
    font-size: 13px;
    line-height: 1.55;
    color: var(--color-text);
}
.sched-stats-help-defs li {
    margin: 3px 0;
}
.sched-stats-help-defs strong {
    font-weight: 600;
    color: var(--color-text);
}
.sched-stats-help-meaning,
.sched-stats-help-useful,
.sched-stats-help-note {
    font-size: 13px;
    line-height: 1.5;
    margin: 4px 0;
}
.sched-stats-help-meaning strong,
.sched-stats-help-useful strong,
.sched-stats-help-note strong {
    color: var(--color-text-muted);
    font-weight: 600;
}
.sched-stats-help-note {
    color: var(--color-text-muted);
}

/* Worked-example footer is part of the same section list but gets a
   subtle background tint so it reads as "here's what this looks like
   in practice", not just another metric definition. */
.sched-stats-help-section--example {
    background: rgba(0, 0, 0, 0.02);
    padding: 14px 14px;
    border-radius: 6px;
    border-bottom: 0;
    margin-top: 12px;
}

/* Mobile: trim modal padding, drop top offset so the panel uses more
   of the viewport on phones. */
@media (max-width: 640px) {
    .sched-stats-help-modal {
        padding: 2vh 8px;
    }
    .sched-stats-help-modal__panel {
        max-height: 96vh;
    }
    .sched-stats-help-modal__header { padding: 12px 14px; }
    .sched-stats-help-modal__body   { padding: 12px 14px 16px; }
}

/* ── Shift Accuracy card subsections ─────────────────────────────────
   Three subsections (All / By role / By location) live inside one card.
   Section heads are small + muted so the card reads as a single analytic
   block; reused later by Variance/Drift/Arrival/Hotspots when they land. */
.sched-stats-subsection {
    margin-top: 18px;
}
.sched-stats-subsection:first-of-type {
    margin-top: 4px;
}
.sched-stats-subsection-head {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
    margin: 0 0 8px 0;
}
.sched-stats-no-data {
    font-size: 13px;
    color: var(--color-text-muted);
    margin: 0;
}
.sched-stats-list {
    display: flex;
    flex-direction: column;
}
.sched-stats-list-row {
    padding: 10px 0;
    border-bottom: 1px solid rgba(0, 0, 0, 0.04);
}
.sched-stats-list-row:first-child { padding-top: 4px; }
.sched-stats-list-row:last-child  { padding-bottom: 4px; border-bottom: 0; }
.sched-stats-list-name {
    font-weight: 600;
    font-size: 14px;
    color: var(--color-text);
    margin-bottom: 4px;
}

/* ── Drill-down accordion ────────────────────────────────────────────
   Information-architecture change: every analytics card leads with a
   combined period summary (always visible) and hides the detailed
   breakdowns (By day / By interval / By role / By location) behind an
   expandable panel — collapsed by default on EVERY view (day / week /
   month / custom) so each card opens as one calm summary and details are
   revealed on demand.

   Not native <details>: a button-driven accordion matching the app's
   existing expand idiom (.sched-row-expand) — aria-expanded on a real
   <button>, caret rotates via the [aria-expanded="true"] selector, JS
   toggles the panel's `hidden`. The panel body reuses the same
   metric/list rendering; only the collapsible chrome is new. Full-width
   toggle = large touch target on mobile. */
.sched-stats-drill {
    margin-top: 14px;
    border-top: 1px solid rgba(0, 0, 0, 0.05);
    padding-top: 6px;
}
.sched-stats-drill-toggle {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    appearance: none;
    -webkit-appearance: none;
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    padding: 8px 6px;
    cursor: pointer;
    text-align: left;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
    transition: background 0.12s, color 0.12s;
}
.sched-stats-drill-toggle:hover {
    background: var(--color-bg);
    color: var(--color-text);
}
.sched-stats-drill-toggle:focus-visible {
    outline: 2px solid var(--color-border-strong);
    outline-offset: -2px;
}
.sched-stats-drill-caret {
    flex: 0 0 auto;
    width: 6px;
    height: 6px;
    margin-left: 2px;
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: rotate(-45deg);      /* points right when collapsed */
    transition: transform 0.15s ease;
}
.sched-stats-drill-toggle[aria-expanded="true"] .sched-stats-drill-caret {
    transform: rotate(45deg);       /* points down when open */
}
.sched-stats-drill-label {
    flex: 1 1 auto;
    min-width: 0;
}
.sched-stats-drill-count {
    font-weight: 500;
    color: var(--color-text-muted);
    letter-spacing: 0;
    text-transform: none;
}
.sched-stats-drill-body {
    margin-top: 6px;
    padding: 0 6px;
}
.sched-stats-drill-body[hidden] { display: none; }

/* Hotspot shift drill-down — the unfilled shifts behind a role or
   location row, listed inside a nested .sched-stats-drill. One row per
   shift: title link, date + time span, filled/capacity, missing badge.

   Wraps rather than truncates: the row carries four independent facts,
   so on a narrow card the operator gets a second line instead of
   losing the count that made the row worth opening.

   .sched-stats-shiftdrill must stay AFTER .sched-stats-drill — same
   specificity, and it deliberately overrides that rule's margin-top.
   The element carries both classes; reordering silently reinstates the
   14px gap meant for a top-level drill. */
.sched-stats-shiftdrill { margin-top: var(--space-xs); }
.sched-stats-shiftlist { list-style: none; margin: 0; padding: 0; }
.sched-stats-shiftlist-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-sm);
    padding: var(--space-xs) 0;
    border-bottom: 1px solid var(--color-border);
}
.sched-stats-shiftlist-row:last-child { border-bottom: none; }

/* ── Card subtitle ──────────────────────────────────────────────────
   Optional secondary line under `.card-title`. Phase 1.1: used on the
   Shift Accuracy card ("Operational quality"); kept generic so future
   analytics cards (Variance / Drift / Arrival / Hotspots) drop into the
   same hierarchy without per-card styling. */
.card-subtitle {
    font-size: 12px;
    color: var(--color-text-muted);
    margin-top: -8px;       /* pull tight under .card-title which has margin-bottom: var(--space-md) */
    margin-bottom: 16px;
    letter-spacing: 0.01em;
}

/* ── Mobile + tablet rework ──────────────────────────────────────────
   Below 1024px (mobile, tablet, narrow laptops) the page rehierarchises
   so it reads as mobile-native:
     1. Period nav  (primary — main operational browsing)
     2. Date range  (secondary)
     3. Filters     (tertiary — present, calmer)
   Stats per location stops being a compressed desktop table and renders
   as label/value pairs stacked under each location heading. No heavy
   cards or backgrounds — minimal dividers do the grouping.
   Threshold bumped from 640 → 1023: the tablet range (641-1023) doesn't
   have enough horizontal room for the desktop three-column grid without
   crushing the filter pills, so it shares the mobile vertical layout.
   ─────────────────────────────────────────────────────────────────── */
@media (max-width: 1023px) {
    /* Vertical stack of the three control groups, with explicit order so
       the period nav lands first regardless of source order. */
    .sched-stats-row {
        flex-direction: column;
        align-items: stretch;
        gap: 14px;
    }
    .sched-stats-group {
        width: 100%;
    }
    .sched-stats-group-nav       { order: 1; margin-left: 0; justify-content: center; }
    .sched-stats-group-daterange { order: 2; }
    .sched-stats-group-filters   { order: 3; }
    /* When the page has an in-grid mobile variant of the export pair
       (Statistics), hide the trailing-slot copy so we don't show the
       actions twice. Other pages (crew-list) keep the trailing slot —
       it's the only place those actions render — and the mobile
       layout below makes it a full-width equal-share row. */
    body:has(.sched-stats-export-mobile) .sched-tabs-trailing { display: none; }
    .sched-stats-export-mobile   { display: grid; }
    /* Trailing slot on mobile: flatten margin-left:auto, take full row
       width, and lay the export links out as equal-width cells. The
       crew-list ships four (Print, Email, PDF, CSV); flex: 1 1 0
       divides the row equally however many there are. Add a touch of
       top margin so the export row reads as a separate band from the
       pill nav above it instead of butting against it. */
    .sched-tabs-trailing {
        margin-left: 0;
        margin-top: 12px;
        width: 100%;
        display: flex;
        gap: 8px;
    }
    .sched-tabs-trailing > .sched-stats-export-link {
        flex: 1 1 0;
        justify-content: center;
        padding: 0 8px;
        text-align: center;
    }

    /* Period nav: chip group centered, arrows on either side */
    .sched-stats-presets {
        flex: 1 1 auto;
        justify-content: center;
    }
    .sched-stats-preset {
        flex: 1 1 0;
        justify-content: center;
        padding: 0 14px;
    }

    /* Date range: stack from/to vertically, drop the arrow.
       Labels stay inline with the input on each row but constrained to a
       fixed gutter so the inputs align with each other. */
    .sched-stats-group-daterange {
        flex-direction: column;
        align-items: stretch;
        gap: 8px;
    }
    .sched-stats-group-daterange .sched-stats-arrow { display: none; }
    .sched-stats-daterange {
        display: flex;
        align-items: center;
        gap: 10px;
    }
    .sched-stats-daterange-label {
        flex: 0 0 36px;
    }
    .sched-stats-daterange .hrc-toolbar-control {
        flex: 1 1 auto;
        min-width: 0;
    }

    /* Crew-list date-nav exception: the daterange wraps a single date
       input with prev/next day arrows. On mobile that block stays a
       horizontal row — arrows on the flanks, input grows to fill —
       overriding the column-stack rule above which is meant for
       Statistics's from/to pair. */
    .sched-stats-group-daterange.cl-date-nav {
        flex-direction: row;
        align-items: center;
        gap: 6px;
    }
    .sched-stats-group-daterange.cl-date-nav .hrc-toolbar-control {
        flex: 1 1 auto;
        min-width: 0;
    }

    /* Filters: two-up grid keeps the row calm without pushing each select
       to its own line. Role inherits whatever lane is free. */
    .sched-stats-group-filters {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 8px;
    }
    .sched-stats-group-filters .hrc-toolbar-control {
        width: 100%;
        min-width: 0;
    }

    /* Mobile export pair: lives inside the 4th cell of the 2×2 filter
       grid (next to "All roles"). Splits the cell into two equal lanes
       for PDF and CSV at the same height as the filter selects. */
    .sched-stats-export-mobile {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 8px;
        min-width: 0;
    }
    .sched-stats-export-mobile .hrc-toolbar-control {
        width: 100%;
        padding-left: 8px;
        padding-right: 8px;
        font-size: 12px;
    }

    /* Schedule listing: "Add shift" CTA on mobile.
       Hide the desktop trailing-slot variant; render the in-toolbar copy
       as a primary action. On Schedule it shares the date-picker row
       (compact, right-aligned, auto width) — the per-form override
       below promotes it to a row partner of the date input. On other
       pages that don't override placement, it stays full-width within
       its container. */
    .sched-tabs-add-shift { display: none; }
    .sched-stats-add-mobile {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 100%;
        height: 36px;
        padding: 0 14px;
        font-size: 13px;
        font-weight: 500;
        line-height: 1;
        color: var(--color-white);
        background: var(--color-text);
        border: 1px solid var(--color-text);
        border-radius: 8px;
        text-decoration: none;
    }
    .sched-stats-add-mobile:hover {
        color: var(--color-white);
        opacity: 0.92;
    }

    /* Schedule toolbar: keep daterange + Add shift on a single mobile
       row. The date picker grows to fill remaining width, Add stays
       compact and right-aligned. Filter grid below is unchanged
       (still 2×2). Scoped to #sched-filter-form so other pages that
       use .sched-stats-group-daterange (Statistics from/to pair,
       crew-list date nav) keep their current behaviour. */
    #sched-filter-form .sched-stats-group-daterange {
        flex-direction: row;
        align-items: center;
        gap: 8px;
    }
    #sched-filter-form .sched-stats-group-daterange .hrc-toolbar-control {
        flex: 1 1 auto;
        min-width: 0;
    }
    #sched-filter-form .sched-stats-group-daterange .sched-stats-add-mobile {
        width: auto;
        flex: 0 0 auto;
        margin-left: auto;
    }

    /* Tighter rhythm between sections on mobile so the page doesn't feel
       tall once the chart + per-location cards are below. */
    .sched-stats-controls    { margin-bottom: var(--space-md); }
    .sched-stats-chart-wrap  { margin-bottom: var(--space-md); }
    .sched-stats-table-card  { margin-bottom: var(--space-md); }

    /* Row-block tweaks for mobile. The base layout already stacks via the
       auto-fit metrics grid; this just tightens vertical rhythm so 30
       day-rows don't read as a wall. */
    .sched-stats-row-block { padding: 10px 0; }
    .sched-stats-pairs     { gap: 2px 18px; }
}

/* Legend — small, muted, tucked into the top-right. Line indicators. */
.hrc-graph-legend {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
    font-size: 11px;
    color: var(--color-text-muted);
    margin-bottom: 0;
    line-height: 1.4;
    letter-spacing: 0.01em;
}
.hrc-graph-legend-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.hrc-graph-dash {
    display: inline-block;
    width: 14px;
    height: 0;
    border-top: 2px solid currentColor;
    flex-shrink: 0;
}

/* Per-series legend tint — driven by class, no inline styles */
.hrcg-legend-clocked .hrc-graph-dash { color: var(--color-graph-green); }
.hrcg-legend-filled  .hrc-graph-dash { color: rgba(17, 24, 39, 0.55); }
.hrcg-legend-planned .hrc-graph-dash { color: rgba(0, 0, 0, 0.22); border-top-width: 1.4px; }
/* Understaffed baseline strip. Solid, like the clocked dash: the strip is
   painted at fill-opacity 0.22 so it sits behind the series, but a 14x2px
   swatch at that alpha is invisible. The dash names the colour; the mark
   itself stays faint. */
.hrcg-legend-understaffed .hrc-graph-dash { color: var(--color-danger); }

/* Body = y-axis | plot column (with HTML overlays inside) */
.hrc-graph-body {
    display: flex;
    gap: 8px;
    align-items: stretch;
}
.hrcg-y-axis {
    width: 22px;
    flex-shrink: 0;
    position: relative;
    font-size: 10px;
    color: var(--color-text-muted);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}
.hrcg-y-axis span {
    position: absolute;
    right: 0;
    transform: translateY(-50%);
    /* top set inline as % so labels align with gridline y-positions */
}
.hrcg-plot {
    position: relative;
    flex: 1;
    min-width: 0;
}

/* SVG canvas — fixed height, fluid width. Aspect varies with card width;
   the dot uses stroke-only circles + non-scaling-stroke so it stays round
   regardless of any viewBox stretching. */
.hrc-graph-canvas {
    width: 100%;
    height: 220px;
    display: block;
    overflow: visible;
}

/* NOW label — HTML overlay anchored above the dashed marker, immune to SVG
   stretching. Top is set inline as a % of the SVG canvas height so the label
   sits in the reserved top gutter (controlled by $mt in the renderer);
   translateY(-100%) anchors the BOTTOM of the label at that y, leaving the
   plot area cleanly below it on both desktop and mobile. */
.hrcg-now-label {
    position: absolute;
    /* top set inline as % */
    transform: translate(-50%, -100%);
    font-size: 9px;
    font-weight: 600;
    letter-spacing: 0.06em;
    color: rgba(0, 0, 0, 0.65);
    text-transform: uppercase;
    pointer-events: none;
    background: var(--color-white);
    padding: 0 4px;
    line-height: 1.2;
}

/* X-axis — absolute-positioned HTML labels aligned to plot bucket positions */
.hrcg-x-axis {
    position: relative;
    height: 16px;
    margin-top: 6px;
    margin-left: 30px; /* y-axis width + gap = 22 + 8 */
    font-size: 10px;
    color: var(--color-text-muted);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-variant-numeric: tabular-nums;
}
.hrcg-x-axis span {
    position: absolute;
    top: 0;
    transform: translateX(-50%);
    white-space: nowrap;
    line-height: 1;
}
.hrcg-x-axis span:first-child { transform: translateX(0); }
.hrcg-x-axis span:last-child  { transform: translateX(-100%); }

/* Adaptive-label visibility for axis renders that tag labels with
   data-priority (today: Schedule → Statistics page).
     priority="1" — anchor, always visible (first, last, even subset)
     priority="2" — hidden on mobile (≤640px)
     priority="3" — desktop only (hidden on tablet + mobile)
   Bucket-stride callers (live dashboard) don't set the attribute so
   none of these selectors match and behavior there is unchanged. */
@media (max-width: 1023px) {
    .hrcg-x-axis span[data-priority="3"] { display: none; }
}
@media (max-width: 640px) {
    .hrcg-x-axis span[data-priority="2"] { display: none; }
}

/* Horizontal dotted gridlines — calm operational reference */
.hrcg-grid {
    stroke: rgba(0, 0, 0, 0.08);
    stroke-width: 1;
    stroke-dasharray: 2 3;
    vector-effect: non-scaling-stroke;
}
/* Baseline gridline — slightly stronger anchor at y=0 */
.hrcg-grid-baseline {
    stroke: rgba(0, 0, 0, 0.14);
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
    shape-rendering: crispEdges;
}

/* Vertical day-boundary gridlines — Schedule statistics page,
   daily-anchor modes. Slightly softer than the horizontal dotted
   gridlines: solid hairline at very low opacity so the eye reads
   them as cell dividers, not as data. */
.hrcg-grid-vertical {
    stroke: rgba(0, 0, 0, 0.07);
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
    shape-rendering: crispEdges;
}

/* Planned reference — secondary, sits quietly behind filled */
.hrcg-line-planned {
    fill: none;
    stroke: rgba(0, 0, 0, 0.16);
    stroke-width: 1;
    stroke-linejoin: round;
    stroke-linecap: round;
    shape-rendering: geometricPrecision;
    vector-effect: non-scaling-stroke;
}

/* Filled (neutral staffing reference) — thin trace, sits between planned
   and clocked-in. Was the primary in earlier revisions; demoted so the
   clocked-in series can carry the visual weight of the live attendance. */
.hrcg-line-filled {
    fill: none;
    stroke: rgba(17, 24, 39, 0.55);
    stroke-width: 1.4;
    stroke-linejoin: round;
    stroke-linecap: round;
    shape-rendering: geometricPrecision;
    vector-effect: non-scaling-stroke;
}

/* Clocked-in (primary / active / live) — vertical gradient area + confident
   green line. The chart's operational headline: how many people are
   actually working right now (or were, for historical ranges). */
.hrcg-area-clocked {
    /* fill set inline via url(#hrcg-fill-clocked-…) for top-down depth */
}
.hrcg-line-clocked {
    fill: none;
    stroke: var(--color-graph-green);
    stroke-width: 1.8;
    stroke-linejoin: round;
    stroke-linecap: round;
    shape-rendering: geometricPrecision;
    vector-effect: non-scaling-stroke;
}

/* Danger baseline strip — narrow indicator, per-bucket only.
   SHARED COLOUR: the PDF export paints the same strip and cannot read CSS
   custom properties, so --color-danger is duplicated as the literal #c0392b
   in modules/schedule/statistics_export.php (rect fill + .sw-understaffed
   swatch) and in scripts/fixtures/capture_schedule_statistics_chart.php,
   which matches strips by fill. Move the token → move all three.
   The alpha is NOT shared: 0.22 here, 0.6 in print. Intentional — a 2px
   strip at 0.22 reads on a backlit display and vanishes on paper. */
.hrcg-danger-strip {
    fill: var(--color-danger);
    fill-opacity: 0.22;
    pointer-events: none;
}

/* Now marker — clear at-a-glance current-time reference, still subtle */
.hrc-graph-now-marker {
    stroke: rgba(0, 0, 0, 0.50);
    stroke-width: 1.2;
    stroke-dasharray: 3 4;
    vector-effect: non-scaling-stroke;
}

/* Hit areas — invisible but capture hover for tooltip */
.hrcg-hit {
    fill: transparent;
    cursor: default;
}
.hrcg-hit:hover {
    fill: rgba(29, 158, 117, 0.06);
}

/* Floating tooltip — informational, not modal */
.hrc-graph-tooltip {
    position: absolute;
    pointer-events: none;
    background: var(--color-white);
    border: 0.5px solid rgba(0, 0, 0, 0.10);
    border-radius: 5px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
    padding: 4px 7px;
    font-size: 9.5px;
    font-family: 'Space Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    line-height: 1.35;
    color: var(--color-text);
    min-width: 80px;
    max-width: 150px;
    z-index: 100;
    opacity: 0;
    transform: translate(-50%, -100%) translateY(-5px);
    transition: opacity 0.1s ease;
}
.hrc-graph-tooltip.is-visible {
    opacity: 0.96;
}
.hrc-graph-tooltip-title {
    font-weight: 700;
    margin-bottom: 2px;
    font-size: 9px;
    letter-spacing: 0.02em;
    color: var(--color-text-muted);
}
.hrc-graph-tooltip-row {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    font-variant-numeric: tabular-nums;
}
.hrc-graph-tooltip-row span:first-child {
    color: var(--color-text-muted);
    font-weight: 400;
}
.hrc-graph-tooltip-row span:last-child {
    color: var(--color-text);
    font-weight: 700;
}

/* Viewport-variant swap. Desktop card visible by default, mobile card hidden.
   At the same 640px breakpoint the rest of the dashboard uses for mobile,
   swap which variant is visible. Two cards live in the same live-region so
   the 25s partial-poll refresh updates both atomically. */
.hrcg-variant-mobile  { display: none; }
@media (max-width: 640px) {
    .hrcg-variant-desktop { display: none; }
    .hrcg-variant-mobile  { display: block; }
}

/* Mobile — keep chart legible at narrow widths */
@media (max-width: 640px) {
    .hrc-graph-card {
        padding: 8px 12px 6px;
    }
    .hrc-graph-canvas {
        height: 160px;
    }
    .hrcg-y-axis,
    .hrcg-x-axis {
        font-size: 9px;
    }
    .hrcg-x-axis {
        margin-left: 26px;
    }
    .hrcg-y-axis {
        width: 18px;
    }
}

/* Statens vegvesen registry-locked fields (public auto-fill module).
   Visual lock only — the server is authoritative on the stored value. */
.svv-locked {
    background: var(--color-surface-alt, #f3f3f3) !important;
    cursor: not-allowed;
}

/* ============================================================
   Phase selector cards (canonical shared component)
   ------------------------------------------------------------
   Single source of truth for every surface where a manager
   curates project phases as checkbox chip-cards:
     - Supplier accreditation configuration
     - Accreditation intake-link configuration
     - Accreditation record add/edit form
     - Vehicle access phases  (standalone link + supplier config)
     - Vehicle parking phases (standalone link + supplier config)

   Replaces the per-module .acc-phase-* and .vpl-acc-phase-*
   phase grids. Card visuals are intentionally identical to the
   old per-module rules (border, radius, padding, hover, checked,
   locked) so selected/unselected states are unchanged. The only
   behavioural change is a DETERMINISTIC column count — 5 per row
   on desktop, 3 on tablet, 2 on mobile — instead of the old
   auto-fit grid whose column count drifted with container width
   and item count (which is why the same markup rendered 5-per-row
   on the wide supplier panel but 4-per-row in the narrower link
   editor).

   NOT for the vehicle place pickers (locations / zones / gates /
   parking objects) — those keep their own auto-fit grid so a
   short list still stretches to fill the row.

   Selected-state rule (locked across the app):
     hover  : --color-border-strong
     checked: border --color-accent, bg --color-accent-subtle
     never green / never gradient / always neutral
   ============================================================ */
.phase-selector-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-sm);
    margin-top: var(--space-xs);
}
@media (min-width: 700px) {
    .phase-selector-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (min-width: 1100px) {
    .phase-selector-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}
.phase-selector-card {
    display: flex; align-items: flex-start; gap: var(--space-sm);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    padding: var(--space-md);
    background: #fff;
    cursor: pointer;
    transition: border-color 0.15s, background 0.15s;
    margin: 0;
}
.phase-selector-card:hover { border-color: var(--color-border-strong, #b8b8b5); }
.phase-selector-card:has(input:checked) {
    border-color: var(--color-accent);
    background: var(--color-accent-subtle);
}
.phase-selector-card-locked { cursor: not-allowed; opacity: 0.85; }
.phase-selector-card-locked input { cursor: not-allowed; }
.phase-selector-card input[type="checkbox"] { margin-top: 2px; flex-shrink: 0; }
.phase-selector-card-body { flex: 1; min-width: 0; }
.phase-selector-card-title { font-weight: 600; line-height: 1.3; }
.phase-selector-card-meta {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    margin-top: 2px;
}


/* ── Messaging module ─────────────────────────────────────────
   Shared presentation for the list / detail / compose so the three
   surfaces read as one feature. Restrained: no new shadows, gradients
   or widgets. Compose reuses native form primitives (.form-group /
   .form-hint / .form-actions); the Person/Group selector reuses the
   global .phase-selector-card. */

/* Recipient identity — small icon + name + muted qualifier (no avatars) */
.msg-rec { display: inline-flex; align-items: flex-start; gap: 8px; min-width: 0; }
.msg-rec-ic { flex-shrink: 0; margin-top: 2px; color: var(--color-text-muted); }
.msg-rec-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
.msg-rec-name { font-weight: 600; color: var(--color-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-rec-qual { font-size: var(--font-size-sm); color: var(--color-text-muted); }

/* Delivery outcome — discrete signal (status + count), never a meter */
.msg-outcome { display: inline-flex; align-items: center; gap: 6px; }
.msg-outcome-count { font-size: var(--font-size-sm); color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
.msg-outcome-fail { font-size: var(--font-size-sm); color: var(--color-danger); font-weight: 600; }

/* List row: subject is the scan anchor; date/sender stay quiet */
.msg-list-subject { color: var(--color-text); }
.msg-list-quiet { color: var(--color-text-muted); white-space: nowrap; }

/* Detail — message is native content (case-detail-grid); the full email
   lives in the shared "View sent email" modal. These remaining classes
   style the email iframe + the modal summary (recipient + subject). */
.msg-email-wrap { border: 1px solid var(--color-border); border-radius: var(--radius-md); overflow: hidden; background: #f5f5f4; }
.msg-email-frame { display: block; width: 100%; border: 0; min-height: 320px; background: #f5f5f4; }
.msg-facts { display: flex; flex-direction: column; gap: var(--space-md); font-size: 0.92rem; }
.msg-fact-label { font-size: var(--font-size-sm); color: var(--color-text-muted); margin-bottom: 3px; }
.msg-fact-value { color: var(--color-text); }

/* Detail record — one primary .card frames the whole record; sections are
   divided by hairlines (no nested boxes). The message body is the
   centerpiece (reading measure); the metadata band is a compact supporting
   grid. No email chrome here — the exact envelope lives in the modal. */
.msg-record-head { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--space-md); flex-wrap: wrap; }
.msg-record-title { display: flex; align-items: center; gap: var(--space-sm); flex-wrap: wrap; min-width: 0; }
.msg-record-title h2 { margin: 0; font-size: 1.3rem; line-height: 1.3; }
.msg-record-section { margin-top: var(--space-xl); padding-top: var(--space-xl); border-top: 1px solid var(--color-border); }
.msg-meta-band { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--space-md) var(--space-lg); font-size: var(--font-size-sm); }
.msg-meta-band .msg-rec-name { font-weight: 500; }

/* Message — the centerpiece, on its own subtle white sheet (its card
   border separates it, so no divider here — avoids a line + box stack). */
.msg-message { margin-top: var(--space-xl); }
.msg-sheet { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: var(--space-md); }
.msg-sheet-body { max-width: 620px; font-size: var(--font-size-base); line-height: 1.65; color: var(--color-text); }

/* Recipients roster — failures first, success list collapsible */
.msg-roster-fail-head { color: var(--color-danger); font-weight: 600; margin: 0 0 var(--space-sm); }
.msg-roster-toggle { background: none; border: 0; padding: 0; color: var(--color-text-muted); cursor: pointer; font: inherit; text-decoration: underline; }
.msg-roster-toggle:hover { color: var(--color-text); }
summary.msg-roster-toggle { list-style: none; display: inline-block; }
summary.msg-roster-toggle::-webkit-details-marker { display: none; }

/* Empty state — first-run welcome */
.msg-empty { text-align: center; padding: var(--space-xl) var(--space-lg); max-width: 540px; margin: 0 auto; }
.msg-empty-ic { color: var(--color-text-muted); margin-bottom: var(--space-md); }
.msg-empty-title { font-size: 1.15rem; font-weight: 700; color: var(--color-text); margin: 0 0 var(--space-xs); }
.msg-empty-lead { color: var(--color-text-muted); margin: 0 0 var(--space-lg); line-height: 1.55; }
.msg-empty-points { text-align: left; display: flex; flex-direction: column; gap: var(--space-md); margin: 0 0 var(--space-lg); }
.msg-empty-point { display: flex; gap: 10px; align-items: flex-start; color: var(--color-text); line-height: 1.5; }
.msg-empty-point svg { flex-shrink: 0; margin-top: 3px; color: var(--color-text-muted); }

/* Compose Person/Group selector reuses the global option-card pattern
   (.phase-selector-card — same as Accreditation/Vehicles), constrained
   to two equal columns. Messaging-scoped: the card itself is the control,
   so the radio is visually hidden but kept accessible + keyboard-focusable.
   The global component is unchanged. */
.msg-to-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-sm); margin-top: var(--space-xs); }
.msg-to-grid .phase-selector-card { position: relative; align-items: center; justify-content: center; text-align: center; }
.msg-to-grid .phase-selector-card input {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
.msg-to-grid .phase-selector-card:has(input:focus-visible) { outline: 2px solid var(--color-text); outline-offset: 2px; }
.msg-to-grid .phase-selector-card-title { font-weight: 500; }
.msg-to-grid .phase-selector-card:has(input:checked) .phase-selector-card-title { font-weight: 600; }

/* Recipient confirmation reuses the native .form-hint; only the
   empty-resolution warning state needs a colour. */
#compose-summary.is-empty { color: var(--color-danger); }

/* Email-preview modal — slightly wider so the email shows near its
   natural width; the body scrolls (modal is capped at 90vh). The email
   surface spans the full modal width (cancels the modal-body's side
   padding) so it isn't boxed in by white space; the summary keeps its
   padding. */
.modal.modal-preview { max-width: 680px; }
.modal-preview #preview-email-wrap {
    margin-left: calc(-1 * var(--space-xl));
    margin-right: calc(-1 * var(--space-xl));
    border-left: 0;
    border-right: 0;
    border-radius: 0;
}


/* ── Production Plan (Ops) ─────────────────────────────────────
   Status row backgrounds + priority dots for the plan table.
   Row background carries STATUS (legacy sheet convention:
   ok = amber "awaiting review", completed = green). Priority is
   a compact leading dot — deliberately not a background, so it
   stays legible on top of any status colour. */
/* Period bar reuses the schedule/power .sched-stats-* system; only the
   second filter row (selects + search) needs local layout.
   Scoped on the ROW class, not on one parent: the checklist reuses this
   row inside .pp-cl-controls, and while the rule was scoped to
   .pp-plan-controls the checklist's filters sat flush against the
   period bar with a 0px gap. */
.pp-plan-filters {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
    margin-top: var(--space-xs);
}

/* Priority column: plain text labels (ruling: black, not emphasized —
   the row background already carries urgency signalling). */

/* Status backgrounds on the cells (a tr background is painted over
   by any cell hover/zebra rules; cells win consistently). */
.pp-row-ongoing   td { background: #eff6ff; }  /* subtle blue — run open */
.pp-row-ok        td { background: var(--color-warning-bg); }  /* awaiting review */
.pp-row-completed td { background: var(--color-success-bg); }  /* signed off */
.pp-table td { vertical-align: top; }
/* Title column keeps a readable floor; the wrap scrolls instead. */
.pp-table .pp-col-title { min-width: 220px; }
/* Secondary lines (comment / need): long URLs and freetext must never
   define the column's min-content width — wrap anywhere. */
.pp-cell-sub { font-size: .85rem; overflow-wrap: anywhere; }
/* Dense plan table: headers may wrap (Avdeling etc.), and the
   cell padding is tighter than the generic hrc-table — 12 columns of
   operational data need the width more than the breathing room. */
.pp-table th { white-space: normal; }
.pp-table th, .pp-table td { padding-left: 8px; padding-right: 8px; }
/* ── Supplier value: linked vs freetext ──────────────────────
   Two states that mean different things, so they must not look alike.
   Rendered identically on the plan and the supplier report by
   pp_supplier_cell_html() (and mirrored in renderRow).

   A registered supplier is a link and keeps the normal link treatment.
   A freetext value is italic and slightly softened — #4a4a4a, NOT
   --color-text-muted (#666), which is the tone this UI uses for
   secondary/disabled data. Freetext is primary content that happens to
   be unlinked, and must stay comfortably readable. */
.pp-sup-ft {
    font-style: italic;
    color: #4a4a4a;
}
/* Editable freetext advertises itself only on hover/focus — a permanent
   badge or underline on every unlinked row would be noise on a plan
   where most supplier values start out as freetext. */
.pp-sup-ft-edit { cursor: pointer; }
.pp-sup-ft-edit:hover,
.pp-sup-ft-edit:focus-visible {
    color: var(--color-text);
    text-decoration: underline dotted;
    text-underline-offset: 2px;
}
/* Explanatory line at the top of the freetext action popover. */
.pp-pop-note {
    font-size: .75rem;
    font-style: italic;
    color: var(--color-text-muted);
    margin-bottom: 8px;
}
/* Sections inside that popover need separating from the control above. */
.pp-pop .pp-pop-label + .pp-msel-inline { margin-top: 2px; }
.pp-pop-actions + .pp-pop-label { margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(0, 0, 0, 0.10); }

/* Verbose freetext cells (supplier notes) are bounded at 4 lines —
   full text via title tooltip. Keeps data-driven rows from dominating
   the plan's vertical rhythm. */
.pp-cell-clamp {
    display: -webkit-box;
    -webkit-line-clamp: 4;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
/* Inline editor popover (slice 4) */

/* ── Priority + Department as sub-lines (2026-08-09) ──────────
   Both lost their columns; they now read under What and under
   Responsible. `.pp-subedit` is the nested click target the row
   dispatcher resolves via [data-subf], so it needs its own hover
   affordance — the parent td.pp-edit:hover outline covers the whole
   cell and cannot show which sub-line is about to be edited. */
.pp-subedit { cursor: pointer; border-radius: 3px; }
.pp-subedit:hover { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.18); }

/* An empty editable department still needs a hit area, or a row with no
   department could never be given one. */
.pp-dept-sub:empty { min-height: 1em; }

/* Priority had NO styling before this move: it sat in its own column,
   where position alone said what it was. As a sub-line among comment
   and need text it needs to carry its own meaning — `normal` stays
   quiet (it is the overwhelming majority and must not shout), while
   the exceptions have to survive being read at a glance on a 185-row
   plan. Colours are the existing semantic tokens, not new ones. */
.pp-prio-sub { margin-top: 2px; }
.pp-prio-text { font-size: 0.85em; }
.pp-prio-text-normal   { color: var(--color-text-muted); }
.pp-prio-text-low      { color: var(--color-text-muted); }
.pp-prio-text-high     { color: var(--color-warning-text); font-weight: 600; }
.pp-prio-text-critical { color: var(--color-danger-text); font-weight: 700; }

/* New-row form: the two selects that lost their columns sit under the
   title input and the responsible chipfield respectively. */
.pp-nf-sub { margin-top: 4px; }
/* A new-row select whose EMPTY option is selected is showing its
   placeholder, not a value — same muted tone as the picker
   placeholders (.pp-msel-label:empty::before). Options re-assert the
   normal colour so the open dropdown is not washed out. */
.pp-new-row select:has(option[value='']:checked) { color: var(--color-text-light); }
.pp-new-row select option { color: var(--color-text); }
.pp-nf-sub select { width: 100%; }

/* Load-more strip. Full-width row at the foot of the table, same shape
   as the batch-add footer so the table has one visual language for
   'controls that belong to the whole list'. */
.pp-more-row td { text-align: center; padding: var(--space-md); }
.pp-more-count { margin-left: var(--space-sm); font-size: var(--font-size-sm); }

td.pp-edit { cursor: pointer; }
td.pp-edit:hover { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.18); }
.pp-pop {
    position: fixed; z-index: 1000;
    min-width: 250px; max-width: 340px;
    background: #fff; border: 1px solid rgba(0, 0, 0, 0.15);
    border-radius: 6px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
    padding: 10px; text-align: left; white-space: normal;
    font-weight: 400;
}
.pp-pop-field { margin-bottom: 8px; }
.pp-pop-label { display: block; font-size: .75rem; color: rgba(0, 0, 0, .55); margin-bottom: 2px; }
.pp-pop .hrc-toolbar-control, .pp-pop .form-control { width: 100%; }
.pp-pop-actions { display: flex; justify-content: flex-end; gap: 6px; margin-top: 8px; }
/* Start task inside the status popover: an ACTION, not one of the
   statuses in the select under it (ruling Q1) — full width and set off
   by a rule so the two never read as one control. */
.pp-pop-start { display: block; width: 100%; margin-bottom: 10px; }
.pp-pop-start + * { border-top: 1px solid rgba(0, 0, 0, 0.10); padding-top: 10px; }
/* Status targets as buttons rather than a select: the pick IS the
   commit, so each row is a full-width tap target and there is no Save
   under them. 36px is the operational minimum for a gloved tap on the
   iPads this runs on in the field. */
.pp-pop-choices { display: flex; flex-direction: column; gap: 6px; }
.pp-pop-choice { display: block; width: 100%; min-height: 36px; text-align: center; }
/* msel body inside an edit popover (no own button/foot — the popover
   provides Save/Cancel) */
.pp-msel-inline { min-width: 230px; }
.pp-msel-inline .pp-msel-list { max-height: 200px; }
/* Match-mode row inside a filter panel (supplier any/all). Sits between
   the search input and the option list. Uses the platform .toggle-pill —
   the documented control for MODE selection rather than on/off — so it
   inherits the segmented look, the sliding highlight and the focus ring
   without a bespoke widget. */
.pp-msel-mode {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin: 8px 0 2px;
}
.pp-msel-mode-label {
    font-size: .75rem;
    color: var(--color-text-muted);
    white-space: nowrap;
}
/* The panel is narrow — let the two segments share the leftover width
   instead of overflowing it. */
.pp-msel-mode .toggle-pill { min-width: 0; }
.pp-msel-mode .toggle-pill-off,
.pp-msel-mode .toggle-pill-on { padding: 3px 8px; letter-spacing: .02em; }

/* "Add «text»" create-on-type row (labels, supplier freetext) */
.pp-msel-add {
    padding: 6px 4px; border-radius: 6px; cursor: pointer;
    font-size: .9rem; color: var(--color-text-muted);
}
.pp-msel-add:hover { background: rgba(0, 0, 0, 0.04); color: var(--color-text); }
.pp-conflict { margin-top: 8px; padding: 8px; background: var(--color-warning-bg); border: 1px solid var(--color-warning-border); border-radius: 4px; }
.pp-conflict-msg { font-size: .8rem; margin-bottom: 4px; }
.pp-conflict-val { font-size: .85rem; font-weight: 600; margin-bottom: 6px; overflow-wrap: anywhere; }
.pp-col-del { width: 1%; }
.pp-del-btn { border: 0; background: none; cursor: pointer; color: rgba(0, 0, 0, .4); font-size: 1rem; line-height: 1; padding: 2px 6px; }
.pp-del-btn:hover { color: var(--color-danger); }
.pp-new-row td { background: rgba(0, 0, 0, 0.02); }
/* Draft-row status cell (the save action lives in the batch footer;
   the row-level × sits in the trailing narrow column beside where real
   rows carry their delete button). */
.pp-new-actions { white-space: nowrap; }
.pp-nf-rowactions { white-space: nowrap; text-align: right; }
.pp-nf-rowactions .pp-del-btn { padding: 2px 4px; }
.pp-nf-rowactions .pp-nf-add { font-size: 1.05rem; font-weight: 600; color: rgba(0, 0, 0, .55); }
.pp-nf-rowactions .pp-nf-add:hover { color: var(--color-text); }
/* Batch footer — the save action for one draft and for many; shows and
   hides together with the draft rows. */
.pp-new-foot td {
    background: rgba(0, 0, 0, 0.02);
    text-align: right;
    padding-top: 8px;
    padding-bottom: 8px;
}
.pp-new-foot-count {
    margin-right: 10px;
    font-size: .85rem;
    color: var(--color-text-muted);
}
/* Live updates: background changes animate; polled row changes pulse. */
.pp-table td { transition: background-color .5s ease; }
.pp-row-overdue td { background: var(--color-danger-ghost-bg); }  /* start passed, not started */
/* Unconfirmed: nothing has been agreed for this row yet. Red, same as
   overdue — both mean "this needs someone's attention before the day
   runs". It is a status like any other now (migration 392), so it is
   selected through $pp_row_class and never stacks with a lifecycle
   colour. `followup` deliberately reuses .pp-row-ok rather than
   defining a twin rule — one colour, one declaration. */
.pp-row-unconfirmed td { background: var(--color-danger-ghost-bg); }
@keyframes ppRowFlash {
    from { box-shadow: inset 0 0 0 999px rgba(230, 126, 34, 0.16); }
    to   { box-shadow: inset 0 0 0 999px rgba(230, 126, 34, 0); }
}
tr.pp-flash td { animation: ppRowFlash 1.2s ease-out; }
/* New-row inputs: compact, one per column. */
.pp-new-row .hrc-toolbar-control { width: 100%; min-width: 56px; padding: 4px 6px; }
/* Per-control width floors. The generic 56px floor above is too low for
   the controls whose content has a known minimum, and .pp-table is
   layout:auto — so on a plan with many content-heavy DATA rows those
   rows win the width and the draft's inputs get squeezed below
   legibility (measured with 34 rows at 1692px: date 73px, type 75px).
   Symptoms differ per control and neither overflows, so neither is
   visible to a scrollWidth check: a native <select> silently truncates
   its own label ("Oppgav"), and the date field's text slides under the
   calendar button, which lives in its 30px padding-right gutter.
   Values are each control's UNCONTENDED width, so this restores what
   an empty plan already showed. Pressure moves into the table-wrap's
   own horizontal scroll, same as every other wide column here. */
.pp-new-row .pp-nf-status { min-width: 116px; }
.pp-new-row .pp-nf-type,
.pp-new-row .pp-nf-priority { min-width: 96px; }
/* The date's floor goes on BOTH the visible text field and the .hrc-di
   wrapper date-input.js puts around it — the wrapper is inline-flex and
   would otherwise shrink-to-fit the squeezed cell and clip the field
   back. .pp-nf-date resolves to the VISIBLE field (the component copies
   the class onto it), which is the one that must fit the mask. */
.pp-new-row .hrc-di,
.pp-new-row .pp-nf-date { min-width: 106px; }
/* Chip typeahead (new-row Lokasjon / Ansvarlig / Leverandør / Etikett) */
.pp-chipfield { position: relative; min-width: 110px; }
/* Empty picker reads like the text placeholders beside it. Rendered from
   an attribute so the element's textContent stays genuinely empty — the
   selection label and the hint must never be confusable. */
.pp-msel-label:empty::before {
    content: attr(data-ph);
    color: var(--color-text-light);
}
/* New-row cell variant of the msel button + its body-attached panel */
.pp-msel-btn-cell { width: 100%; min-width: 96px; }
.pp-msel-panel-fixed { position: fixed; top: auto; left: auto; z-index: 1001; }
/* First/last column breathe against the card edges. The generic
   .card .table-wrap first/last-child rule pads by exactly the
   full-bleed offset (--space-lg), which lands text flush at the card
   edge — the plan table adds real visible spacing on top. */
.card .table-wrap table.pp-table thead th:first-child,
.card .table-wrap table.pp-table tbody td:first-child { padding-left: calc(var(--space-lg) + 12px); }
.card .table-wrap table.pp-table thead th:last-child,
.card .table-wrap table.pp-table tbody td:last-child  { padding-right: calc(var(--space-lg) + 12px); }
/* Last row breathes against the card bottom (full-bleed margin). */
.card .table-wrap table.pp-table tbody tr:last-child td { padding-bottom: calc(var(--space-lg) + 8px); }
/* …but :last-child is structural, and the load-more row does not leave
   the DOM when it retires — _editor_js.php sets `moreRow.hidden = true`
   once the final page is appended, and :last-child keeps matching a
   display:none row. The allowance above is then spent on an invisible
   row while the real last row keeps the ordinary 8px, so the card's
   overflow:hidden cuts ~15px off its second line. Measured, not
   theorised: the clip appears only in this state — with the button
   still showing, the more-row absorbs the full-bleed pull itself.
   Give the row preceding a retired load-more row the same allowance. */
.card .table-wrap table.pp-table tbody tr:has(+ tr.pp-more-row[hidden]) td {
    padding-bottom: calc(var(--space-lg) + 8px);
}
/* Inline toolbar checkbox (Hide completed). Sized to sit level with the
   pill controls beside it rather than as a bare form input. */
.pp-toolbar-check {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
    font-size: .85rem;
    color: var(--color-text);
    cursor: pointer;
    padding: 0 4px;
}
.pp-toolbar-check input[type="checkbox"] { cursor: pointer; margin: 0; }

/* Search field fills the remaining toolbar width; results fade in.
   One rule for all three views — the control itself is shared
   (_search.php / pp_search). Inside the supplier toolbar's filter group
   the ≥1024 grid rule on .sched-stats-group-filters .hrc-toolbar-control
   is more specific and takes over, which is intended. */
.pp-search { flex: 1 1 220px; }
/* Search lives on the FIRST toolbar row (plan) / the single row
   (supplier report) and spans the slack between the pills and the
   preset nav. The search's own flex basis only stretches within its
   GROUP — the group itself must be allowed to grow. Scoped: the
   checklist keeps its natural-width groups. */
.pp-plan-controls .sched-stats-group-filters,
.pp-sup-controls  .sched-stats-group-filters { flex: 1 1 auto; }
.table-wrap, .pp-cl-list { transition: opacity .25s ease; }
.table-wrap.pp-loading, .pp-cl-list.pp-loading { opacity: .45; }

/* ── Empty result — ONE treatment for all three plan views ────
   Plan table, checklist list and supplier report all render the same
   centered, muted, italic line inside their normal result container:
   one result row, a little taller. Never a separate empty-state card,
   never a view-specific message, never a per-view override.

   --pp-empty-pad is the VISIBLE breathing room above and below the
   text, defined once so the <td> variant and the checklist's <div>
   variant share a spacing rhythm despite different box models. */
.pp-empty,
.card .table-wrap table.pp-table tbody tr.pp-empty-row td {
    --pp-empty-pad: var(--space-lg);
    text-align: center;
    font-style: italic;
    font-size: .9rem;
    line-height: 1.5;
    color: var(--color-text-muted);
    padding: var(--pp-empty-pad) var(--space-sm);
}

/* The table variant must ALSO absorb the card's full-bleed pull.
   `.card .table-wrap:last-child` lifts the table by --space-lg, and the
   plan / supplier cards are `padding:0`, so that last --space-lg of the
   table hangs past the card — which clips it, because `.card` is
   `overflow:hidden`. Ordinary last rows already pay for this via
   `tr:last-child td { padding-bottom: calc(var(--space-lg) + 8px) }`;
   the shorthand above resets all four sides, so the empty cell has to
   restate it or its text renders under the card's bottom border.
   Applied to the row itself (not :last-child) because the empty row is
   the last row whenever it exists, and tying it to position would break
   the moment anything renders after it. */
.card .table-wrap table.pp-table tbody tr.pp-empty-row td {
    padding-bottom: calc(var(--pp-empty-pad) + var(--space-lg));
}

/* Checklist variant: .pp-cl-list has no surface of its own — its ROWS
   carry the border and background — so the empty block borrows the row
   surface. Without this the message floats on the page background while
   the two table views show it framed, which is the same message reading
   as two different things. Matches .pp-cl-item exactly. */
.pp-cl-list > .pp-empty {
    background: #fff;
    border: 1px solid rgba(0, 0, 0, .08);
    border-radius: 8px;
}
/* Delete confirmation modal content */
.pp-del-what { font-weight: 600; margin: 4px 0 2px; overflow-wrap: anywhere; }
.pp-del-when { font-size: .85rem; color: rgba(0, 0, 0, .55); margin-bottom: 4px; }
/* ── Production plan checklist (slice 5) ─────────────────────── */
.pp-cl-bar { display: flex; justify-content: space-between; gap: var(--space-sm); flex-wrap: wrap; align-items: center; margin-bottom: var(--space-md); }
.pp-cl-datenav { display: flex; align-items: center; gap: 6px; }
.pp-cl-day { font-weight: 600; margin-left: 4px; }
.pp-cl-filters { display: flex; gap: var(--space-xs); flex-wrap: wrap; }
.pp-cl-list { display: flex; flex-direction: column; gap: 8px; }
.pp-cl-dayhead { font-weight: 600; font-size: .9rem; margin: 10px 0 -2px; }
.pp-cl-dayhead:first-child { margin-top: 0; }
.pp-cl-item {
    display: flex; gap: 12px; align-items: flex-start;
    background: #fff; border: 1px solid rgba(0, 0, 0, .08);
    border-radius: 8px; padding: 12px 14px;
    transition: background-color .5s ease;
}
/* Checklist card tints — mirror of .pp-row-* on the plan table.
   followup has no rule of its own: it maps to .pp-cl--ok. */
.pp-cl--unconfirmed { background: var(--color-danger-ghost-bg); }
.pp-cl--ongoing   { background: #eff6ff; }
.pp-cl--ok        { background: var(--color-warning-bg); }
.pp-cl--completed { background: var(--color-success-bg); }
.pp-cl--overdue   { background: var(--color-danger-ghost-bg); }
.pp-cl-time { min-width: 120px; white-space: nowrap; font-weight: 600; }
.pp-cl-dur { display: block; font-weight: 400; font-size: .8rem; color: rgba(0, 0, 0, .55); }
.pp-cl-main { flex: 1; min-width: 0; }
.pp-cl-title { font-weight: 600; overflow-wrap: anywhere; }
.pp-cl-sub { font-size: .85rem; color: rgba(0, 0, 0, .6); overflow-wrap: anywhere; }
.pp-cl-meta { font-size: .8rem; color: rgba(0, 0, 0, .5); margin-top: 2px; }
.pp-cl-run { font-size: .85rem; margin-top: 4px; font-weight: 600; }
.pp-cl-actions { display: flex; flex-direction: column; gap: 6px; align-items: stretch; min-width: 112px; }
.pp-cl-actions .btn { min-height: 38px; }  /* field-thumb touch targets */
.pp-cl-state { text-align: center; font-weight: 600; font-size: .9rem; }
@media (max-width: 640px) {
    .pp-cl-item { flex-wrap: wrap; }
    .pp-cl-time { min-width: 0; }
    .pp-cl-actions { flex-direction: row; width: 100%; }
    .pp-cl-actions .btn { flex: 1; }
}
/* Start/close modal — centered, backdrop does NOT dismiss (modal rule) */
.pp-cl-overlay {
    position: fixed; inset: 0; z-index: 1100;
    background: rgba(0, 0, 0, .35);
    display: flex; align-items: center; justify-content: center; padding: 16px;
}
.pp-cl-modal {
    background: #fff; border-radius: 10px; padding: 18px;
    width: 100%; max-width: 380px;
    box-shadow: 0 12px 40px rgba(0, 0, 0, .2);
}
.pp-cl-modal-title { font-weight: 600; margin-bottom: 10px; overflow-wrap: anywhere; }
.pp-cl-modal-field { display: block; margin-bottom: 10px; font-size: .85rem; color: rgba(0, 0, 0, .6); }
.pp-cl-modal-field .form-control { margin-top: 4px; width: 100%; }
/* Supplier list print: only the report table reaches paper. */
/* ── Production plan print ────────────────────────────────────
   The plan and supplier tables carry 10-11 columns. Portrait at the
   browser's default margins squeezed them into unreadable slivers and
   still clipped the last column, so the sheet is landscape with tight
   margins and the app chrome is dropped entirely. */
@page { size: landscape; margin: 10mm; }

/* Belt and braces on the printed scrollbar: resetting overflow on the
   containers was not enough — a bar was still painted onto the sheet.
   Suppressing scrollbar rendering outright works whichever element
   owns it, and nothing on paper can be scrolled anyway. */
@media print {
    * { scrollbar-width: none !important; -ms-overflow-style: none !important; }
    *::-webkit-scrollbar { display: none !important; width: 0 !important; height: 0 !important; }
}

@media print {
    /* Chrome, controls and anything interactive: not on paper. */
    .sidebar, .app-sidebar, .topbar, .app-topbar, .settings-nav,
    .pp-sup-controls, .pp-plan-controls, .pp-cl-controls,
    .pp-print-btn, .pp-sup-link, .pp-new-row, .pp-col-del,
    .app-footer, footer, .modal-overlay, .pp-pop, .pp-msel-panel,
    .pp-thf {
        display: none !important;
    }

    /* The sidebar is fixed and .app-main reserves its width with a
       margin-left; hiding the sidebar leaves that as a blank strip
       down the left of every printed page. Reclaim the full sheet. */
    .app-main {
        margin-left: 0 !important;
        width: auto !important;
        max-width: none !important;
    }
    .app-content, .app-content-operational {
        padding: 0 !important;
        max-width: none !important;
    }
    /* Project switcher and the live "last updated" strip are screen
       affordances — the sheet already carries the plan title. */
    .live-strip, .live-ts, .live-warn, .live-refresh-btn,
    .pp-project-switcher, .pp-toolbar-right form { display: none !important; }
    .card, .card.pp-sup-print {
        border: 0 !important; box-shadow: none !important;
        padding: 0 !important; margin: 0 !important;
    }
    /* Any scroll container still paints its bar onto the sheet — the
       card, the table wrap and the density wrapper all scroll on
       screen. Reset them all rather than chasing one per view. */
    /* .app-shell is the app scroll container (overflow-y: auto). It was
       missing from this reset, so on paper it kept its own scroll box —
       painting a scrollbar onto the sheet and clipping the last row at
       the page boundary. Identified by rendering the print rules on
       screen and walking the table ancestors. */
    .app-shell, .app-main,
    .table-wrap, .card, .app-content, .app-content-operational,
    .pp-cl-list, .pp-sup-print, .pp-sup-print .table-wrap {
        margin: 0 !important;
        overflow: visible !important;
        overflow-x: visible !important;
        overflow-y: visible !important;
        max-height: none !important;
        height: auto !important;
    }

    /* Fit the width honestly rather than letting columns overflow:
       wrap inside cells instead of clipping the last column. */
    table.pp-table {
        width: 100% !important;
        table-layout: auto;
        font-size: 8.5pt;
        border-collapse: collapse;
    }
    table.pp-table th,
    table.pp-table td {
        padding: 3pt 4pt !important;
        white-space: normal !important;   /* beats the inline nowrap */
        vertical-align: top;
        border-bottom: 0.5pt solid #ccc;
    }
    /* Break long words ONLY in the free-text columns. Applying it to
       every cell split dates mid-token ("10.04.202 / 6"). */
    table.pp-table .pp-col-title,
    table.pp-table .pp-cell-sub { overflow-wrap: anywhere; }
    /* Dates and times are short and must never wrap or break. */
    table.pp-table td:first-child,
    table.pp-table td:nth-child(2) { white-space: nowrap !important; }
    /* The on-screen floor forces the table wider than the sheet and
       pushes the last columns off the page — the print layout sizes
       columns from content instead. */
    table.pp-table .pp-col-title { min-width: 0 !important; }
    .pp-cell-clamp { -webkit-line-clamp: none !important; max-height: none !important; }
    table.pp-table thead th {
        font-size: 7.5pt; text-transform: uppercase;
        border-bottom: 1pt solid #333;
    }
    /* Repeat the header on every page and never split a row — a
       half-row at a page break is unreadable on site. */
    table.pp-table thead { display: table-header-group; }
    table.pp-table tr    { break-inside: avoid; page-break-inside: avoid; }

    /* Status colours carry the meaning of the row — keep them. */
    .pp-row-ongoing td, .pp-row-ok td, .pp-row-unconfirmed td,
    .pp-row-completed td, .pp-row-overdue td {
        -webkit-print-color-adjust: exact;
        print-color-adjust: exact;
    }

    /* Checklist cards flatten to compact rows, each kept intact. */
    .pp-cl-item {
        break-inside: avoid; page-break-inside: avoid;
        border: 0; border-bottom: 0.5pt solid #ccc;
        border-radius: 0; padding: 4pt 0;
    }
    .pp-cl-actions { display: none !important; }
    .pp-cl-dayhead { break-after: avoid; page-break-after: avoid; }
}
/* ── Sheets-style multi-select (pp_msel) ──────────────────────
   Toolbar filter control: button + panel with search, Select all /
   Clear, checkbox list, Cancel/OK. Panel closes only via Cancel /
   OK / Escape / the button (modal rule). */
.pp-msel { position: relative; }
/* The button renders EXACTLY like select.hrc-toolbar-control (same
   border/shade/height from the shared class, same 9px chevron in the
   same spot) — the text caret span is suppressed. */
.pp-msel-btn {
    display: inline-flex; align-items: center;
    max-width: 260px; text-align: left;
    padding-right: 30px;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%23808080' stroke-width='1.5'><path d='M2 2 L6 6 L10 2'/></svg>");
    background-repeat: no-repeat;
    background-position: right 10px center;
    background-size: 9px;
}
.pp-msel-btn.is-filtered { font-weight: 600; }
.pp-msel-label {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    min-width: 0;
}
.pp-msel-caret { display: none; }
.pp-msel-panel {
    position: absolute; z-index: 1000; top: calc(100% + 4px); left: 0;
    width: 280px; max-width: calc(100vw - 24px);
    background: #fff; border: 1px solid rgba(0, 0, 0, 0.12);
    border-radius: 8px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.14);
    padding: 10px;
}
.pp-msel-q { width: 100%; margin-bottom: 4px; }
.pp-msel-links {
    display: flex; gap: 6px; font-size: .8rem;
    margin: 4px 2px 6px; color: rgba(0, 0, 0, .5);
}
.pp-msel-list { max-height: 260px; overflow-y: auto; margin-bottom: 8px; }
.pp-msel-opt {
    display: flex; align-items: center; gap: 8px;
    padding: 6px 4px; border-radius: 6px; cursor: pointer;
    font-size: .9rem;
}
.pp-msel-opt:hover { background: rgba(0, 0, 0, 0.04); }
.pp-msel-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.pp-msel-meta { color: rgba(0, 0, 0, .5); font-size: .75rem; white-space: nowrap; }
.pp-msel-foot { display: flex; justify-content: flex-end; gap: 8px; }
/* Extra panel controls (footer_html slot) — the status filter's
   hide-completed toggle. Visually part of the panel but outside
   .pp-msel-list, so the search field and Select all / Clear skip it. */
.pp-msel-extra {
    border-top: 1px solid rgba(0, 0, 0, 0.08);
    margin: 0 0 8px; padding-top: 4px;
}

/* ── Column-header filter funnels (pp_th_filter) ───────────────
   Sheets-style: the funnel in a <th> is a SECOND trigger for the
   toolbar pp_msel of the same data-param; the panel opens position:
   fixed under the icon (set inline by the msel script). is-filtered
   fills the funnel and shows the selection count, so a filtered
   column stays legible with the panel closed. */
.pp-thf {
    display: inline-flex; align-items: center; gap: 2px;
    margin-left: 4px; padding: 2px 3px;
    border: 0; background: none; border-radius: 4px;
    color: rgba(0, 0, 0, 0.35); cursor: pointer;
    vertical-align: middle; line-height: 0;
}
.pp-thf svg {
    width: 12px; height: 12px;
    fill: none; stroke: currentColor;
    stroke-width: 1.4; stroke-linejoin: round;
}
.pp-thf:hover { color: var(--color-text); background: rgba(0, 0, 0, 0.06); }
/* Label and funnel stay on ONE line. .pp-table th is white-space:normal
   for long labels, but a squeezed column (STATUS, rightmost) wrapped
   the funnel onto its own line under the text. */
.pp-table th:has(.pp-thf) { white-space: nowrap; }
/* Active = the filled glyph alone — no count beside it (2026-08-12). */
.pp-thf.is-filtered { color: var(--color-text); }
.pp-thf.is-filtered svg { fill: currentColor; }

/* Draft row starts COLLAPSED on desktop — the header-row "+" (in the
   trailing narrow column) reveals it, then keeps adding drafts. Scoped
   to ≥641: on phones the thead (and the "+") is gone, and the phone
   disclosure fold owns the row's visibility as before. The batch footer
   collapses with it (JS mirrors the class) — it carries the one save
   action for any draft count since 2026-08-13. */
@media (min-width: 641px) {
    .pp-table tr.pp-new-row.pp-new-collapsed,
    .pp-table tr.pp-new-foot.pp-new-collapsed { display: none; }
}

/* Park the toolbar instance of a header-filtered dimension while the
   table header (and its funnels) is on screen. It must stay RENDERED —
   display:none on the root would keep its panel from ever painting —
   and form-associated, so selections survive every toolbar submit.
   Out-of-flow so it costs no width and no flex gap in the filter row.
   Below 641px the table restacks into cards with no thead, and these
   become the visible controls again. */
@media (min-width: 641px) {
    .pp-msel-thonly { position: absolute; left: -9999px; top: 0; width: 0; }
}


/* ── Localized date inputs (hrc_date_input / date-input.js) ────
   The native control keeps name/value/validation and opens the OS
   picker; a text field in front of it shows the session language's
   mask, because every engine renders <input type="date"> in the
   BROWSER locale and ignores the `lang` attribute.

   The native input must stay RENDERED (opacity, not display:none):
   a non-rendered control cannot be focused for constraint validation
   and showPicker() throws on it. */
.hrc-di {
    position: relative;
    display: inline-flex;
    align-items: center;
    max-width: 100%;
}
/* Sizing: the visible field's intrinsic width comes from its `size`
   attribute (set to the mask length), so an inline-flex wrapper with no
   declared width still shrink-to-fits correctly — `width:100%` here
   would collapse it to nothing. It still grows to fill a wrapper that
   HAS a width, which is how full-width form fields keep their layout
   (enhance() marks those with .hrc-di--full). */
.hrc-di-text {
    flex: 1 1 auto;
    min-width: 0;
    padding-right: 30px;   /* room for the calendar button */
}
.hrc-di--full { display: flex; width: 100%; }
.hrc-di-native {
    position: absolute;
    right: 8px;
    top: 50%;
    transform: translateY(-50%);
    width: 1px;
    height: 80%;
    padding: 0;
    border: 0;
    opacity: 0;
    pointer-events: none;   /* the button drives the picker */
}
.hrc-di-btn {
    position: absolute;
    right: 1px;
    top: 1px;
    bottom: 1px;
    width: 28px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    background: none;
    border-radius: 0 7px 7px 0;
    color: rgba(0, 0, 0, 0.45);
    cursor: pointer;
    transition: color 0.12s ease;
}
.hrc-di-btn:hover { color: var(--color-text); }
/* Invalid free text: flag it without discarding what was typed, so the
   operator can correct a typo instead of re-entering the whole date. */
.hrc-di-text.hrc-di-invalid {
    border-color: var(--color-danger);
    box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.08);
}


/* ── Production plan → Variables tab ──────────────────────────
   Two catalogues side by side on desktop, stacked on narrow. */
/* minmax(min(320px, 100%), …), NOT minmax(320px, …): a grid track never
   shrinks past its own minimum, so a flat 320px min renders a 320px
   card inside the 296px content column at a 320px viewport, and
   .app-shell's overflow-x:hidden CLIPS the overhang rather than
   scrolling to it. Silent, because the missing pixels take the
   scrollbar that would have advertised them with it. min() caps the
   minimum at the container width.

   This used to defer to the same note on .pp-tools-grid; that rule is
   gone (the Tools tab is a collapsed row stack now), so the reasoning
   lives here rather than pointing at a deleted rule. */
.pp-var-grid {
    display: grid;
    gap: var(--space-md);
    grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
    align-items: start;
}
/* The global `.card + .card { margin-top }` rule still applies to grid
   children — they are DOM siblings — so the second card dropped a
   space-md below the first. Same neutralisation the import hub uses. */
.pp-var-grid > .card { margin-top: 0; }
.pp-var-add { display: flex; gap: var(--space-xs); margin-bottom: var(--space-sm); }
.pp-var-add .form-control { flex: 1 1 auto; min-width: 0; }
.pp-var-actions-col { width: 1%; }
.pp-var-actions { white-space: nowrap; display: flex; gap: 4px; align-items: center; justify-content: flex-end; }
/* Archived / hidden rows stay listed (that is how they get restored)
   but read as inactive. */
.pp-var-off td { opacity: .55; }

/* -- Production plan -> Tools tab -----------------------------
   A small administration toolbox, NOT a dashboard. Two sections
   (suppliers, labels) stacked in ONE column; each tool is a collapsed
   row stating in one line what it does, expanding its controls
   underneath at full page width.

   It was a three-column grid of four permanently-open forms. Every
   pick list, note, summary and footer rendered at once for an
   operation that touches exactly one of them, which made the cards
   very tall, the page long, and the fourth card a lone orphan on a
   second row. Collapsing puts the whole toolbox above the fold and
   hands the opened tool the width its searchable lists were being
   denied inside a ~420px card. Do not restore the grid. */
.pp-tools-section + .pp-tools-section { margin-top: var(--space-xl); }
.pp-tools-section-title {
    font-size: var(--font-size-base);
    font-weight: 650;
    margin: 0 0 var(--space-sm);
}

/* .card owns the surface; its padding moves inward to the header and
   the panel so the header row can reach the card's edges and be a
   full-width target. `.card + .card` margin is retuned to the tighter
   rhythm a LIST of tools wants rather than the one separate cards do. */
.pp-tools-item { padding: 0; }
.pp-tools-item + .pp-tools-item { margin-top: var(--space-sm); }

.pp-tools-head {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    width: 100%;
    padding: var(--space-md) var(--space-lg);
    text-align: left;
    background: transparent;
    border: 0;
    font: inherit;
    color: inherit;
}
button.pp-tools-head { cursor: pointer; }
button.pp-tools-head:hover { background: var(--color-bg); }
button.pp-tools-head:focus-visible {
    outline: 2px solid var(--color-border-strong);
    outline-offset: -2px;
}
.pp-tools-head-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1 1 auto;
    min-width: 0;
}
.pp-tools-head-title { font-size: var(--font-size-base); font-weight: 600; }
/* Wraps rather than ellipsizing: this line is what tells the two label
   tools apart, so it is the last thing that may be truncated. */
.pp-tools-head-lead { font-size: var(--font-size-sm); color: var(--color-text-muted); }
/* A tool with nothing to act on stays listed, with the reason in place
   of its lead. Which tools have work is itself an answer, and a better
   one than opening onto an empty panel. */
.pp-tools-item-off .pp-tools-head-title { color: var(--color-text-muted); }

/* Same caret mechanics as .sched-stats-drill-toggle — one expand idiom
   across the app. */
.pp-tools-caret {
    flex: 0 0 auto;
    width: 6px;
    height: 6px;
    margin-right: 2px;
    border-right: 1.5px solid var(--color-text-muted);
    border-bottom: 1.5px solid var(--color-text-muted);
    transform: rotate(-45deg);      /* points right when collapsed */
    transition: transform 0.15s ease;
}
.pp-tools-toggle[aria-expanded="true"] .pp-tools-caret { transform: rotate(45deg); }

.pp-tools-panel {
    padding: var(--space-md) var(--space-lg) var(--space-lg);
    border-top: 1px solid var(--color-border);
}
/* Nothing here sets `display`, so the UA `[hidden]` rule would already
   apply — stated explicitly because .pp-tools-opt below shows how
   quietly that breaks the day someone adds a display declaration. */
.pp-tools-panel[hidden] { display: none; }
/* A single pick list does not want the whole page: past ~760px the
   checkbox, the name and the count drift apart and the row stops
   scanning as one line. The two-column tools keep the full width via
   .pp-tools-cols, which is where the extra room actually helps. */
.pp-tools-panel > .pp-tools-col { max-width: 760px; }
.pp-tools-intro { margin: 0 0 var(--space-md); }

/* At phone widths the card's own horizontal padding is most of the
   row, so the header and panel tighten with it. */
@media (max-width: 640px) {
    .pp-tools-head  { padding: var(--space-sm) var(--space-md); }
    .pp-tools-panel { padding: var(--space-sm) var(--space-md) var(--space-md); }
}

/* The two pick lists. auto-fit stacks them below ~2x240px, which is
   also where a phone lands, so nothing overflows at 320px. */
.pp-tools-cols {
    display: grid;
    gap: var(--space-md);
    grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
}
.pp-tools-col { display: flex; flex-direction: column; min-width: 0; }
.pp-tools-col-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--space-xs);
    margin-bottom: 6px;
}
.pp-tools-col-label { font-size: var(--font-size-sm); font-weight: 600; }
.pp-tools-link {
    background: none;
    border: 0;
    padding: 0;
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
    cursor: pointer;
    text-decoration: underline;
}
.pp-tools-link:hover { color: var(--color-text); }
.pp-tools-search { width: 100%; margin-bottom: 6px; }

/* Scrolls INSIDE its own box, so a plan with 80 freetext values does
   not push the Confirm button off the page. Bounded height is what
   keeps the summary and the action visible while picking. */
.pp-tools-list {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    max-height: 280px;
    overflow-y: auto;
    background: var(--color-surface);
}
.pp-tools-opt {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    padding: 6px 10px;
    cursor: pointer;
    border-bottom: 1px solid var(--color-border);
}
.pp-tools-opt:last-child { border-bottom: 0; }
.pp-tools-opt:hover { background: var(--color-bg); }
/* REQUIRED for the search box to do anything. The UA stylesheet's
   `[hidden] { display: none }` loses to ANY author `display`
   declaration regardless of specificity, so the `display: flex` above
   silently defeated it: the filter set .hidden on every non-matching
   option and every option kept rendering. Do not drop this rule while
   .pp-tools-opt sets display. */
.pp-tools-opt[hidden] { display: none; }
.pp-tools-opt input { flex: 0 0 auto; margin: 0; }
.pp-tools-opt-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--font-size-sm);
}
/* Merge rows carry TWO controls, so the row is a plain div and the
   checkbox gets its own label around the name. Nesting the Keep radio
   inside a label bound to the checkbox would make one click toggle
   both. */
.pp-tools-opt-main {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    flex: 1 1 auto;
    min-width: 0;
    cursor: pointer;
}
.pp-tools-keep {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
    cursor: pointer;
    white-space: nowrap;
}
/* The survivor picker is meaningless until a row is in the merge, so it
   reads as unavailable rather than merely being inert. */
.pp-tools-keep input:disabled { cursor: default; }
.pp-tools-keep:has(input:disabled) { opacity: .35; cursor: default; }
.pp-tools-opt-on { background: var(--color-accent-subtle); }
.pp-tools-opt-on .pp-tools-keep { color: var(--color-text); }
/* The row count. Tabular so the column of numbers stays readable when
   scanning for the big ones, which is how an operator finds the values
   worth cleaning up first. */
.pp-tools-opt-meta {
    flex: 0 0 auto;
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
}
.pp-tools-col-foot { margin-top: 6px; }
.pp-tools-note { margin: var(--space-sm) 0 0; }

.pp-tools-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-sm);
    margin-top: var(--space-sm);
    padding-top: var(--space-sm);
    border-top: 1px solid var(--color-border);
}
/* Muted while the pick is incomplete, full strength once it states a
   real action: the operator should be able to tell at a glance whether
   the sentence in front of them is a prompt or a commitment. */
.pp-tools-summary {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    min-width: 0;
    flex: 1 1 220px;
}
.pp-tools-summary-ready { color: var(--color-text); font-weight: 600; }
.pp-tools-actions { display: flex; gap: var(--space-xs); flex: 0 0 auto; }



/* ── Production plan toolbars: flex, not the statistics grid ───
   At ≥1024px .sched-stats-row becomes `grid: auto minmax(0,1fr) auto`,
   which is a contract for schedule-statistics' exact three groups in a
   fixed order. The plan / checklist / supplier toolbars don't have that
   shape — the plan's filter row alone carries seven selects plus a
   search field — so their controls landed in the wrong cells: one
   select stretched into the 1fr column while the next was pushed to the
   far right and the rest wrapped. Keep the shared period-bar styling,
   opt out of the grid. */
@media (min-width: 1024px) {
    .pp-plan-controls .sched-stats-row,
    .pp-cl-controls   .sched-stats-row,
    .pp-sup-controls  .sched-stats-row {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 6px 10px;
    }
}
/* Period presets / step arrows anchor to the right of their own row,
   the way they do on the statistics page. */
.pp-plan-controls .sched-stats-group-nav,
.pp-cl-controls   .sched-stats-group-nav,
.pp-sup-controls  .sched-stats-group-nav { margin-left: auto; }

/* Label/value tables in the import reconciliation. The global
   "action column" rule (tbody td:last-child { text-align: right })
   catches the VALUE cell of a two-cell <th>/<td> row, which was tolerable
   at max-width:900px and strands the value against the far edge now the
   wizard is full width. These read as a definition list, so the value
   belongs next to its label and must be free to wrap. */

/* Reconciliation action row: commit + cancel on ONE line.
   They are separate <form>s (different `step`, and back must not carry
   the commit fields), so the row is a flex wrapper and each form loses
   its default block margin. Wraps on narrow screens rather than
   overflowing — the buttons keep their order, primary first. */
.pp-imp-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm);
    margin-top: var(--space-md);
}
.pp-imp-actions form { margin: 0; }

.pp-imp-kv tbody td:last-child {
    text-align: left;
    white-space: normal;
}

/* ── Import preview table (shared by every import wizard) ──────
   A tall scroll box with a sticky header, so an operator can read the
   parsed rows before committing without the page growing to 200 screens.
   Used by the shared settings importer (users / shifts / sessions /
   assignments) AND by the production plan's own wizard — it was inline
   in import_partials.php, which meant the plan wizard could not reuse
   the look without copying it. */
.import-preview-scroll {
    max-height: 60vh;
    overflow: auto;
    border: 1px solid var(--color-border, #e5e5e5);
    border-radius: 4px;
}
.import-preview-scroll .data-table thead th,
.import-preview-scroll .hrc-table thead th {
    position: sticky;
    top: 0;
    z-index: 1;
    background: var(--color-surface, #fff);
    box-shadow: inset 0 -1px 0 var(--color-border, #e5e5e5);
}

/* ── Mobile disclosure (phone-only collapse) ───────────────────
   Desktop: the trigger does not exist and the body is simply part of
   the layout — no interaction is introduced above 640px. */
.pp-disclosure { display: none; }
.pp-disclosure-hide { display: none; }

/* Threshold is 1023, not 640: the shared toolbar already switches to the
   vertical stack there ("the tablet range doesn't have enough horizontal
   room for the desktop three-column grid"), and an iPad at 768 showed
   611px of chrome before the first plan row. */
@media (max-width: 1023px) {
    .pp-disclosure {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        gap: 6px;
        width: 100%;
        padding: 9px 12px;
        border: 1px solid var(--color-border);
        border-radius: var(--radius-md);
        background: var(--color-white);
        font-size: .85rem;
        font-weight: 600;
        color: var(--color-text);
        cursor: pointer;
        user-select: none;
    }
    .pp-disclosure:active { background: rgba(0, 0, 0, 0.04); }
    /* Keyboard reachability: the checkbox is hidden, so mirror its
       focus onto the label the user actually sees. */
    .pp-disclosure-cb:focus-visible + .pp-disclosure {
        outline: 2px solid var(--color-text);
        outline-offset: 2px;
    }
    .pp-disclosure-cb:not(:checked) ~ .pp-disclosure-body { display: none; }
    .pp-disclosure-cb:checked ~ .pp-disclosure .pp-disclosure-show { display: none; }
    .pp-disclosure-cb:checked ~ .pp-disclosure .pp-disclosure-hide { display: inline; }
    /* Trigger sits in the toolbar's own rhythm. */
    .sched-stats-controls .pp-disclosure { margin-top: 2px; }
}

/* The new-row only becomes expensive once the table restacks into cards
   (≤640), where its twelve field cells form a 473px column. Between 641
   and 1023 it is still a single ~53px table row, so collapsing it there
   would cost a tap and save nothing. */
@media (min-width: 641px) and (max-width: 1023px) {
    #pp-newrow-toggle + .pp-disclosure { display: none; }
    /* The supplier report's fold is phone-only too — its filter band is
       fully visible on tablet, so the trigger must not render there. */
    #pp-sup-filters-toggle + .pp-disclosure { display: none; }
    /* The plan's second row holds only header-parked controls from 641
       up (search moved to row one) — a trigger here would toggle an
       empty band. Phone-only, like the supplier report's. */
    #pp-filters-toggle + .pp-disclosure { display: none; }
    /* …and that row must stay RENDERED in this band even with the fold
       checkbox unchecked: display:none on it would keep the parked
       msels' panels — what the header funnels open — from ever
       painting. Visually it is empty here; every child is parked
       off-screen. (≤640 keeps the generic fold rule: the thead and its
       funnels are gone there, and the pills must genuinely fold.) */
    .pp-plan-controls .pp-disclosure-cb:not(:checked) ~ .pp-plan-filters { display: flex; }
}
@media (max-width: 640px) {
    /* On phones ONE toggle governs the whole filter chrome: the period
       band (FROM/TO + Phase/Type/Status + hide-completed) folds behind
       the same checkbox as the attribute row. The presets stay visible —
       they are the one-tap period control — and the trigger carries an
       active-filter count so a narrowed list is never unexplained.
       :has() because the checkbox sits AFTER row 1 in the DOM, out of
       sibling-combinator reach. Plan view only: the checklist's period
       band is its primary control (day stepper) and stays put. */
    .pp-plan-controls:has(#pp-filters-toggle:not(:checked)) .sched-stats-group-daterange,
    .pp-plan-controls:has(#pp-filters-toggle:not(:checked)) .sched-stats-group-filters { display: none; }
    /* Supplier report: same fold (4b parity). Its single filter band —
       FROM/TO, supplier, phase, label, hide-completed, search — folds
       behind its own toggle; presets + Print stay. The checkbox renders
       CHECKED server-side when any foldable filter is active, so there
       is no auto-open script here. */
    .pp-sup-controls:has(#pp-sup-filters-toggle:not(:checked)) .sched-stats-group-daterange,
    .pp-sup-controls:has(#pp-sup-filters-toggle:not(:checked)) .sched-stats-group-filters { display: none; }

    .pp-disclosure-cb:not(:checked) ~ .card .pp-new-row,
    .pp-disclosure-cb:not(:checked) ~ .card .pp-new-foot { display: none; }
    /* The status cell stacks like the other draft inputs (.pp-new-row td
       is display:block below); no flex override needed since the Add
       button moved to the batch footer (2026-08-13). */
    #pp-newrow-toggle + .pp-disclosure { margin-bottom: var(--space-sm); }
}

/* ── Production plan toolbars below the desktop grid ───────────
   The shared statistics stack (max-width:1023) turns .sched-stats-row
   into a COLUMN of full-width bands. That is right for the period bar
   and the preset nav, but wrong for the plan's filter groups, which
   carry up to six pills plus a search field:

     • each pill became a full-width column item that painted only its
       natural width — one pill per row, the rest of the row empty. The
       plan toolbar measured 648px tall at 390px wide, before a single
       row of data.
     • .pp-search's `flex: 1 1 220px` is a WIDTH basis. In a column the
       basis applies to the main axis, i.e. the height — so the search
       field rendered as a 220px-tall box.

   Both go away by keeping the filter containers as wrapping ROWS. */
@media (max-width: 1023px) {
    .pp-plan-controls .sched-stats-group-filters,
    .pp-sup-controls  .sched-stats-group-filters,
    .pp-plan-controls .pp-plan-filters,
    .pp-cl-controls   .pp-plan-filters {
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: 6px;
    }
    /* Pack by available width rather than a fixed column count: a 150px
       basis gives two per row at 320px and four to five across a tablet,
       with each pill growing to fill its share — a proper tap target
       instead of a narrow control floating in an empty full-width row.
       No extra breakpoint needed between phone and tablet. */
    .pp-plan-controls .pp-msel,
    .pp-cl-controls   .pp-msel,
    .pp-sup-controls  .pp-msel {
        flex: 1 1 150px;
        min-width: 0;
    }
    .pp-plan-controls .pp-msel-btn,
    .pp-cl-controls   .pp-msel-btn,
    .pp-sup-controls  .pp-msel-btn { width: 100%; }
    /* Search gets its own full row — and, being back in a row context,
       reads 100% as a width again. */
    .pp-plan-controls .pp-search,
    .pp-cl-controls   .pp-search,
    .pp-sup-controls  .pp-search { flex: 1 1 100%; }
    /* Preset labels must not break mid-phrase ("Gjeldende fase"). */
    .pp-plan-controls .sched-stats-preset,
    .pp-cl-controls   .sched-stats-preset,
    .pp-sup-controls  .sched-stats-preset { white-space: nowrap; }
    /* The preset pills fill the row at 320px, so Skriv ut wraps to a
       second line — and was rendering OUTSIDE the nav band, overlapping
       the FROM/TO inputs below it. Let the band wrap and grow with its
       content, and let the preset group shrink rather than claim the
       whole width. */
    .pp-plan-controls .sched-stats-group-nav,
    .pp-cl-controls   .sched-stats-group-nav,
    .pp-sup-controls  .sched-stats-group-nav {
        flex-wrap: wrap;
        align-items: center;
        row-gap: 6px;
        height: auto;
    }
    .pp-plan-controls .sched-stats-presets,
    .pp-cl-controls   .sched-stats-presets,
    .pp-sup-controls  .sched-stats-presets {
        flex: 1 1 auto;
        min-width: 0;
        flex-wrap: wrap;
    }
    .pp-plan-controls .pp-print-btn,
    .pp-cl-controls   .pp-print-btn,
    .pp-sup-controls  .pp-print-btn { flex: 0 0 auto; }
    /* FROM and TO side by side instead of one band each — two short
       datetime inputs fit a phone row and save ~40px of chrome. */
    .pp-plan-controls .sched-stats-group-daterange,
    .pp-cl-controls   .sched-stats-group-daterange,
    .pp-sup-controls  .sched-stats-group-daterange {
        flex-wrap: nowrap;
        gap: 4px;
    }
    .pp-plan-controls .sched-stats-daterange,
    .pp-cl-controls   .sched-stats-daterange,
    .pp-sup-controls  .sched-stats-daterange { flex: 1 1 0; min-width: 0; }
    .pp-plan-controls .sched-stats-daterange .hrc-toolbar-control,
    .pp-cl-controls   .sched-stats-daterange .hrc-toolbar-control,
    .pp-sup-controls  .sched-stats-daterange .hrc-toolbar-control { width: 100%; min-width: 0; }
    /* The shared stack orders nav first — right for the checklist,
       whose nav group is the prev/next DAY STEPPER (primary control),
       wrong for the plan and supplier report, where that group is
       presets + Print: the least-used controls were rendering above
       the filters on every tablet. Filters first, presets/Print last.
       .pp-cl-controls is deliberately NOT included. */
    .pp-plan-controls .sched-stats-group-nav,
    .pp-sup-controls  .sched-stats-group-nav { order: 4; }
}

/* Touch ergonomics for the plan toolbars: the module's own floor is
   38px (.pp-cl-actions .btn — "field-thumb touch targets"), but the
   presets measured 26px, filter-panel option rows 29px and the delete
   button ~24px. Scoped to coarse pointers so desktop density is
   untouched. */
@media (pointer: coarse) {
    .pp-plan-controls .sched-stats-preset,
    .pp-cl-controls   .sched-stats-preset,
    .pp-sup-controls  .sched-stats-preset { min-height: 38px; }
    .pp-msel-opt      { min-height: 38px; }
    .pp-del-btn       { min-width: 38px; min-height: 38px; }
    #pp-live-refresh  { min-width: 38px; min-height: 38px; }
    .pp-cl-controls .sched-stats-nav-arrow { width: 38px; height: 38px; }
}

/* A native select never shrinks below its longest <option>, and the
   project switcher sits in a non-wrapping header row — one long
   project name at 320px was the last remaining horizontal-scroll
   risk on the page. Clamp it; the selected name ellipsizes. */
.pp-project-select { min-width: 0; max-width: 48vw; text-overflow: ellipsis; }

/* ── Row selection + bulk-action bar (Gmail pattern) ──────────── */
.pp-col-sel { width: 30px; text-align: center; }
.pp-col-sel input[type="checkbox"] { margin: 0; vertical-align: middle; }
.pp-bulkbar {
    display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
    padding: 8px 12px; margin-bottom: var(--space-sm);
    border: 1px solid var(--color-border); border-radius: var(--radius-md);
    background: var(--color-surface);
}
.pp-bulkbar[hidden] { display: none; }
.pp-bulkbar-count { font-weight: 600; font-size: .85rem; margin-right: 4px; white-space: nowrap; }
@media (max-width: 640px) {
    /* On cards the selection checkbox is the card's first line; no
       ::before label — a checkbox explains itself. */
    .pp-table tbody td.pp-col-sel { display: flex; justify-content: flex-end; }
    .pp-table tbody td.pp-col-sel::before { content: none; }
    /* The draft row's empty selection cell is dead space on a card. */
    .pp-new-row td.pp-col-sel { display: none; }
}
@media (pointer: coarse) {
    .pp-col-sel input[type="checkbox"] { width: 22px; height: 22px; }
    .pp-bulkbar .btn { min-height: 38px; }
}

/* Start-task modal: grouped headcounts (own / external). */
.pp-cl-modal-group {
    font-weight: 600; font-size: .8rem; text-transform: uppercase;
    letter-spacing: .04em; color: rgba(0, 0, 0, .55);
    margin: 10px 0 4px;
}
.pp-cl-modal-group:first-child { margin-top: 0; }
.pp-cl-modal-toggle {
    display: flex; align-items: center; gap: 8px;
    font-weight: 600; font-size: .8rem; text-transform: uppercase;
    letter-spacing: .04em; color: rgba(0, 0, 0, .55);
    margin: 12px 0 4px; cursor: pointer;
}


/* ── Production plan table → cards on phones ───────────────────
   12 columns cannot be a table on a 390px screen. It did not
   overflow the page (the wrap scrolls) but that is the floor, not a
   design: LOKASJON onward sat off-screen behind a sideways scroll.
   Under 640px each row becomes a card, every column gets its header
   as an inline label, and the columns hidden on mobile come BACK —
   a card has vertical room, so there is no reason to drop data.
   Labels come from ::before rules generated in index.php, keyed on
   the data-f each cell already carries, so no markup changed. */
@media (max-width: 640px) {
    .pp-table thead { display: none; }
    .pp-table, .pp-table tbody { display: block; width: 100%; }

    /* Undo the card's full-bleed offsets. They are desktop TABLE
       geometry: `.card .table-wrap` pulls -24px left/right (and -24px
       bottom on :last-child) so table borders meet the card edge. Once
       the rows are restacked as cards that offset dragged the whole
       stack 24px out of its container, and `.card { overflow: hidden }`
       clipped it — which is why the inline labels rendered as "ATO",
       "YPE", "OKASJON" with their first letters cut off. Cards want
       ordinary in-card padding instead; the .card itself is padding:0
       inline, so the padding goes on the wrap. */
    .card .table-wrap:has(table.pp-table) {
        margin-left: 0;
        margin-right: 0;
        margin-bottom: 0;
        padding: var(--space-sm);
    }

    .pp-table tbody tr {
        display: block;
        border: 1px solid rgba(0, 0, 0, .10);
        border-radius: 8px;
        padding: 10px 12px;
        margin-bottom: 8px;
    }
    /* Status tint moves from the cells to the whole card. */
    .pp-table tbody tr.pp-row-ongoing   { background: #eff6ff; }
    .pp-table tbody tr.pp-row-ok        { background: var(--color-warning-bg); }
    .pp-table tbody tr.pp-row-completed { background: var(--color-success-bg); }
    .pp-table tbody tr.pp-row-overdue   { background: var(--color-danger-ghost-bg); }
    .pp-table tbody tr.pp-row-unconfirmed { background: var(--color-danger-ghost-bg); }
    .pp-table tbody tr td { background: transparent !important; }

    /* The edge-padding overrides are desktop table geometry. */
    .card .table-wrap table.pp-table tbody td:first-child,
    .card .table-wrap table.pp-table tbody td:last-child { padding: 0 !important; }
    .card .table-wrap table.pp-table tbody tr:last-child td,
    .card .table-wrap table.pp-table tbody tr:has(+ tr.pp-more-row[hidden]) td { padding-bottom: 0 !important; }
    /* …but the empty row is a message, not a data cell being restacked
       into a card. It keeps its own padding so the text is not flush
       against the card edge. (Proper mobile pass still to come.) */
    .card .table-wrap table.pp-table tbody tr.pp-empty-row td {
        display: block;
        padding: var(--space-lg) var(--space-sm) !important;
    }

    .pp-table tbody td {
        display: flex;
        gap: 10px;
        align-items: baseline;
        padding: 3px 0 !important;
        border: 0;
        white-space: normal !important;   /* beats the inline nowrap */
    }
    /* No pp-table cell carries .hide-mobile any more: the class blanked
       Supplier/Label/delete in the 641-768px band where the TABLE still
       renders (cards start below 641), hiding data and actions on iPad
       portrait. Every column now shows at every width. */
    .pp-table tbody td.pp-col-del  { display: flex; justify-content: flex-end; }

    .pp-table tbody td::before {
        content: attr(data-f);            /* replaced per column below */
        flex: 0 0 84px;
        font-size: .7rem;
        text-transform: uppercase;
        letter-spacing: .04em;
        color: rgba(0, 0, 0, .45);
    }
    /* Desktop right-aligns the trailing status column; inside a card the
       label+value pair must read left like every other field. */
    .pp-table tbody td { text-align: left !important; }
    /* Title leads the card: no label, full width, heavier. */
    .pp-table tbody td.pp-col-title { display: block; font-weight: 600; }
    .pp-table tbody td.pp-col-title::before { content: none; }
    .pp-table tbody td.pp-col-del::before   { content: none; }
    /* Hide fields with no value: a card showing "Ansvarlig —" four
       times over is noise on a phone. td:empty cannot do this (the
       cells carry template whitespace), so the row declares which
       fields are blank and CSS matches the word. */
    .pp-table tbody tr[data-empty~="type_id"]             td[data-f="type_id"],
    .pp-table tbody tr[data-empty~="locations"]           td[data-f="locations"],
    .pp-table tbody tr[data-empty~="responsibles"]        td[data-f="responsibles"],
    .pp-table tbody tr[data-empty~="suppliers"]           td[data-f="suppliers"],
    .pp-table tbody tr[data-empty~="labels"]              td[data-f="labels"],
    /* status is deliberately NOT in this list: the blank not_started cell
       is the card's tap target for setting a status — hiding it locked
       phones out of the rows that still need action. */
    .pp-table tbody tr[data-empty~="del"]                 td.pp-col-del { display: none !important; }

    /* Cell min-width floors are table geometry; drop them. */
    .pp-table .pp-col-title { min-width: 0; }
    /* The add-row keeps its own stacked layout, not the card labels. */
    .pp-new-row td::before { content: none !important; }
    .pp-new-row td { display: block; padding: 3px 0 !important; }
}

/* -- Bulk-actions card (shared primitive) --------------------
   Consumed by BOTH workspace/groups (group detail) and
   workspace/departments (department detail). Lived as an inline
   <style> in groups/index.php until 2026-08-09; moved here when
   Departments adopted the same card so the two surfaces cannot
   drift apart visually. Everything is scoped behind .bulk-card, so
   promoting it to global changes nothing about what it matches.
   Add new bulk-card rules HERE, never back into a page. */

/* ── Bulk-actions card (group detail • details tab) ──────────
   Scoped via .bulk-card so nothing here leaks into other cards. */
.bulk-card { margin-top: var(--space-lg); padding: 0; }
.bulk-card-head {
    padding: var(--space-md) var(--space-md) var(--space-sm);
}
.bulk-card-title { margin: 0 0 4px; font-size: 1.05rem; }
.bulk-card-intro {
    margin: 0;
    color: var(--color-muted);
    font-size: 0.875rem;
    line-height: 1.45;
}
.bulk-action-list {
    display: flex;
    flex-direction: column;
    border-top: 1px solid var(--color-border);
}
.bulk-action-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 280px 110px;
    gap: var(--space-md);
    align-items: center;
    padding: var(--space-md);
    border-bottom: 1px solid var(--color-border);
}
.bulk-action-row:last-child { border-bottom: none; }
.bulk-section-divider {
    padding: var(--space-sm) var(--space-md);
    background: var(--color-surface-alt, #fafafa);
    border-top: 1px solid var(--color-border);
    border-bottom: 1px solid var(--color-border);
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--color-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.bulk-action-row-danger {
    background: rgba(220, 38, 38, 0.04);
}
.bulk-action-row-danger .bulk-action-title { color: #b91c1c; }
.bulk-action-text { min-width: 0; }
.bulk-action-title { font-weight: 600; font-size: 0.95rem; line-height: 1.3; }
.bulk-action-desc {
    color: var(--color-muted);
    font-size: 0.82rem;
    line-height: 1.4;
    margin-top: 2px;
}
.bulk-action-input { min-width: 0; }
.bulk-action-input .form-control { width: 100%; }
.bulk-action-cta { display: flex; }
.bulk-action-cta .btn { width: 100%; white-space: nowrap; }

@media (max-width: 720px) {
    .bulk-card-head { padding: var(--space-md); }
    .bulk-action-row {
        grid-template-columns: 1fr;
        gap: var(--space-sm);
        padding: var(--space-md);
    }
    .bulk-action-input:empty { display: none; }
    .bulk-action-cta .btn { width: 100%; }
}
.bulk-counts {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-sm);
    margin-top: var(--space-md);
    padding: var(--space-sm) var(--space-md);
    background: var(--color-surface-alt, #fafafa);
    border-radius: var(--radius-md);
    font-size: 0.95rem;
}
.bulk-counts strong { font-size: 1.5rem; display: block; }
@media (max-width: 480px) {
    .bulk-counts { grid-template-columns: 1fr; }
}
.bulk-typed-block {
    margin-top: var(--space-md);
    padding: var(--space-sm) var(--space-md);
    border: 1px solid #fecaca;
    background: #fef2f2;
    border-radius: var(--radius-md);
}
.bulk-typed-warn {
    margin: 0 0 var(--space-sm);
    color: #b91c1c;
    font-size: 0.875rem;
    line-height: 1.4;
}
.bulk-typed-label {
    display: block;
    font-size: 0.85rem;
    color: var(--color-muted);
    margin-bottom: 6px;
}
.bulk-typed-label strong { color: #b91c1c; letter-spacing: 0.04em; }
.bulk-typed-block .form-control {
    font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}
.bulk-counts-loading { color: var(--color-muted); font-style: italic; padding: var(--space-md) 0; text-align: center; }
.bulk-warn { color: var(--color-warning); font-size: 0.85rem; margin-top: var(--space-xs); }

/* ── Shift eligibility picker (migration 402) ──────────────────
   WHO MAY SIGN UP for a shift. Parents are selectable here and cover
   their children, so a covered child is shown dimmed rather than
   hidden — the operator can see the roll-up it is part of. */
/* All / Selected groups. "All" is the empty set — the radios make that
   default an explicit choice rather than a consequence of leaving the
   tree untouched. */
/* The .hrc-scope-* names on each rule are the module-neutral spelling of
   the same control: an All / Selected radio pair over a parent-child
   checkbox tree, where ticking a parent covers its children. The shift
   eligibility picker (.sched-elig-*) was the first user; the Production
   Plan responsible scope is the second. New surfaces use .hrc-scope-*.
   Both spellings are kept on one rule set on purpose — one visual, and
   nothing to keep in sync — until schedule/index.php is renamed over. */
.sched-elig-mode,
.hrc-scope-mode {
    display: flex;
    /* Wraps because the shift control carries three options, the longest
       of which ("Not available for signup") cannot share a 320px row with
       the other two. Row-gap keeps the wrapped rows from touching. */
    flex-wrap: wrap;
    gap: var(--space-md);
    row-gap: var(--space-xs);
    margin-bottom: var(--space-xs);
}
.sched-elig-mode .sched-elig-row,
.hrc-scope-mode .hrc-scope-row { padding: 0; }
.sched-elig-picker[hidden],
.hrc-scope-picker[hidden] { display: none; }
.sched-elig-picker,
.hrc-scope-picker {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm, 4px);
    padding: var(--space-xs) var(--space-sm);
    max-height: 220px;
    overflow-y: auto;
}
.sched-elig-branch + .sched-elig-branch,
.hrc-scope-branch + .hrc-scope-branch {
    margin-top: var(--space-xs);
    padding-top: var(--space-xs);
    border-top: 1px solid var(--color-border);
}
.sched-elig-row,
.hrc-scope-row {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    padding: 3px 0;
    cursor: pointer;
    font-weight: 400;
    margin: 0;
}
.sched-elig-row--child,
.hrc-scope-row--child { padding-left: 22px; }
.sched-elig-row input,
.hrc-scope-row input { margin: 0; flex: none; }
.sched-elig-row.is-covered,
.hrc-scope-row.is-covered { opacity: 0.45; cursor: default; }
/* Section label above a tree when one picker spans two dimensions
   (Departments / Groups). The heading directly above the first branch
   would otherwise inherit that branch's top border. */
.hrc-scope-heading {
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
    margin: var(--space-sm) 0 2px;
}
.hrc-scope-heading:first-child { margin-top: 0; }
.hrc-scope-heading + .hrc-scope-branch {
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
}
/* Roll-up annotation on a parent row. Its own class, not a bare
   .text-muted span: inline after the name it read as part of the
   group label ("Stab inkluderer undergrupper"). */
.sched-elig-rollup,
.hrc-scope-rollup {
    color: var(--color-text-muted);
    font-size: 0.8rem;
    font-weight: 400;
    white-space: nowrap;
}


/* ══ Production Plan · Sheet (staging entry grid) ══════════════
   A dense keyboard/paste entry surface. It reuses the platform inputs,
   borders, radii and the .hrc-picker-dd dropdown rather than
   introducing a spreadsheet look of its own. The only thing here that
   is genuinely new is the ROW STATE colouring, because nothing else in
   the app has four states on one row.

   Row-state palette. The scale is deliberately quiet: a sheet being
   typed into is mostly "incomplete", and a surface that shouts at every
   half-filled row is a surface people stop reading. Only two states
   carry real colour.

     empty       no tint at all
     incomplete  a hairline on the row gutter, nothing more
     invalid     a soft warm tint + a red left edge
     valid       a soft green tint, the one the operator is aiming for

   Cell-level problems are a border + a title tooltip, never inline
   text: a message under every bad cell would double the row height and
   push the columns being typed into off-screen. */

.pp-sheet-card { padding-bottom: var(--space-md); }

.pp-sheet-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    flex-wrap: wrap;
    gap: var(--space-sm);
}
.pp-sheet-lede {
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    margin: 2px 0 0;
    max-width: 62ch;
}
.pp-sheet-actions {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    flex-wrap: wrap;
}
.pp-sheet-counts {
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
    white-space: nowrap;
}

.pp-sheet-legend {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-sm) var(--space-md);
    margin: var(--space-sm) 0;
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
}
.pp-sheet-legend span { display: inline-flex; align-items: center; gap: 6px; }
.pp-sheet-legend .pp-sheet-hint { margin-left: auto; }
/* Each swatch is a MINIATURE ROW, not a colour chip: the tint plus the
   3px left accent bar, which is exactly the pair of signals a real row
   carries. A 12px square showing only the tint rendered as four
   near-white boxes with a hairline border - visually indistinguishable
   from each other AND from unchecked checkboxes, which also implied
   they were filters you could toggle. Seen in a real browser at 1500px;
   the computed colours had been "correct" all along. */
.pp-sheet-sw {
    width: 26px; height: 14px;
    border-radius: 2px;
    border: 1px solid var(--color-border);
    display: inline-block;
    vertical-align: -2px;
}
.pp-sheet-sw-empty      { background: var(--color-surface); }
.pp-sheet-sw-incomplete { background: #fafafa;   box-shadow: inset 3px 0 0 rgba(0, 0, 0, 0.14); }
.pp-sheet-sw-invalid    { background: #fdf1ef;   box-shadow: inset 3px 0 0 rgba(203, 60, 45, 0.75); }
.pp-sheet-sw-valid      { background: #eef6f0;   box-shadow: inset 3px 0 0 rgba(38, 138, 78, 0.65); }

/* Restored-draft strip. Restoring silently would be the wrong kind of
   helpful: the operator has to be able to see that this content is from
   a previous visit, and to throw it away in one click. */
.pp-sheet-restored {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: 8px 10px;
    margin-bottom: var(--space-sm);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: rgba(0, 0, 0, 0.02);
    font-size: var(--font-size-sm);
}
.pp-sheet-discard {
    background: none; border: 0; padding: 0;
    color: var(--color-text);
    text-decoration: underline;
    cursor: pointer;
    font: inherit;
}

/* Horizontal scroll lives on the WRAPPER, never on the page. The full
   template is wider than any laptop and squeezing it would make every
   column unusable; scrolling one container keeps the workspace layout
   rule (no horizontal page scroll) intact. */
.pp-sheet-wrap {
    overflow-x: auto;
    overflow-y: visible;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface);
}

table.pp-sheet {
    border-collapse: separate;
    border-spacing: 0;
    width: max-content;
    min-width: 100%;
    font-size: var(--font-size-sm);
}
table.pp-sheet th,
table.pp-sheet td {
    border-bottom: 1px solid var(--color-border);
    border-right: 1px solid var(--color-border);
    padding: 0;
    vertical-align: middle;
}
table.pp-sheet th {
    background: var(--color-surface);
    padding: 6px 8px;
    text-align: left;
    font-size: var(--font-size-xs);
    text-transform: uppercase;
    letter-spacing: 0.02em;
    color: var(--color-text-muted);
    white-space: nowrap;
    position: sticky;
    top: 0;
    z-index: 3;
    border-bottom: 1px solid var(--color-border-strong);
}
/* The required marker is the entry POLICY, not a domain constraint.
   The title attribute on the abbr says so in words. */
.pp-sheet-req {
    color: var(--color-text-muted);
    margin-left: 2px;
    text-decoration: none;
    cursor: help;
}

/* Frozen Date + What. Two sticky columns, plain CSS, no grid library:
   they are the two the operator reads to know which row they are in.

   The left offsets are the accumulated widths of everything to their
   left, which is why those columns carry fixed widths below AND why
   pp_sheet_columns() puts Date and What adjacent. With another column
   between them, the intervening cells scroll UNDER the frozen block and
   a moving gap opens between the two frozen ones. Do not reorder the
   contract without re-deriving these offsets. */
table.pp-sheet .pp-sheet-gutter,
table.pp-sheet th.pp-sheet-th-plan_date,
table.pp-sheet td[data-col="plan_date"],
table.pp-sheet th.pp-sheet-th-title,
table.pp-sheet td[data-col="title"] {
    position: sticky;
    background: var(--color-surface);
    z-index: 2;
}
table.pp-sheet th.pp-sheet-gutter,
table.pp-sheet th.pp-sheet-th-plan_date,
table.pp-sheet th.pp-sheet-th-title { z-index: 4; }

/* Column geometry. Every selector below is scoped to th/td ON PURPOSE.
   buildRow() stamps data-col on the cell's <input> as well as on the
   cell, so a bare [data-col="..."] matches the input too. That is
   invisible for width while the input is position: static — but
   .pp-sheet-in:focus sets position: relative, which ACTIVATES the `left`
   below and shifted the focused What input 150px to the right, over the
   Type column (Dato had the same bug at its own 34px offset). Do not
   drop the th/td qualifier to shorten these. */
table.pp-sheet .pp-sheet-gutter { left: 0; width: 34px; min-width: 34px; }
table.pp-sheet th[data-col="plan_date"],
table.pp-sheet td[data-col="plan_date"] { left: 34px;  width: 116px; min-width: 116px; }
table.pp-sheet th[data-col="title"],
table.pp-sheet td[data-col="title"] { left: 150px; width: 220px; min-width: 220px; }
/* A visible edge where the frozen block ends, so a scrolled column does
   not look like it belongs to the frozen pair. */
table.pp-sheet th[data-col="title"],
table.pp-sheet td[data-col="title"] { border-right: 1px solid var(--color-border-strong); }

table.pp-sheet th[data-col="plan_time"],
table.pp-sheet td[data-col="plan_time"],
table.pp-sheet th[data-col="deadline_time"],
table.pp-sheet td[data-col="deadline_time"] { width: 76px;  min-width: 76px; }
table.pp-sheet th[data-col="priority"],
table.pp-sheet td[data-col="priority"],
table.pp-sheet th[data-col="status"],
table.pp-sheet td[data-col="status"] { width: 120px; min-width: 120px; }
table.pp-sheet th[data-col="type_id"],
table.pp-sheet td[data-col="type_id"] { width: 130px; min-width: 130px; }
table.pp-sheet th[data-col="need"],
table.pp-sheet td[data-col="need"] { width: 140px; min-width: 140px; }
table.pp-sheet th[data-col="comment"],
table.pp-sheet td[data-col="comment"] { width: 180px; min-width: 180px; }
table.pp-sheet th[data-col="locations"],
table.pp-sheet td[data-col="locations"],
table.pp-sheet th[data-col="responsibles"],
table.pp-sheet td[data-col="responsibles"],
table.pp-sheet th[data-col="departments"],
table.pp-sheet td[data-col="departments"],
table.pp-sheet th[data-col="suppliers"],
table.pp-sheet td[data-col="suppliers"],
table.pp-sheet th[data-col="labels"],
table.pp-sheet td[data-col="labels"] { width: 160px; min-width: 160px; }

.pp-sheet-gutter {
    text-align: center;
    padding: 0 2px;
    color: var(--color-text-muted);
    font-size: var(--font-size-xs);
    font-variant-numeric: tabular-nums;
    user-select: none;
}
.pp-sheet-gutter-r { width: 30px; min-width: 30px; text-align: center; border-right: 0; }
.pp-sheet-del {
    display: block; width: 100%;
    background: none; border: 0; cursor: pointer;
    color: rgba(0, 0, 0, 0.35);
    font-size: 1rem; line-height: 1; padding: 2px 0;
    text-align: center;
}
.pp-sheet-del:hover { color: var(--color-danger); }

/* The cell IS the input. No separate display/edit modes: a click lands
   in a live field, which is the whole point of a grid. */
.pp-sheet-in,
table.pp-sheet .hrc-di-text {
    width: 100%;
    border: 0;
    background: transparent;
    padding: 5px 7px;
    font: inherit;
    color: var(--color-text);
    border-radius: 0;
    min-width: 0;
}
.pp-sheet-in:focus,
table.pp-sheet .hrc-di-text:focus {
    outline: 0;
    /* The same focus signal the rest of the app uses (full-strength
       black border plus a faint ring), drawn INSIDE the cell so it does
       not shift the grid by a pixel. */
    box-shadow: inset 0 0 0 2px var(--color-text), 0 0 0 2px rgba(0, 0, 0, 0.05);
    background: var(--color-surface);
    position: relative;
    z-index: 1;
}
/* date-input.js wraps the native control; the wrapper must fill the
   cell or the picker button falls outside it. */
table.pp-sheet .hrc-di { display: flex; width: 100%; align-items: center; }
table.pp-sheet .hrc-di-btn { flex: 0 0 auto; padding: 0 4px; }

/* ── Row states ───────────────────────────────────────────── */
.pp-sheet-row td { transition: background-color .18s ease; }

.pp-sheet-row.is-empty      td { background: var(--color-surface); }
.pp-sheet-row.is-incomplete td { background: rgba(0, 0, 0, 0.018); }
.pp-sheet-row.is-valid      td { background: rgba(38, 138, 78, 0.09); }
.pp-sheet-row.is-invalid    td { background: rgba(203, 60, 45, 0.06); }

/* The left edge carries the state as well as the tint, so the four
   states stay distinguishable without relying on colour alone. */
.pp-sheet-row.is-valid      .pp-sheet-gutter { box-shadow: inset 3px 0 0 rgba(38, 138, 78, 0.65); }
.pp-sheet-row.is-invalid    .pp-sheet-gutter { box-shadow: inset 3px 0 0 rgba(203, 60, 45, 0.75); }
.pp-sheet-row.is-incomplete .pp-sheet-gutter { box-shadow: inset 3px 0 0 rgba(0, 0, 0, 0.14); }

/* Sticky cells re-declare the tint: they paint their own background to
   cover the columns scrolling underneath, so the row rule alone would
   leave the frozen pair uncoloured. */
.pp-sheet-row.is-incomplete td[data-col="plan_date"],
.pp-sheet-row.is-incomplete td[data-col="title"],
.pp-sheet-row.is-incomplete .pp-sheet-gutter { background: #fafafa; }
.pp-sheet-row.is-valid td[data-col="plan_date"],
.pp-sheet-row.is-valid td[data-col="title"],
.pp-sheet-row.is-valid .pp-sheet-gutter { background: #eef6f0; }
.pp-sheet-row.is-invalid td[data-col="plan_date"],
.pp-sheet-row.is-invalid td[data-col="title"],
.pp-sheet-row.is-invalid .pp-sheet-gutter { background: #fdf1ef; }

/* ── Cell states ──────────────────────────────────────────── */
.pp-sheet-cell.is-cell-invalid { box-shadow: inset 0 0 0 1.5px rgba(203, 60, 45, 0.7); }
/* Missing-required is a faint hint, not an error box: an unfinished row
   is the normal state of a grid in use. */
.pp-sheet-cell.is-cell-missing { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.16); }
/* Free-text value: accepted by the domain, worth flagging because it
   will not link to a registered supplier or an existing label. */
.pp-sheet-cell.is-cell-free    { box-shadow: inset 0 0 0 1px rgba(196, 138, 20, 0.55); }

/* ── Fill handle ────────────────────────── */
/* The spreadsheet gesture: a small square in the focused cell's corner,
   dragged down (or up) to copy that cell into the ones it sweeps.
   Absolutely positioned INSIDE the cell, so it scrolls with the grid
   and never needs repositioning.

   position: relative is set at low specificity ON PURPOSE: the frozen
   Date and What columns are position: sticky through a much more
   specific selector, and must stay that way. Sticky is also a
   containing block for an absolute child, so the handle works in both
   kinds of cell without this rule ever having to win. */
.pp-sheet-cell { position: relative; }
.pp-sheet-fill {
    position: absolute;
    right: 0;
    bottom: 0;
    width: 7px;
    height: 7px;
    background: var(--color-text);
    border-radius: 1px;
    box-shadow: 0 0 0 1px var(--color-surface);
    cursor: crosshair;
    /* Above the focused input, which lifts itself to z-index 1. */
    z-index: 3;
    /* The drag is ours; without this the touch scrolls the grid. */
    touch-action: none;
}
/* Sweep preview. Specific enough to beat the row-state background rules
   (.pp-sheet-row.is-valid td and the sticky re-declarations), which
   would otherwise repaint over it. */
table.pp-sheet td.pp-sheet-cell.is-fill-target {
    background: rgba(0, 0, 0, 0.06);
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
}

.pp-sheet-row.has-server-error td[data-col="title"] { box-shadow: inset 0 0 0 1.5px rgba(203, 60, 45, 0.8); }

.pp-sheet-errors {
    margin-top: var(--space-sm);
    padding: 8px 10px;
    border-radius: var(--radius-md);
    font-size: var(--font-size-sm);
    background: rgba(203, 60, 45, 0.08);
    border: 1px solid rgba(203, 60, 45, 0.3);
    color: var(--color-text);
}
.pp-sheet-errors.is-ok {
    background: rgba(38, 138, 78, 0.10);
    border-color: rgba(38, 138, 78, 0.35);
}

/* Suggestion menu: the platform dropdown, narrowed for a grid cell. */
.pp-sheet-dd { font-size: var(--font-size-sm); }
.pp-sheet-dd .hrc-picker-row { padding: 5px 8px; }

/* Screen-reader-only label, scoped to this view rather than added as a
   global utility: the app has no shared one, and introducing one here
   would be a project-wide change nobody asked for. */
table.pp-sheet .pp-sheet-sr {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

/* Phones. The grid stays a grid: restacking 13 columns into a card
   would destroy the one thing this view is for. It scrolls instead,
   with the frozen pair keeping the row identifiable. */
@media (max-width: 640px) {
    .pp-sheet-legend .pp-sheet-hint { margin-left: 0; width: 100%; }
    .pp-sheet-actions { width: 100%; justify-content: space-between; }
    table.pp-sheet th[data-col="plan_date"],
    table.pp-sheet td[data-col="plan_date"] { width: 104px; min-width: 104px; }
    table.pp-sheet th[data-col="title"],
    table.pp-sheet td[data-col="title"] { width: 160px; min-width: 160px; left: 138px; }
}

/* The Sheet is a staging surface with nothing worth printing. */
@media print {
    .pp-sheet-card { display: none; }
}


/* ============================================================
   Help pages (workspace/_help.php) — embedded product manual.
   Two-column: sticky anchor TOC ("In this guide" + "Related
   help") beside a readable-width article. Sections are headings
   and whitespace, not stacked cards. Below 1024px the TOC
   collapses to a native <details> selector above the article.
   ============================================================ */
.help-layout {
    display: grid;
    grid-template-columns: 240px minmax(0, 1fr);
    gap: var(--space-xl);
    /* No align-items: start here — the aside cell must stretch to the
       full row height or the sticky TOC has no room to travel and
       silently never sticks. */
}
.help-toc {
    position: sticky;
    top: calc(var(--topbar-height) + var(--space-md));
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 8px;
    padding: var(--space-md);
}
.help-toc-title {
    font-size: var(--font-size-xs, 0.75rem);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
    list-style: none;
    cursor: default;
}
.help-toc-title::-webkit-details-marker { display: none; }
.help-toc-body {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin-top: var(--space-sm);
}
.help-toc-link {
    display: block;
    padding: 4px 8px;
    border-radius: 6px;
    font-size: var(--font-size-sm, 0.875rem);
    color: var(--color-text);
    text-decoration: none;
    line-height: 1.4;
}
.help-toc-link:hover { background: #ebebeb; }
.help-toc-heading {
    font-size: var(--font-size-xs, 0.75rem);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
    margin: var(--space-md) 0 var(--space-xs);
}
.help-toc-related { color: var(--color-text-muted); }

.help-article {
    max-width: 760px;
    min-width: 0;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 8px;
    padding: var(--space-xl);
}
.help-section {
    scroll-margin-top: calc(var(--topbar-height) + var(--space-md));
    margin-bottom: var(--space-xl);
}
.help-section:last-child { margin-bottom: 0; }
.help-section-heading {
    font-size: 1.2rem;
    font-weight: 650;
    margin: 0 0 var(--space-sm);
    padding-bottom: var(--space-xs);
    border-bottom: 1px solid var(--color-border);
}
.help-p {
    margin: 0 0 var(--space-sm);
    line-height: 1.65;
}
.help-subhead {
    font-weight: 600;
    margin: var(--space-md) 0 var(--space-xs);
}
.help-list {
    margin: 0 0 var(--space-sm) var(--space-md);
    line-height: 1.8;
    padding-left: var(--space-sm);
}
.help-list li { margin-bottom: 2px; }
.help-table { margin-bottom: var(--space-sm); }
.help-faq-item { margin-bottom: var(--space-md); }
.help-faq-item:last-child { margin-bottom: 0; }
.help-faq-q { font-weight: 600; margin-bottom: var(--space-xs); }
.help-faq-a { margin: 0; line-height: 1.65; color: var(--color-text-muted); }
.help-section .help-p:last-child,
.help-section .help-list:last-child { margin-bottom: 0; }

/* Relationship flow: chips joined by the house ➜ glyph. */
.help-flow {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs);
    margin: var(--space-sm) 0 var(--space-md);
}
.help-flow-item {
    display: inline-block;
    padding: 3px 10px;
    border: 1px solid var(--color-border-strong);
    border-radius: 999px;
    background: var(--color-bg);
    font-size: var(--font-size-sm, 0.875rem);
    font-weight: 500;
    white-space: nowrap;
}
.help-flow-sep { color: var(--color-text-muted); }

/* Compact callout for load-bearing notes. */
.help-callout {
    border-left: 3px solid var(--color-border-strong);
    background: var(--color-bg);
    border-radius: 0 6px 6px 0;
    padding: var(--space-sm) var(--space-md);
    margin: 0 0 var(--space-md);
    line-height: 1.6;
}

/* Disclosure for reference material that should sit low-key. */
.help-details {
    border: 1px solid var(--color-border);
    border-radius: 8px;
    margin: 0 0 var(--space-md);
}
.help-details > summary {
    padding: var(--space-sm) var(--space-md);
    font-weight: 600;
    cursor: pointer;
}
.help-details-body { padding: 0 var(--space-md) var(--space-md); }

@media (max-width: 1023px) {
    .help-layout { grid-template-columns: minmax(0, 1fr); gap: var(--space-md); }
    .help-toc { position: static; }
    .help-toc-title {
        cursor: pointer;
        color: var(--color-text);
    }
    .help-toc-title::after { content: ' ▾'; color: var(--color-text-muted); }
    .help-toc[open] .help-toc-title::after { content: ' ▴'; }
    .help-article { padding: var(--space-md); }
}


/* ── Deliveries: multi-item parent row + its nested plan items ──
   Vehicles > Leveringer lists DELIVERIES, one row each. A delivery
   carrying more than one plan item stays ONE row until the operator
   asks what is on it, so the expander is deliberately a quiet inline
   control in the secondary line beside the reference, not a caret
   column: a column would spend width on every row to serve the few
   that are multi-item. */
.del-expand {
    background: none; border: 0; padding: 0;
    font: inherit; color: var(--color-text);
    text-decoration: underline; cursor: pointer;
}
.del-expand:hover { opacity: .7; }
/* The nested rows are the SAME table row shape, tinted and indented
   rather than restyled — they are ordinary plan items, and giving them
   their own visual language would suggest they are sub-records of the
   delivery, which is exactly what they are not. */
.del-item-row > td { background: rgba(0, 0, 0, 0.02); }
@media (max-width: 640px) {
    /* On the card layout there is no indent to inherit, so the child
       card is marked by its rule instead. */
    .del-item-row { border-left: 3px solid var(--color-border); }
}


/* ── Production Plan: the delivery reference on a row ──────────
   Every plan row stays an ordinary row in plan order; a row that
   belongs to a delivery just names it in the sub-line. When the
   delivery is SHARED — two or more rows arriving on the same movement
   — a link glyph precedes the reference, so "this travels with
   something else" is visible without opening anything.

   The glyph is the only difference between a shared and a solo
   delivery here. That is deliberate: a plan row is not the place to
   restate the whole delivery, and the modal is one click away. */
.pp-delivery-link {
    display: inline-flex; align-items: center; gap: 3px;
    color: inherit;
}
a.pp-delivery-link:hover { color: var(--color-text); }
.pp-delivery-link.is-shared { font-weight: 600; }
.pp-delivery-icon {
    /* Sized off the surrounding sub-line rather than fixed, so it keeps
       its proportion if the cell's type scale ever changes. */
    font-size: 1.05em; line-height: 1;
}
@media (pointer: coarse) {
    /* A reference on a phone is a tap target inside a dense cell. */
    a.pp-delivery-link { padding: 2px 0; min-height: 24px; }
}
