/*
 * Bodyshop design system — global tokens, self-hosted fonts, base typography.
 * Bootstrap 5 is imported before this (in app.js); these rules layer the brand on top.
 * Page-specific layout lives in its own file (e.g. auth.css).
 */

@font-face {
    font-family: 'Bebas Neue';
    src: url("../fonts/bebas-neue-400-kCS-Go6.woff2") format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: 'Archivo';
    src: url("../fonts/archivo-400-6zzj1o8.woff2") format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: 'Archivo';
    src: url("../fonts/archivo-600-som1JOa.woff2") format('woff2');
    font-weight: 600;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: 'Archivo';
    src: url("../fonts/archivo-700-NpNxM9d.woff2") format('woff2');
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

:root {
    /* Workshop palette */
    --bs-ink: #15171a;          /* tyre black / backdrop */
    --bs-ink-2: #0e0f11;        /* deepest */
    --bs-steel: #21252b;        /* raised panel */
    --bs-steel-2: #2b3037;
    --bs-hi-vis: #00de91;       /* signature accent */
    --bs-hi-vis-press: #00bd7b;
    --hero-accent: #00b3ff;     /* the always-dark hero KPI card's number (blue in both themes) */

    /* Good / warning / bad, for anything that says "this moved the right way". These were only ever
       declared inside .workshop-tv, so every use outside that board — the KPI tiles' and detail
       pages' up/down deltas, and the benchmark's winning cell — resolved to nothing and rendered in
       plain body text: a green rise and a red fall looked identical (found in the browser on
       RSPH-143, computed colour rgb(29,33,38) for both). These match the semantic figures already in
       use on .kpi-stat__n--green/amber/red; the TV board keeps its own brighter set. */
    --rag-green: #1a7f3c;
    --rag-amber: #e8a400;
    --rag-red: #b3261e;

    /* Hover affordance (see the block at the end of this file). Resolved per theme so every
       "this is clickable" rule is written once instead of once per theme. */
    --hover-ring: var(--bs-hi-vis);
    --hover-lift: 0 6px 18px -10px rgba(16,24,40,0.2);
    --hover-tint: #fafbfc;
    --bs-paper: #fcfbf9;        /* card */
    --table-scroll-cover: #fff; /* solid edge-mask for the wide-table scroll shadow; must match the panel bg behind the table */
    --bs-line: #c9ced6;         /* hairline */
    --bs-muted: #6b7280;
    --bs-text: #1d2126;
    --due-amber: #9a6700;       /* deadline urgency: 2-3 days out */
    --due-red: #b3261e;         /* deadline urgency: 1 day / due today / overdue */

    --bs-font-display: 'Bebas Neue', 'Arial Narrow', system-ui, sans-serif;
    --bs-font-ui: 'Archivo', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
    --bs-font-mono: ui-monospace, 'SFMono-Regular', 'Cascadia Code', 'JetBrains Mono', monospace;

    --bs-body-font-family: var(--bs-font-ui);
}

body {
    font-family: var(--bs-font-ui);
    color: var(--bs-text);
}

/* A reusable workshop micro-label: monospace, uppercase, tracked — the WIP/reg-number voice. */
.eyebrow {
    font-family: var(--bs-font-mono);
    text-transform: uppercase;
    letter-spacing: 0.18em;
    font-size: 0.72rem;
    color: var(--bs-muted);
}

/* ---- Authenticated app shell ---- */
.shell { display: flex; min-height: 100vh; }
.shell__main { flex: 1; min-width: 0; }

.sidebar {
    width: 248px;
    flex-shrink: 0;
    position: sticky;
    top: 0;
    align-self: flex-start;
    height: 100vh;
    background: var(--bs-ink);
    color: #eef1f5;
    border-inline-end: 3px solid var(--bs-hi-vis);
    display: flex;
    flex-direction: column;
}

.sidebar__brand {
    font-family: var(--bs-font-display);
    font-size: 1.7rem;
    line-height: 1;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: #fff;
    text-decoration: none;
    padding: 1.15rem 1.35rem;
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.sidebar__logo { display: block; height: 48px; width: auto; }
@media (max-width: 860px) { .sidebar__logo { height: 40px; } }

/* The tenant's own platform logo beneath the product mark (RSPH-150). Rendered only when uploaded, so this
   block never applies to a tenant that has not set one — there is no empty state to style.

   Tenants upload whatever they have: a wide horizontal wordmark, a square badge, a tall crest. The
   box is therefore a bounded area rather than a fixed size, and the image is contained inside it —
   `cover` would crop someone's logo, which is worse than showing it small.

   NO PLATE. The logo meets the sidebar directly, which is what a transparent logo is for — the white
   box this used to sit on read as a sticker stuck onto the sidebar rather than as part of it. What
   makes that safe is that this is its own upload (TenantConfiguration::$platformLogoStorageKey),
   separate from the letterhead logo, so a workshop supplies artwork drawn for a dark surface rather
   than a print file we have to rescue. Workshop settings previews it on this exact colour. */
.sidebar__tenant {
    padding: 0.9rem 1.35rem;
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.sidebar__tenant-logo {
    display: block;
    max-height: 44px;
    max-width: 100%;
    width: auto;
    object-fit: contain;
}
@media (max-width: 860px) {
    /* The sidebar becomes a horizontal bar; the emblem keeps its place beside the product mark but
       gives up height with it. */
    .sidebar__tenant { padding: 0.8rem 1rem; border-bottom: none; }
    .sidebar__tenant-logo { max-height: 34px; }
}

/* The logos on file, previewed on the workshop-settings screen. Two tiles, because the two places a
   logo lands are opposite colours: white paper and the dark sidebar. Each tile carries the surface
   its own upload lands on, so "will this be readable there?" is answered by looking rather than by
   imagining — the --dark tile is literally --bs-ink, the same value the sidebar uses. Without it, a
   workshop that uploaded a dark-ink platform logo would only find out in the sidebar. */
.logo-preview {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin: 0 0 0.75rem;
}
.logo-preview__img {
    display: block;
    max-height: 56px;
    max-width: 220px;
    width: auto;
    object-fit: contain;
    background: #fff;
    border: 1px solid var(--bs-line);
    border-radius: 8px;
    padding: 0.4rem 0.55rem;
}
.logo-preview__img--dark { background: var(--bs-ink); border-color: var(--bs-ink); }
.logo-preview__note { color: var(--bs-muted); font-size: 0.85rem; margin: 0 0 0.5rem; }

/* Site scope switcher (group managers only): a compact disclosure below the brand. Native <details>
   so it works without JS; the summary shows the current scope, the menu lists sites + "All sites". */
.site-switch { border-bottom: 1px solid rgba(255, 255, 255, 0.08); }
.site-switch__current {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    padding: 0.7rem 1.35rem;
    cursor: pointer;
    list-style: none;
    color: #eef1f5;
}
.site-switch__current::-webkit-details-marker { display: none; }
.site-switch__current::after {
    content: "";
    position: absolute;
    inset-inline-end: 1.2rem;
    width: 0.5rem;
    height: 0.5rem;
    margin-top: 0.55rem;
    border-inline-end: 2px solid #aeb6c2;
    border-bottom: 2px solid #aeb6c2;
    transform: rotate(45deg);
}
.site-switch { position: relative; }
.site-switch[open] .site-switch__current::after { transform: rotate(-135deg); margin-top: 0.75rem; }
.site-switch__eyebrow {
    font-size: 0.66rem;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: #8b93a1;
}
.site-switch__value { font-weight: 600; font-size: 0.95rem; }
.site-switch__menu { padding: 0.25rem 0 0.55rem; }
.site-switch__menu form { margin: 0; }
.site-switch__opt {
    display: block;
    width: 100%;
    padding: 0.45rem 1.35rem;
    font: inherit;
    font-size: 0.9rem;
    font-weight: 600;
    text-align: start;
    color: #aeb6c2;
    background: none;
    border: none;
    border-inline-start: 3px solid transparent;
    cursor: pointer;
}
.site-switch__opt:hover { color: #fff; background: rgba(255, 255, 255, 0.04); }
.site-switch__opt.is-active { color: #fff; border-inline-start-color: var(--bs-hi-vis); background: rgba(0, 222, 145, 0.10); }

.sidebar__nav { flex: 1; overflow-y: auto; padding: 0.5rem 0; }
.sidebar__group { padding: 0.35rem 0; }
.sidebar__group + .sidebar__group { border-top: 1px solid rgba(255, 255, 255, 0.05); }
/* Nav rows — plain links (Dashboard, Reports, Help) and collapsible group headings
   (buttons) share ONE look; the chevron is the only differentiator on expandable groups. */
.sidebar__nav a,
.sidebar__nav button.sidebar__label {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    padding: 0.5rem 1.35rem;
    font-family: inherit;
    font-size: 0.92rem;
    font-weight: 600;
    color: #aeb6c2;
    text-decoration: none;
    text-align: start;
    background: none;
    border: none;
    border-inline-start: 3px solid transparent;
    cursor: pointer;
}
.sidebar__nav a:hover,
.sidebar__nav button.sidebar__label:hover { color: #fff; background: rgba(255, 255, 255, 0.04); }
.sidebar__nav a.is-active { color: #fff; border-inline-start-color: var(--bs-hi-vis); background: rgba(0, 222, 145, 0.10); }
.sidebar__nav button.sidebar__label:focus-visible { outline: 2px solid var(--bs-hi-vis); outline-offset: -2px; }

/* Expanded child items get a leading dot to distinguish them from top-level rows. */
.sidebar__submenu a::before {
    content: "";
    flex-shrink: 0;
    width: 4px;
    height: 4px;
    border-radius: 50%;
    background: currentColor;
    opacity: 0.55;
}

/* Chevron: the only differentiator on an expandable heading. Points right, rotates down when open. */
.sidebar__chevron {
    width: 0;
    height: 0;
    flex-shrink: 0;
    margin-inline-start: auto;
    border-top: 3.5px solid transparent;
    border-bottom: 3.5px solid transparent;
    border-inline-start: 4px solid currentColor;
    transition: transform 0.18s ease;
}
.sidebar__group.is-open .sidebar__chevron { transform: rotate(90deg); }

/* Collapse only applies once the controller marks the nav JS-enhanced, so no-JS renders open. */
.sidebar-nav--js .sidebar__submenu {
    overflow: hidden;
    max-height: 0;
    transition: max-height 0.22s ease;
}
.sidebar-nav--js .sidebar__group.is-open .sidebar__submenu { max-height: 40rem; }

/* The current section's heading text goes hi-vis: the group of the active page, or a dropdown you
   click open — but NOT one merely restored open from memory on a group-less page (e.g. Dashboard). */
.sidebar__group.is-current > button.sidebar__label { color: var(--bs-hi-vis); }

.sidebar__foot {
    border-top: 1px solid rgba(255, 255, 255, 0.08);
    padding: 0.9rem 1.35rem;
    display: flex;
    flex-direction: column;
    gap: 0.55rem;
}
.sidebar__bell {
    display: inline-flex; align-items: center; gap: 0.4rem;
    font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
    color: #aeb6c2; text-decoration: none;
}
.sidebar__bell:hover, .sidebar__bell.is-active { color: #fff; }
.sidebar__badge {
    background: var(--bs-hi-vis); color: var(--bs-ink-2);
    font-family: var(--bs-font-mono); font-size: 0.66rem; font-weight: 700;
    border-radius: 999px; padding: 0.05rem 0.4rem; line-height: 1.4;
}
/* The platform console still identifies an admin by login address, so the mono/break-all treatment
   that suits an email stays. The app shell names a person: normal type, and wrapping on spaces. */
.sidebar__email { font-family: var(--bs-font-mono); font-size: 0.72rem; letter-spacing: 0.05em; color: #7e879a; word-break: break-all; }
.sidebar__user { font-size: 0.82rem; font-weight: 600; color: var(--bs-hi-vis); overflow-wrap: break-word; }
/* The branch, under the name. Deliberately the small mono caps the sidebar already uses for secondary
   metadata, so the pair reads as one block: who you are, then where you are.
   Green name over blue branch, taken from the palette tokens rather than fresh hexes so they cannot
   drift from the house colours. Both are safe here despite the sidebar being dark in both themes:
   --bs-hi-vis is re-tokenised only inside .workshop-tv, which has no sidebar, and --hero-accent is
   declared once and is blue in both. One value each for the same reason — the sidebar ground is
   #15171a whichever theme is selected, so a theme-conditional rule would be dead code. */
.sidebar__site {
    font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.1em;
    font-size: 0.62rem; color: var(--hero-accent); margin-top: -0.25rem; overflow-wrap: break-word;
}
/* Sign out and the theme icons share a line, the icons pushed to the far edge. */
.sidebar__actions { display: flex; align-items: center; gap: 0.5rem; }
.sidebar__actions .theme-toggle { margin-inline-start: auto; }
.sidebar__signout-body { margin: 0; color: var(--bs-text-secondary, #667085); }
.sidebar__signout {
    align-self: flex-start;
    font-size: 0.78rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
    color: var(--bs-hi-vis); text-decoration: none;
    border: 1.5px solid rgba(0, 222, 145, 0.45); border-radius: 8px; padding: 0.3rem 0.65rem;
}
.sidebar__signout:hover { background: rgba(0, 222, 145, 0.12); }

/* ---- Narrow-screen shell: an app bar + the sidebar as an off-canvas drawer ----
 *
 * What this replaced: below 860px the sidebar used to turn into a horizontal strip with every group's
 * links flattened into it (.sidebar__submenu { display: contents }), scrolling sideways. Two things
 * went wrong with that. The group headings never actually hid — `.sidebar__nav button.sidebar__label`
 * (0,2,1) outranks the `button.sidebar__label { display: none }` (0,1,1) meant to hide them — so every
 * heading AND all ~35 links rendered in one row: 5,700px wide on a 390px phone, and because the strip
 * is a flex child the whole document grew with it rather than scrolling inside the nav. So the page
 * itself ran off the screen sideways, not just the menu.
 *
 * The fix isn't a narrower strip, it's the drawer the nav was already built for: the accordion groups
 * are what keep a 35-link menu short, and flattening them threw that away. So the drawer IS the
 * sidebar — same markup, same groups, same accordion, moved off-screen behind a hamburger. There is no
 * second, phone-only link list to drift out of step with the real one.
 *
 * The app bar and scrim are display:none by default and only exist inside the media query below, so
 * nothing here reaches the desktop layout. */
.appbar { display: none; }
.appbar__scrim { display: none; }

/* Freeze the page under an open drawer. Set on <html> rather than <body> because Turbo swaps <body>
   out on navigation, which would strand the lock. */
html.nav-open { overflow: hidden; }

/* One convention through this whole block: logical properties on the inline axis, which has to mirror
   under dir="rtl", and physical top/bottom on the block axis, which doesn't. Mixing a logical override
   against a physical base is what broke the drawer's height the first time round — see .sidebar below. */
@media (max-width: 860px) {
    /* The bar is fixed, so the shell reserves its height rather than flowing beneath it. */
    .shell { display: block; padding-top: 56px; }

    .appbar {
        position: fixed;
        top: 0;
        inset-inline: 0;
        z-index: 60;
        height: 56px;
        display: flex;
        align-items: center;
        gap: 0.15rem;
        padding-inline: 0.3rem 1rem;
        background: var(--bs-ink);
        /* The same 3px hi-vis rule the sidebar carries on its edge — the shell's one graphic device,
           rotated to suit the bar. */
        border-bottom: 3px solid var(--bs-hi-vis);
    }

    .appbar__burger {
        position: relative;
        flex-shrink: 0;
        width: 44px;
        height: 44px;
        display: grid;
        place-items: center;
        padding: 0;
        background: none;
        border: none;
        cursor: pointer;
        -webkit-tap-highlight-color: transparent;
    }
    .appbar__burger:focus-visible { outline: 2px solid var(--bs-hi-vis); outline-offset: -4px; border-radius: 8px; }

    /* Three absolutely-positioned rules in a fixed 22×14 box: fixed geometry is what lets them rotate
       into a cross about a known centre. The middle one is hi-vis — the bar's own rule, in miniature. */
    .appbar__bars { display: block; position: relative; width: 22px; height: 14px; }
    .appbar__bars span {
        position: absolute;
        inset-inline: 0;
        height: 2px;
        border-radius: 1px;
        background: #eef1f5;
        transition: transform 0.22s ease, opacity 0.14s ease, background-color 0.22s ease;
    }
    .appbar__bars span:nth-child(1) { inset-block-start: 0; }
    .appbar__bars span:nth-child(2) { inset-block-start: 6px; background: var(--bs-hi-vis); }
    .appbar__bars span:nth-child(3) { inset-block-start: 12px; }

    /* Open: the outer two meet in the middle and cross, the hi-vis one fades out from under them —
       and the cross takes its colour, so the accent moves with the state instead of vanishing. */
    .shell.is-nav-open .appbar__bars span:nth-child(1) { transform: translateY(6px) rotate(45deg); background: var(--bs-hi-vis); }
    .shell.is-nav-open .appbar__bars span:nth-child(2) { opacity: 0; }
    .shell.is-nav-open .appbar__bars span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); background: var(--bs-hi-vis); }

    /* Anything waiting in the drawer's Alerts/Messages rows. A count behind a hamburger is a count
       nobody sees; the dot says "there is something", the rows still give the number. */
    .appbar__dot {
        position: absolute;
        inset-block-start: 7px;
        inset-inline-end: 7px;
        width: 8px;
        height: 8px;
        border-radius: 50%;
        background: var(--bs-hi-vis);
        box-shadow: 0 0 0 2px var(--bs-ink);
    }

    .appbar__brand { display: flex; align-items: center; min-width: 0; text-decoration: none; }
    .appbar__logo { display: block; height: 30px; width: auto; }

    /* ---- The drawer ---- */
    .sidebar {
        position: fixed;
        /* An explicit height, and physical `top` rather than the logical `inset-block` shorthand. The
           base .sidebar rule sets `top: 0` and `height: 100vh`; overriding those with `inset-block:
           56px 0` and `height: auto` left the panel at CONTENT height instead of viewport height, so
           the nav never scrolled and the foot (your name, sign out, the theme icons) sat below the fold
           on a short screen with no way to reach it. Stating the height outright settles it either way.
           100dvh, not 100vh, so a phone's collapsing address bar doesn't push the foot off. */
        top: 56px;
        height: calc(100dvh - 56px);
        inset-inline-start: 0;
        z-index: 55;
        width: min(320px, 86vw);
        transform: translateX(-100%);
        /* Hidden, not merely off-screen: a translated panel still takes Tab focus and still reads to a
           screen reader. The zero-duration visibility step is delayed until the slide finishes so the
           panel doesn't blink out mid-transition. */
        visibility: hidden;
        transition: transform 0.24s ease, visibility 0s linear 0.24s;
    }
    [dir="rtl"] .sidebar { transform: translateX(100%); }
    .shell.is-nav-open .sidebar {
        transform: translateX(0);
        visibility: visible;
        transition: transform 0.24s ease, visibility 0s;
    }
    .sidebar:focus { outline: none; }

    /* The app bar above already carries the product mark, so the drawer doesn't repeat it. The
       tenant's own emblem is not a duplicate and stays at the top of the panel. */
    .sidebar__brand { display: none; }

    .appbar__scrim {
        display: block;
        position: fixed;
        top: 56px;
        bottom: 0;
        inset-inline: 0;
        z-index: 50;
        background: rgba(8, 10, 12, 0.55);
        opacity: 0;
        visibility: hidden;
        transition: opacity 0.24s ease, visibility 0s linear 0.24s;
    }
    .shell.is-nav-open .appbar__scrim {
        opacity: 1;
        visibility: visible;
        transition: opacity 0.24s ease, visibility 0s;
    }
    /* A near-black wash over an already-dark page barely reads as "this is inactive", so the dark
       theme leans harder on it. The panel itself is the same ink in both themes. */
    [data-bs-theme="dark"] .appbar__scrim { background: rgba(0, 0, 0, 0.7); }

    /* Nav rows are touch targets here, not pointer targets. */
    .sidebar__nav a,
    .sidebar__nav button.sidebar__label { padding-block: 0.7rem; }
    .site-switch__opt { padding-block: 0.6rem; }

    /* The assistant sits at z-index 1075 so transient toasts can still reach over it — which also
       puts it above the scrim, hovering brightly over a page the drawer has just dimmed. It goes
       away while the drawer is open and comes straight back when it closes. */
    .shell.is-nav-open .askrs { display: none; }
}

/* No slide, no fade, no morph — the drawer is simply there or not. Scoped to the same breakpoint so
   it can't reach the desktop sidebar, which has no transitions to suppress. */
@media (max-width: 860px) and (prefers-reduced-motion: reduce) {
    .appbar__bars span,
    .sidebar,
    .shell.is-nav-open .sidebar,
    .appbar__scrim,
    .shell.is-nav-open .appbar__scrim { transition: none; }
}

/* Notifications list */
.notif-list { list-style: none; margin: 0; padding: 0; }
/* The unread wash is a square box painted right to the panel edge, so with nothing clipping it the
   first and last rows square off the panel's 14px corners. Long-standing, but invisible while the
   wash was near-white. Rounds the flush child instead of putting overflow on .panel itself, matching
   the .table-scroll pair further down — .panel's shadow sits outside its box. */
.panel > .notif-list { border-radius: 14px; overflow: hidden; }
.notif { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.85rem 1rem; border-bottom: 1px solid #eef0f3; }
.notif:last-child { border-bottom: none; }
/* Unread wash. Blue on the light canvas, and deliberately NOT amber: the type badges next to it now
   carry meaning (amber = needs attention, red = already wrong), so an amber row read as a second,
   contradictory severity signal. Blue is the one tone in the chip set that says "new" without
   claiming a health rating. Kept well lighter than badge-flag--blue (#cfe8fa) so a blue chip still
   separates from the row it sits on. Shared with the messages list, which wants the same "unread". */
.notif--unread { background: #ecf5fd; }
.notif__main { display: flex; align-items: center; gap: 0.7rem; flex: 1 1 auto; min-width: 0; }
.notif__when { font-family: var(--bs-font-mono); font-size: 0.66rem; letter-spacing: 0.06em; color: var(--bs-muted); white-space: nowrap; flex: 0 0 auto; }
.notif__delete { flex: 0 0 auto; display: flex; }

.app__main {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2.25rem 2rem;
}

.page-title {
    font-weight: 700;
    font-size: 1.7rem;
    letter-spacing: -0.01em;
    margin: 0.25rem 0 1.25rem;
}

/* App surface (auth/marketing pages cover this with their own backgrounds) */
body { background: #f5f6f8; }

/* Toolbar: title on the left, actions on the right */
.toolbar { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; margin-bottom: 1.25rem; flex-wrap: wrap; }
.toolbar .page-title { margin: 0.25rem 0 0; }
/* The toolbar's right-hand action cluster. This class was used by seven templates but never
   defined, so it fell back to a plain block: inline-block links sat on one line while any action
   wrapped in a <form> (Mark all read, and every POST action elsewhere) was pushed onto its own row,
   left-aligned under them. Buttons wrap as a group only when the row genuinely runs out of width. */
.toolbar__actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 0.5rem; }
/* A <form> here exists only to carry the CSRF token for a single button; it should not introduce a
   layout box of its own. */
.toolbar__actions > form { display: contents; }

/* Buttons */
.btn {
    display: inline-block;
    font-family: var(--bs-font-ui);
    font-weight: 600;
    font-size: 0.9rem;
    text-decoration: none;
    border: 1.5px solid transparent;
    border-radius: 9px;
    padding: 0.55rem 0.9rem;
    cursor: pointer;
    transition: background-color 0.12s ease, border-color 0.12s ease, transform 0.04s ease;
}
.btn:active { transform: translateY(1px); }
/* .btn sets `display`, and an author rule beats the UA's `[hidden] { display: none }` — so without
   this a hidden button stays on screen. Same reason .board-modal[hidden] and .board-toast[hidden]
   exist. The booking calendar's edit modal hides its "Open job" link for a slot (no job to open). */
.btn[hidden] { display: none; }
.btn--primary { background: var(--bs-hi-vis); color: var(--bs-ink-2); }
.btn--primary:hover { background: var(--bs-hi-vis-press); }
/* Secondary main action (brand blue, white text): a real action that sits alongside the primary
   button, distinct from white ghost buttons used for Cancel / navigation. */
.btn--secondary { background: #02b3fe; border-color: #02b3fe; color: #fff; }
.btn--secondary:hover { background: #0299d6; border-color: #0299d6; }
.btn--ghost { background: #fff; border-color: var(--bs-line); color: var(--bs-text); }
/* The brand blue of .btn--secondary, drawn rather than filled: a real action that has to stay
   subordinate to the filled button beside it. Used by Add Support on the job page's crew heading,
   where a filled blue would compete with Update Status a line above. `background: transparent`, not
   a colour, so the one rule serves both themes — the card behind it is what shows through. */
.btn--outline { background: transparent; border-color: #02b3fe; color: #02b3fe; }
.btn--outline:hover { background: rgba(2,179,254,0.1); border-color: #0299d6; color: #0299d6; }
.btn--outline:focus-visible { outline: 3px solid rgba(2,179,254,0.4); outline-offset: 2px; }
/* CRM claim action (exact brand cyan #00b3ff): the "New Claim" trigger and "Save & Proceed To
   Estimate". Distinct from .btn--secondary (#02b3fe) — the ticket specifies this precise hex. */
.btn--claim { background: #00b3ff; border-color: #00b3ff; color: #fff; }
.btn--claim:hover { background: #0098d6; border-color: #0098d6; }
.btn--claim:focus-visible { outline: 3px solid rgba(0,179,255,0.4); outline-offset: 2px; }
/* Import Estimate (exact #0211FF): a deep blue for the API/AI-PDF import entry point (currently a stub). */
.btn--import { background: #0211FF; border-color: #0211FF; color: #fff; }
.btn--import:hover { background: #020ecc; border-color: #020ecc; }
.btn--import:focus-visible { outline: 3px solid rgba(2,17,255,0.4); outline-offset: 2px; }
.btn--danger { background: #fff; border-color: #f0b4b4; color: #b3261e; }
.btn--danger:hover { background: #fdecec; }
/* Attention (not destructive): a required-but-missing action, e.g. a job with no team leader. */
.btn--alert { background: #e5341f; color: #fff; border-color: #e5341f; }
.btn--alert:hover { background: #c72817; border-color: #c72817; }
.btn--sm { padding: 0.4rem 0.65rem; font-size: 0.82rem; }

/* Native <dialog> modal (see modal_controller.js). Used for the on-card New Claim form.
   NOT named `.modal`: Bootstrap (loaded globally for the job-view tabs) defines its own
   component `.modal { position:fixed; display:none }`, and since that `display:none` is an
   author style it beats the UA `dialog[open]{display:block}` — so a `<dialog class="modal">`
   stays invisible even after showModal(). `.dialog-modal` keeps us clear of that collision. */
/* `white-space: normal` is not cosmetic: a dialog is positioned over the page but still INHERITS
   from wherever its markup sits, and a row-action dialog lives inside `.data-table__actions`, which
   sets `white-space: nowrap` for its buttons. Without this reset the dialog's prose renders as one
   unwrapped line and is clipped at the panel edge (hit on the surplus dialog, RSPH-137). */
.dialog-modal { border: none; border-radius: 14px; padding: 0; max-width: 720px; width: calc(100% - 2rem); box-shadow: 0 20px 50px rgba(16,24,40,0.25); background: #fff; color: var(--bs-text); white-space: normal; text-align: start; }
.dialog-modal::backdrop { background: rgba(16,24,40,0.45); }
.modal__panel { padding: 1.25rem 1.4rem 1.4rem; }
.modal__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; }
.modal__close { background: none; border: none; font-size: 1.6rem; line-height: 1; cursor: pointer; color: var(--bs-text-secondary, #667085); padding: 0 0.25rem; }
.modal__close:hover { color: var(--bs-text); }

/* The job page's Add-technician modal (crew_add_controller.js): a plain [hidden]-toggled div rather
   than a native <dialog>, so it gets none of the browser's top-layer centering or ::backdrop — this
   supplies both. `div.dialog-modal` (element+class) outranks the bare `.dialog-modal` rule above, so
   every property that would otherwise leak from the native-dialog styling (the card look, sized for a
   <dialog>) is reset here and moved onto .dialog-modal__card instead. */
div.dialog-modal { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; max-width: none; width: auto; padding: 1rem; border-radius: 0; box-shadow: none; background: rgba(16,24,40,0.45); }
div.dialog-modal[hidden] { display: none; }
.dialog-modal__card { background: #fff; color: var(--bs-text); border-radius: 14px; box-shadow: 0 20px 50px rgba(16,24,40,0.25); max-width: 420px; width: 100%; padding: 1.25rem 1.4rem 1.4rem; display: flex; flex-direction: column; gap: 0.65rem; }
.dialog-modal__title { margin: 0 0 0.25rem; font-size: 1.05rem; }
.dialog-modal__actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 0.5rem; }

/* The status form's crew dialog (_status_crew_modal.html.twig) is a real <dialog>, but it asks the
   Add-technician card's two questions — which trade, then which person on it — so it takes that
   card's 420px measure and column rhythm rather than the 720px prose width .dialog-modal defaults
   to. A two-field form does not need the wider measure, and at 720px a pool of a dozen names read
   as a wall rather than a question. The header's own bottom margin is dropped because the column
   gap now supplies that space. */
.dialog-modal--compact { max-width: 420px; }
.dialog-modal--compact .modal__panel { display: flex; flex-direction: column; gap: 0.65rem; }
.dialog-modal--compact .modal__header { margin-bottom: 0; }
.dialog-modal--compact .form-hint { margin: 0; }
/* The crew dialog hides its person field when the trade's pool is empty, and .clock-panel__field
   sets `display` — an author rule arguing with the browser's `[hidden] { display: none }`, the same
   trap .btn[hidden] and .vehicle-card-wrap[hidden] document above. Stated rather than left to the
   UA sheet. */
.dialog-modal--compact .clock-panel__field[hidden] { display: none; }

/* The vehicle-photo carousel (estimating/show.html.twig) needs room for a large image plus a
   thumbnail strip beneath it — the 720px default reads cramped for that, so this widens it the
   same way .dialog-modal--compact narrows it. */
.dialog-modal--wide { max-width: 900px; }

/* The job page's crew readout (job.change_status): who's assigned to each trade slot, independent of
   who's actually clocked on (.clock-panel__on-job above it). Each member renders as a .pill so it
   picks up that component's light/dark styling for free; .job-crew__remove only resets the ×
   button's form wrapper so it sits inline with the name rather than dropping to its own line. */
.job-crew { display: flex; flex-direction: column; gap: 0.5rem; margin: 0.75rem 0; padding-top: 0.75rem; border-top: 1px solid var(--bs-line); }
.job-crew__rows { display: contents; }
/* Heading and its one control on a line: the heading takes the panel's own .section-heading so the
   crew list reads as a section rather than as more of the status form, and Add technician sits at
   the end of that line instead of stretching across the panel the way a flex-column child does. */
.job-crew__head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.job-crew__head .section-heading { margin: 0; }
.job-crew__role { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
.job-crew__label { font-size: 0.72rem; font-weight: 600; color: var(--bs-muted); text-transform: uppercase; letter-spacing: 0.04em; margin-right: 0.15rem; }
.job-crew__remove { display: inline-flex; }
.btn--icon { background: none; border: none; padding: 0; margin-left: 0.3rem; font-size: 1rem; line-height: 1; cursor: pointer; color: var(--bs-muted); }
.btn--icon:hover { color: #b3261e; }

/* Per-technician clock controls on the phase row's crew bubbles. Each control is its own tiny POST
   form so play and pause work without JS; .crew-clock__form only resets that wrapper so the buttons
   sit inline with the name instead of each dropping to its own line (same reason as
   .job-crew__remove). Colours are semantic rather than themed — green/amber/red read the same in
   both light and dark, where a muted grey trio would not. */
.crew-clock { display: inline-flex; align-items: center; gap: 0.2rem; margin-left: 0.35rem; }
.crew-clock__form { display: inline-flex; }
.crew-clock__btn { width: 1.35rem; height: 1.35rem; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 0.6rem; line-height: 1; cursor: pointer; border: 1px solid transparent; background: none; padding: 0; }
.crew-clock__btn--play { color: #1a7f37; border-color: rgba(26,127,55,0.45); background: rgba(26,127,55,0.12); }
.crew-clock__btn--pause { color: #9a6700; border-color: rgba(154,103,0,0.45); background: rgba(154,103,0,0.12); }
.crew-clock__btn--complete { color: #12b76a; border-color: rgba(18,183,106,0.5); background: rgba(18,183,106,0.14); margin-left: 1rem; }
.crew-clock__btn:hover { filter: brightness(1.15); }

/* Status picker and its Update button on one line: the button aligns to the select's bottom edge
   rather than stretching, which flex-end on a label+select column would otherwise do. */
.status-form__row { display: flex; align-items: flex-end; gap: 0.6rem; }

/* Panel / card */
.panel { background: #fff; border: 1px solid var(--bs-line); border-radius: 14px; box-shadow: 0 1px 2px rgba(16,24,40,0.04); }

/* A heading above a full-bleed table inside a panel. .panel carries no padding of its own (the table
   has to reach the rounded edges), so a bare heading lands flush in the corner while the table's cells
   sit 1rem in — the two read as belonging to different boxes. This wrapper supplies the missing
   padding, matched to the cell padding so heading and first column share a left edge. The wrapper —
   not the heading — is padded so the dark theme's accent bar (which pads .section-heading itself)
   doesn't fight the shorthand. */
.panel__heading { padding: 1rem 1rem 0; }
.panel__heading > .section-heading { margin-bottom: 0; }
/* The counterpart to .panel__heading, for a panel whose body is a gallery, a form or prose rather
   than the full-bleed table .panel is built around. Same 1rem inline padding, so a body and the
   heading above it share a left edge; the top gap replaces the margin the heading gives up. Padding
   the wrapper rather than .panel itself keeps the heading out of it — nesting the two paddings
   would indent the heading a further 1rem past the body it introduces. */
.panel__body { padding: 0.85rem 1rem 1rem; }
/* Dark theme: pull the wrapper in so the accent bar hangs in the gutter and the heading text keeps
   its 1rem alignment with the column below it. 0.3rem wrapper + 0.7rem bar allowance = 1rem. */
[data-bs-theme="dark"] .panel__heading { padding-inline-start: 0.3rem; }

/* First-run setup panel on the dashboard. Rows are quiet by default and only the outstanding ones
   carry weight, so a finished list reads as reassurance rather than noise. */
.setup-panel { margin-bottom: 1.25rem; }
/* Only the flex that puts the count beside the title, same reasoning as .estimate-bucket__heading:
   it must be written on the <h2> itself, not on .panel__heading — [data-bs-theme="dark"]
   .section-heading pads the heading itself for its accent bar and outranks a shorthand on the
   wrapper. */
.setup-panel__heading { display: flex; align-items: baseline; gap: 0.6rem; }
.setup-panel__count { font-family: var(--bs-font-mono); font-size: 0.72rem; color: var(--bs-muted); }
/* Same 1rem inline grid as .panel__body (see its comment above), and the same 0.85rem top gap so
   the first row doesn't sit flush against the heading. */
.setup-panel__list { list-style: none; margin: 0; padding: 0.85rem 1rem 1rem; display: flex; flex-direction: column; gap: 0.4rem; }
.setup-panel__item { display: flex; align-items: baseline; gap: 0.6rem; font-size: 0.85rem; }
.setup-panel__item--done { color: var(--bs-muted); }
.setup-panel__item--todo .setup-panel__label { font-weight: 700; }
.setup-panel__tick { flex: 0 0 auto; }
/* Not --bs-hi-vis: that token is #00de91, tuned for the dark sidebar/board surfaces where it's used
   everywhere else. On this panel's white background it's ~1.9:1, near-invisible. #0a9d68 is the
   value already established for exactly this (see the .workshop-tv light-theme override below) —
   legible as text on white (~3.5:1) and still legible on the dark panel surface (~4.7:1 on #1c1f24),
   so one value serves both themes without a dark-mode pair. */
.setup-panel__item--done .setup-panel__tick { color: #0a9d68; }
.setup-panel__label { flex: 1; min-width: 0; }
.setup-panel__hint { color: var(--bs-muted); font-weight: 400; margin-left: 0.4rem; }

/* Data table */
/* Horizontal-scroll container for wide tables: a data-dense table stays usable on a phone by
   scrolling sideways inside its panel instead of widening the whole page. The inset shadows hint
   that there's more table to the side; they fade as you reach each edge. */
.table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    background:
        linear-gradient(to right, var(--table-scroll-cover) 30%, transparent) left center / 28px 100% no-repeat,
        linear-gradient(to left, var(--table-scroll-cover) 30%, transparent) right center / 28px 100% no-repeat,
        radial-gradient(farthest-side at 0 50%, rgba(16,24,40,0.10), rgba(0,0,0,0)) left center / 12px 100% no-repeat,
        radial-gradient(farthest-side at 100% 50%, rgba(16,24,40,0.10), rgba(0,0,0,0)) right center / 12px 100% no-repeat;
    background-attachment: local, local, scroll, scroll;
}
/* The scroll container paints an opaque background (the hint gradients above), and .panel is
   rounded but not clipped — overflow:hidden there would clip popovers and sticky headers in every
   panel in the app. So the container matches the panel's radius on whichever corners it actually
   touches; otherwise its square background fills the panel's rounded corners and the colour appears
   to drop out at the edges. */
.panel > .table-scroll:first-child { border-top-left-radius: 14px; border-top-right-radius: 14px; }
.panel > .table-scroll:last-child { border-bottom-left-radius: 14px; border-bottom-right-radius: 14px; }

/* Only force the table to its natural (unwrapped) width — and thus sideways scrolling — on
   tablet-and-smaller. Above this, the table returns to width:100% and shows every column in the
   panel with no scroll. (860px is the app's primary breakpoint — see the sidebar/two-col rules.) */
@media (max-width: 860px) {
    .table-scroll .data-table { min-width: max-content; }
}

.data-table { width: 100%; border-collapse: collapse; }
.data-table thead th {
    text-align: start;
    font-family: var(--bs-font-mono);
    font-size: 0.68rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--bs-muted);
    padding: 0.85rem 1rem;
    border-bottom: 1px solid var(--bs-line);
}
/* `tbody th` as well as td: a row-header cell (`<th scope="row">`, which the site benchmark's KPI
   column is) matched neither this rule nor the `thead th` one above, so it fell through to the
   browser default of 1px padding and no bottom border — the label sat flush against the row edges
   while every number beside it was properly inset. Measured in the browser at 1px against these
   13.6px before the fix. */
.data-table tbody td,
.data-table tbody th { padding: 0.85rem 1rem; border-bottom: 1px solid #eef0f3; vertical-align: middle; }
.data-table tbody tr:last-child td,
.data-table tbody tr:last-child th { border-bottom: none; }
.data-table tbody tr:hover { background: #fafbfc; }
/* The row opens the job (row_link_controller), so it has to say so under the cursor. The hover tint
   above already existed for scanning; this is what makes it read as a target rather than a highlight. */
.data-table tbody tr.is-row-link { cursor: pointer; }
.data-table__reg { font-family: var(--bs-font-mono); font-weight: 600; letter-spacing: 0.03em; }
/* A money cell showing a loss — a part sold below what it cost (RSPH-142). The only figure in the
   parts table that gets a colour, because it is the only one that is a problem rather than a number.
   Colour alone is not the signal: the figure already carries a minus sign, so a viewer who cannot
   distinguish the red still reads the loss. Safe as a bare class — `.data-table tbody td` sets
   padding and borders but no colour, so there is nothing here to lose the specificity fight with. */
.data-table__reg--loss { color: #b3261e; }
.data-table__reg .eyebrow { display: block; margin-top: 0.15rem; opacity: 0.75; }
/* Columns holding a single indivisible value — a date, a person's name, a money
   amount, a reference. Auto table layout will happily break "30-07-2026" into
   "30-07-" / "2026" to save a few pixels, which reads as corrupt data rather
   than as a tight column (RSPH-103). Prose-ish columns (a vehicle's make and
   model, a status label) are left wrappable on purpose. */
.data-table__atomic { white-space: nowrap; }
/* A cell holding several discrete values — chips, a registration beside a WIP
   number, an amount beside its qualifier. The cell may wrap BETWEEN those
   values, but none of them may break mid-word ("Panel Technician" over two
   lines reads as two roles; "AED13200.00" split reads as corrupt). Deliberately
   scoped rather than applied to .badge-flag globally — the long status chips on
   the workshop boards are allowed to wrap. */
.data-table__chips > * { white-space: nowrap; }
/* The workshop boards' status column. Its labels are long enough ("Awaiting
   Insurance Authority") that a chip is allowed to run to two lines, but once
   the neighbouring columns were made nowrap this column was squeezed to ~158px
   and the chip went to three, making every row taller than before. Hold a floor
   that keeps it to two lines without forcing a full nowrap. */
.data-table__status { min-width: 11.5rem; }
/* Keep the actions cell a real table-cell (not display:flex, which detaches it from the row and
   staggers the row divider) and lay its buttons out in a right-aligned inline row instead. */
.data-table__actions { text-align: end; white-space: nowrap; }
/* A labelled actions column needs its heading over its own cells. `.data-table thead th` sets
   text-align: start and, with two type selectors above its class, outranks the bare class above — so
   the heading stayed left while everything under it went right, leaving it hanging over the data
   column to its left as if it labelled that instead. Has to reach `thead th` to win. */
.data-table thead th.data-table__actions { text-align: end; }
/* Opt-out for action cells holding a list rather than a fixed button pair: a
   nowrap list grows without bound and starves the data columns. */
.data-table__actions--wrap { white-space: normal; }
.data-table__actions > * { display: inline-flex; vertical-align: middle; }
.data-table__actions > * + * { margin-inline-start: 0.4rem; }

/* A <details> row-action disclosure (the training-type and equipment edit forms). Its panel opens
   DOWNWARDS, which puts it at odds with every rule the actions cell sets for a row of buttons:
   `display: inline-flex` above makes the <form> a second flex item sitting BESIDE the summary rather
   than under it, `white-space: nowrap` strips the wrapping from the field hints, and `text-align: end`
   right-aligns the labels away from their inputs. The form-grid columns then size to that unwrapped
   prose, so the table ends up several screens wide. Putting the disclosure back to a block undoes all
   four at once — it is the display value, not the individual symptoms, that is wrong here. */
.data-table__actions .inline-edit { display: inline-block; text-align: start; white-space: normal; }
/* A definite width so the actions column has a predictable size while open, rather than being sized by
   the longest hint in the form. Narrow enough to sit in a table, so the grid drops to one column. */
.data-table__actions .inline-edit > form { width: min(22rem, 70vw); }
.data-table__actions .inline-edit .form-grid { grid-template-columns: 1fr; gap: 0.9rem; }
/* An OPEN disclosure is a panel, not a button, and `inline-block` above leaves it in the cell's
   inline flow — so the sibling controls that share the cell (the courtesy car loan row's Sign /
   Agreement / Replace agreement and its signature-method badge) wrapped AROUND the open form and
   came to rest between its own fields, and the long inline line pushed the table past its panel.
   Going block-level for the open state gives the form a line of its own: the siblings can only sit
   above or below it, and the cell's width stops being "form plus every sibling on one line".
   The cell is display:table-cell on purpose (see the note above — flex detaches it from the row and
   staggers the divider), so this has to be done with the disclosure's own display, not the cell's.
   `text-align` is flipped back to `end` so the summary and the panel stay in the right-aligned
   column with the other row actions, and restored to `start` inside the form so its labels stay
   with their inputs. */
.data-table__actions .inline-edit[open] { display: block; text-align: end; }
.data-table__actions .inline-edit[open] > form { margin-inline-start: auto; text-align: start; }
/* An open editor makes its row many times taller than the data in it, and middle-aligned cells then
   leave the training's own name floating halfway down the page, level with nothing. Top-align the row
   being edited so the record reads across from the first field of the form that is editing it. */
.data-table tr:has(.inline-edit[open]) > td { vertical-align: top; }
/* Applies to both homes of this component — the row-action cell and the panel body beneath the table. */
.inline-edit[open] > form { margin-top: 0.75rem; }
/* Inline table-cell inputs (e.g. a part's editable expected-delivery date): match the app's
   text-field styling at a size that fits a table row. */
.inline-form input[type="date"],
.inline-form input[type="text"],
.inline-form select {
    font-family: var(--bs-font-ui);
    font-size: 0.9rem;
    padding: 0.4rem 0.55rem;
    border: 1.5px solid var(--bs-line);
    border-radius: 8px;
    background: #fff;
    color: var(--bs-text);
}
.inline-form input:focus,
.inline-form select:focus {
    outline: none;
    border-color: var(--bs-hi-vis);
    box-shadow: 0 0 0 3px rgba(0, 222, 145, 0.18);
}

/* Empty state */
.empty-state { padding: 3rem 1.5rem; text-align: center; color: var(--bs-muted); }

/* Board tiles (live work situation) */
.board-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; }
.board-tile {
    display: block;
    text-decoration: none;
    color: inherit;
    background: #fff;
    border: 1px solid var(--bs-line);
    border-radius: 14px;
    padding: 1.25rem 1.3rem;
    transition: border-color 0.12s ease, transform 0.04s ease, box-shadow 0.12s ease;
}
.board-tile__count { font-family: var(--bs-font-display); font-size: 2.6rem; line-height: 1; color: var(--bs-ink); }
.board-tile__count--zero { color: #c2c8d0; }
.board-tile__label { font-weight: 600; margin-top: 0.35rem; }
.board-tile__hint { font-family: var(--bs-font-mono); font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--bs-muted); margin-top: 0.5rem; }
/* Compact per-status pipeline tiles. Red-RAG statuses (e.g. Awaiting Parts) flag in red. */
.board-tile--status { padding: 0.85rem 1rem; }
.board-tile--status .board-tile__count { font-size: 1.9rem; }
.board-tile--status .board-tile__label { font-size: 0.9rem; margin-top: 0.2rem; }
.board-tile--red .board-tile__count,
.board-tile--red .board-tile__label { color: #b3261e; }
@media (max-width: 860px) { .board-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 480px) { .board-grid { grid-template-columns: 1fr; } }
/* The dashboard draws a tile for every workflow status — the same list as the job page's status
   picker — so the grid runs to around forty cards. Tighter columns and a smaller type scale keep it a
   block you can scan in one go rather than a page of its own; auto-fill handles the narrow widths,
   which is why these rules sit after the fixed-column media queries above. */
.board-grid--dense { grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr)); gap: 0.5rem; }
.board-grid--dense .board-tile--status { padding: 0.5rem 0.65rem; border-radius: 10px; }
.board-grid--dense .board-tile--status .board-tile__count { font-size: 1.3rem; }
.board-grid--dense .board-tile--status .board-tile__label { font-size: 0.75rem; line-height: 1.3; margin-top: 0.1rem; }
/* An empty status greys out even when its RAG is red — a red 0 reads as a problem that isn't there. */
.board-tile--red .board-tile__count--zero { color: #c2c8d0; }
/* A summary strip whose tile count depends on what the reader may see — the stock item's, where the
   markup and charge-out price are behind revenue.view. Fixed columns would leave two dead cells for
   everyone else; auto-fit fills the row at either width, which is why this sits after the
   fixed-column media queries above (they would otherwise win at the narrow sizes). */
.board-grid--fit { grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); }
/* A part selling for less than it cost. Same red as a red-RAG tile, but saying something different:
   a figure pointing the wrong way, not a status. */
.board-tile--loss .board-tile__count,
.board-tile--loss .board-tile__label { color: #b3261e; }
/* "Not priced" stands where a count would, but it is a sentence: set at figure size and in the
   display face it reads as data and competes with the real numbers either side of it. Deliberately
   NOT a .board-tile__count modifier — the count's size and its dark-theme colour are each set at
   higher specificity (one twenty lines up, one two thousand lines down), so a modifier would have to
   out-specify two distant rules to say something this simple. */
.board-tile__absent { font-size: 1rem; line-height: 1.7; color: var(--bs-muted); }

/* ---- Board card view: compact per-vehicle cards (RSPH-3) ---- */
.view-toggle { display: inline-flex; gap: 0.35rem; }
.vehicle-card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(158px, 1fr));
    gap: 0.55rem;
}
.vehicle-card {
    position: relative;
    display: flex;
    flex-direction: column;
    text-decoration: none;
    color: inherit;
    background: #fff;
    border: 1px solid var(--bs-line);
    border-radius: 12px;
    overflow: hidden;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
/* Card wrapper (RSPH: keyboard Move control) — a draggable card's grid child is now this wrapper,
   not the card <a> itself, so the wrapper has to fill the cell and the card has to fill the
   wrapper. A non-draggable card (equipment/customer panels) never gets the wrapper and is
   unaffected. */
.vehicle-card-wrap { position: relative; display: flex; height: 100%; }
/* The card filter hides this wrapper (it is the grid child) so a filtered-out card gives up its
   cell instead of leaving a blank slot. `display: flex` above is an author rule, so it can beat the
   UA's `[hidden] { display: none }` — same trap as `.btn[hidden]` further up. */
.vehicle-card-wrap[hidden] { display: none; }
.vehicle-card-wrap > .vehicle-card { flex: 1 1 auto; }
/* A Total Loss job reads as 'dead' — greyed and faded so it recedes from the live board. The whole
   card (image included) fades; hover/focus lifts it back so it stays readable and openable. */
.vehicle-card--dead { opacity: 0.4; filter: grayscale(1); }
.vehicle-card--dead:hover,
.vehicle-card--dead:focus-visible { opacity: 0.82; filter: grayscale(0.35); }
/* Same 'dead' treatment for the jobs table: the Total Loss row's text/badges/emblem dim and
   desaturate. The WIP cell is exempt because the WIP number is now the row's only real link (the
   Open column it used to keep legible is gone), and opacity cannot be undone on a child — a link
   inside a 0.45 cell cannot be brought back to 1, so the exemption has to be on the cell. */
.data-table tbody tr.is-total-loss td:not(.job-cell-wip) { opacity: 0.45; }
.data-table tbody tr.is-total-loss .badge-flag,
.data-table tbody tr.is-total-loss .make-emblem { filter: grayscale(1); }
/* A reversed completion payment (RSPH-166): the receipt stays on the ledger and stays readable, but
   reads at a glance as money that no longer counts. Struck through and dimmed — NOT hidden, which
   would defeat the whole point of keeping the row. The status cell is exempt from both, because it
   carries the only statement of who reversed it and when, which must stay legible.
   `.data-table tbody tr … td` matches the specificity of the base `.data-table tbody td` padding
   rule above it; a bare `.is-reversed` class on the cell would lose to it. */
.data-table tbody tr.is-reversed td:not(.data-table__reversal) { text-decoration: line-through; opacity: 0.55; }
.data-table tbody tr.is-reversed .badge-flag { filter: grayscale(1); text-decoration: none; }
.data-table tbody tr.is-reversed td.data-table__reversal .badge-flag { filter: none; }
/* Deadline urgency (mirrors the Workshop TV board) on the Live Job Status page. Cards view: the card
   border recolours amber 2-3 days from the requested completion date, red at 1 day / due today /
   overdue with a slow pulse. The double class + ring reads as a 2px border without a layout shift
   and holds over the hover border colour.
   The ring is an ::after overlay, NOT `box-shadow: inset` (which is what it was until RSPH). An
   inset shadow paints in the element's own background layer, so the full-bleed photo — a child,
   painted later — covered it: the ring appeared to start below the image and wrap the details only.
   A positioned pseudo-element paints above the photo, so the urgency reads around the WHOLE card.
   `inset: 0` sits it just inside the 1px border (radius 11px = 12px minus that border), giving the
   same 2px total weight as before; pointer-events: none keeps the card's link and drag untouched. */
.vehicle-card.vehicle-card--due-amber { border-color: var(--due-amber); }
.vehicle-card.vehicle-card--due-red {
    border-color: var(--due-red);
    animation: cardDuePulse 1.3s ease-in-out infinite;
}
.vehicle-card--due-amber::after,
.vehicle-card--due-red::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    border: 1px solid var(--due-amber);
    border-radius: 11px;
    pointer-events: none;
}
.vehicle-card--due-red::after {
    border-color: var(--due-red);
    animation: cardDueRingPulse 1.3s ease-in-out infinite;
}
/* The two pulses are one effect split across two boxes (border + ring), so they share a duration and
   easing and start together — keep them in step if either is ever retimed. */
@keyframes cardDuePulse {
    0%, 100% { border-color: var(--due-red); }
    50% { border-color: var(--bs-line); }
}
@keyframes cardDueRingPulse {
    0%, 100% { border-color: var(--due-red); }
    50% { border-color: transparent; }
}
/* The WIP number is an anchor, not a span: it is the keyboard's way into a row whose click handler
   only a mouse can reach. It should not READ as a link though — the whole row is the target and a
   blue underlined cell in a column of plain ones would suggest otherwise. So it inherits the table's
   colour and shows its underline only on hover or focus. Declared ahead of the due-urgency modifiers
   below so their colours still win. */
.wip { color: inherit; text-decoration: none; }
.wip:hover, .wip:focus-visible { text-decoration: underline; }
/* Table view: the WIP number is the affected field — same amber/red, red pulsing between the alarm
   colour and the normal text colour so it stays legible in either theme. */
.wip--due-amber { color: var(--due-amber); font-weight: 700; }
.wip--due-red {
    color: var(--due-red);
    font-weight: 700;
    animation: wipDuePulse 1.3s ease-in-out infinite;
}
@keyframes wipDuePulse {
    0%, 100% { color: var(--due-red); }
    50% { color: var(--bs-text); }
}
@media (prefers-reduced-motion: reduce) {
    .vehicle-card--due-red, .vehicle-card--due-red::after, .wip--due-red { animation: none; }
}
.vehicle-card__photo {
    position: relative;
    flex: 0 0 auto;
    aspect-ratio: 11 / 5;
    overflow: hidden;
    background: #f4f6f8;
    display: flex;
    align-items: center;
    justify-content: center;
}
.vehicle-card__photo img { width: 100%; height: 100%; object-fit: contain; max-width: 100%; }
/* Dark-mode brand emblems: a make with a published dark variant carries the URL inline as
   --emblem-dark; in dark theme we swap the <img>'s rendered image to it (white/light logo for the
   dark surface). Makes without a variant never get the .themed-emblem class, so they keep the
   standard logo. One element, no layout change, alt text preserved. */
[data-bs-theme="dark"] .themed-emblem { content: var(--emblem-dark); }
/* A real stock photo fills the wide slot (cover); brand emblems stay contained. The photo is taken
   out of flow so its intrinsic height can't stretch the slot past the 11/5 aspect ratio. */
.vehicle-card__photo--cover { background: #0d0f12; }
.vehicle-card__photo--cover img { position: absolute; inset: 0; object-fit: cover; }
.vehicle-card__photo--empty { background: linear-gradient(135deg, #eef1f4, #e2e7ec); }
.vehicle-card__initial { font-family: var(--bs-font-display); font-size: 1.5rem; color: #9aa4b0; }
.vehicle-card__body { padding: 0.4rem 0.5rem 0.5rem; display: flex; flex-direction: column; gap: 0.05rem; }
.vehicle-card__reg { font-family: var(--bs-font-mono); font-weight: 700; letter-spacing: 0.03em; font-size: 0.82rem; color: var(--bs-ink); }
.vehicle-card__wip { font-size: 0.68rem; color: var(--bs-muted); }
.vehicle-card__model { font-size: 0.76rem; font-weight: 600; }
.vehicle-card__loc { font-size: 0.66rem; color: var(--bs-muted); }
/* RSPH-3: a card can carry more than one badge (current status + Awaiting Parts progress). */
.vehicle-card__badges { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.3rem; }

/* Keyboard/no-mouse path to the assign modal (RSPH: board move keyboard access). Sits over the
   card's photo corner, never the body below where the reg and status badges live, so it can never
   cover either. Invisible until the card is hovered OR the control itself is focused — the
   :focus-visible reveal is the whole point: a keyboard user tabbing onto the control has to see it
   even though the card underneath is not being hovered. Stays pointer-events: none while hidden so
   an idle pointer over that corner still hits the card link beneath (and gets its hover ring)
   instead of the invisible pill; the ellipsis/max-width guard is a safety net for a long label
   (translation TBD) on the narrowest ~158px card. */
.vehicle-card__move {
    position: absolute;
    top: 0.35rem;
    right: 0.35rem;
    z-index: 1;
    max-width: 5.5rem;
    display: inline-flex;
    align-items: center;
    overflow: hidden;
    padding: 0.2rem 0.55rem;
    font-size: 0.68rem;
    font-weight: 600;
    line-height: 1.3;
    white-space: nowrap;
    text-overflow: ellipsis;
    border: 1px solid var(--bs-line);
    border-radius: 999px;
    background: var(--bs-paper);
    color: var(--bs-text);
    text-decoration: none;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.12s ease, border-color 0.12s ease;
}
.vehicle-card-wrap:hover .vehicle-card__move,
.vehicle-card__move:focus-visible {
    opacity: 1;
    pointer-events: auto;
}

/* Equipment reuses the vehicle card, but its title is a plain name, not a number-plate — drop the
   monospace/tracked plate voice so "Spray Booth 1" reads as a label. */
.equipment-card .vehicle-card__reg { font-family: var(--bs-font-ui); letter-spacing: 0; text-transform: none; font-size: 0.85rem; }

/* Stock item cards (RSPH-138) — the Inventory tab's Cards view. Same card as Live Job Status, with
   two changes that come from the subject rather than taste:
     • the title is a part name, so it drops the number-plate voice (as equipment/customer do)
     • the photo slot is squarer than the vehicle card's 11/5 letterbox: a tin, a bracket or a roll of
       masking tape is not a car-shaped subject, and a letterbox crop cuts the top and bottom off the
       very thing the picture is there to show. */
.stock-card .vehicle-card__reg { font-family: var(--bs-font-ui); letter-spacing: 0; text-transform: none; font-size: 0.85rem; }
.stock-card .vehicle-card__photo { aspect-ratio: 4 / 3; }
.stock-card .vehicle-card__body { gap: 0.15rem; }
.stock-card__value { margin-top: 0.15rem; font-size: 0.72rem; color: var(--bs-muted); font-variant-numeric: tabular-nums; }

/* Parts-cockpit furniture that is no longer one page's own (RSPH-151). These five lived in
   parts/cockpit.html.twig's {% block stylesheets %}, which was fine while the cockpit was their only
   user. parts/stock/browse.html.twig extends the base layout directly, so it never inherited that
   block — its request form rendered with browser-default full-width inputs that pushed the "Request
   stock" button clean out of the row, and its filter bar had no layout at all. Shared rules belong in
   the shared sheet; the cockpit-only ones stay where they were. */
.pc-filters { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-bottom: 0.9rem; }
.pc-worklist__head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin-bottom: 0.4rem; }
.pc-meta { font-size: 0.72rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--bs-muted, #6b7280); }
.pc-sub { display: block; font-size: 0.78rem; color: var(--bs-muted, #6b7280); }
.pc-rowform { display: inline-flex; gap: 0.3rem; align-items: center; }
.pc-rowform select { font: inherit; font-size: 0.76rem; padding: 0.25rem 0.35rem; border: 1px solid var(--bs-line, #c9ced6); border-radius: 6px; }
.pc-rowform input { font: inherit; font-size: 0.76rem; padding: 0.25rem 0.35rem; border: 1px solid var(--bs-line, #c9ced6); border-radius: 6px; width: 5.5rem; }

/* The ask, on the card, for the request-browse page only (RSPH-151). That card is the one place a
   stock card is not a link — the item sits in a department this user cannot see, so its own page is a
   404 from here — and a card that neither links nor acts would be a dead end, so it carries the
   request form instead.

   Stacked, never a row: the grid track is 158px at its narrowest, and a side-by-side department
   select + quantity + button collapses into three unreadable slivers well before that. The rule above
   it separates the ask (something you do) from the figures above (something you read); it is the only
   divider on the card, which is what keeps it reading as a card rather than a form. */
.stock-card__ask { margin-top: 0.4rem; padding-top: 0.4rem; border-top: 1px solid var(--bs-line); }
/* align-items: stretch overrides .pc-rowform's centring — left as centre, the button shrinks to its
   text and sits off-axis from the field above it. */
.stock-card__ask form { display: flex; flex-direction: column; align-items: stretch; gap: 0.25rem; }
.stock-card__ask select, .stock-card__ask input { width: 100%; min-width: 0; }

/* The photo on the inventory row (RSPH-138) — the ticket's core ask. A fixed narrow column so the
   images line up down the list and the name column does not shift as photos come and go.

   Sized at 64px (RSPH-149). It began at 36px because the cockpit was squeezed into the default
   1200px shell and every pixel here was paid for by the Order controls at the far end — but at that
   size a tin of primer and a box of abrasive disks are the same grey square, which was the whole
   complaint. The cockpit now uses the 1600px board shell, so the column can afford to answer "what
   does this part look like" on the row itself. The lightbox is still there for real detail.

   Stored thumbnails are generated up to ThumbnailGenerator::MAX_DIMENSION (512px), so existing
   photos serve this size sharply at 2x with no re-generation.

   The selectors carry `thead th` / `tbody td` because `.data-table tbody td` sets `padding: 0.85rem 1rem`
   and a bare `.data-table__thumb` loses to it on specificity — which silently left 32px of padding
   around the image and made this column cost twice what it looks like. */
.data-table thead th.data-table__thumb-head { width: 64px; padding-inline: 1rem 0.15rem; }
.data-table tbody td.data-table__thumb { width: 64px; padding-inline: 1rem 0.15rem; }
.data-table__thumb a { display: block; width: 64px; height: 64px; border-radius: 9px; overflow: hidden; border: 1px solid var(--bs-line); }
.data-table__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The hover ring lives in the affordance block at the end of this file, not here — declared before
   the dark surface block it would be overridden and do nothing in dark mode. */
.data-table__thumb a:focus-visible { outline: 2px solid var(--hover-ring); outline-offset: 2px; }
/* The inventory row's part name (RSPH-149). Nine columns will not fit a laptop screen once the
   inline raise-order form is on the row, so the table scrolls — and without a floor here the browser
   settles that by shrinking the name, which is the one column the row exists to show. A floor makes
   the overflow land in the scroll instead. Same device as .data-table__status above. */
.pc-part-cell { min-width: 13rem; }
/* No photo: the item's initial in the same box, so the column keeps its rhythm rather than
   collapsing to an empty cell halfway down the table. */
.thumb-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 64px;
    height: 64px;
    border-radius: 9px;
    border: 1px dashed var(--bs-line);
    background: linear-gradient(135deg, #eef1f4, #e2e7ec);
    color: #9aa4b0;
    font-family: var(--bs-font-display);
    font-size: 1.4rem;
}

/* The stock item's photos panel: which tile is the cover, and the add/remove controls under it. */
.photo-tile--cover { box-shadow: inset 0 0 0 2px var(--bs-hi-vis); }
.photo-tile__flag {
    position: absolute;
    inset-block-start: 5px;
    inset-inline-start: 5px;
    padding: 0.1rem 0.34rem;
    border-radius: 5px;
    background: var(--bs-hi-vis);
    color: var(--bs-ink-2);
    font-size: 0.62rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
/* The remove control rides on its tile (see the note in _photos_panel.html.twig). It stays visible
   rather than appearing on hover: a destructive action that only shows on hover is easy to hit by
   accident and impossible to find on a touchscreen. */
.photo-tile-wrap { position: relative; display: block; }
.photo-tile__remove { position: absolute; inset-block-start: 4px; inset-inline-end: 4px; margin: 0; }
.photo-tile__remove button {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    padding: 0;
    border: none;
    border-radius: 6px;
    background: rgba(13, 15, 18, 0.66);
    color: #fff;
    font-size: 0.72rem;
    line-height: 1;
    cursor: pointer;
}
.photo-tile__remove button:hover { background: var(--bs-danger, #d92d20); }
.photo-tile__remove button:focus-visible { outline: 2px solid var(--bs-hi-vis); outline-offset: 2px; }
/* The upload row sits at its natural size rather than stretching: a file input spread across a
   1300px panel with the button flung to the far edge reads as two unrelated controls. */
.photo-upload__row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.7rem; }
.photo-upload__row .field { flex: 0 1 340px; margin: 0; }
.photo-upload > .field__hint { margin: 0.45rem 0 0; }

/* Customer panel tiles (RSPH-104) — reuse the vehicle-card grid/card, with a taller-friendly body.
   Like the equipment card above, the title is a person/company name, not a number-plate, so it
   drops the monospace plate voice too. */
.customer-card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.55rem; }
.customer-card .vehicle-card__reg { font-family: var(--bs-font-ui); letter-spacing: 0; text-transform: none; font-size: 0.85rem; }
.customer-card .vehicle-card__body { gap: 0.15rem; }
.customer-card__contact { font-size: 0.85rem; color: var(--bs-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* .vehicle-card sets `display: flex`, an author rule that beats the UA's
   `[hidden] { display: none }` — the same trap as .vehicle-card-wrap above. The pager hides
   off-page cards with the hidden attribute, so without this the paging is a visual no-op. */
.customer-card[hidden] { display: none; }

/* Customer panel filter row (RSPH-106): the Live/All scope toggle sits beside the tile search, both
   of them controls over WHICH customers are listed. The search takes the remaining width and the
   pair wraps to two lines on narrow screens rather than squeezing the toggle. The bottom margin
   separates the row from the card grid below, which would otherwise sit flush against the search box. */
.panel-search { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 0.9rem; }
.panel-search .table-tools__search { flex: 1 1 16rem; min-width: 0; }

/* Result count beside the scope toggle. Pushed to its own line only when the row wraps, so it sits
   with the controls it describes rather than floating over the grid. */
.panel-count { font-size: 0.85rem; color: var(--bs-muted); white-space: nowrap; }

/* Card-grid pager (25 per page). Empty for a single page of results, and `:empty` keeps the margin
   from opening a gap under the grid when there is nothing to draw. */
.pager { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; margin-top: 1rem; }
.pager:empty { display: none; }
.pager__btn {
    appearance: none;
    border: 1px solid var(--bs-line);
    background: var(--surface, #fff);
    color: inherit;
    border-radius: 8px;
    padding: 0.35rem 0.7rem;
    font: inherit;
    font-size: 0.85rem;
    cursor: pointer;
}
/* The hover ring lives in the affordance block at the end of this file, not here — declared before
   the dark surface block it would be overridden and do nothing in dark mode. */
.pager__btn:disabled { opacity: 0.45; cursor: default; }
/* The completion worklist pages server-side, so its pager is made of <a>s (and, at the ends and on
   the current page, <span>s) rather than the <button>s the card-grid pager uses. They need the
   anchor resets the buttons get for free, and `.is-disabled` is the span's stand-in for :disabled —
   an anchor cannot be disabled, so the ends are simply not links. */
a.pager__btn { text-decoration: none; display: inline-block; }
.pager__btn.is-disabled { opacity: 0.45; cursor: default; }
/* The counter and the pager share a row on wide screens and stack on narrow ones. */
.pager-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem 1rem; margin-top: 1rem; padding: 0 1rem 1rem; }
.pager-bar .pager { margin-top: 0; }
/* Matches .view-switch__btn.is-active exactly (same hi-vis-on-ink-2 pressed state) so the pager
   reads as part of the same control family. */
.pager__btn.is-active { background: var(--bs-hi-vis); border-color: var(--bs-hi-vis); color: var(--bs-ink-2); font-weight: 600; }
/* The scope segments are links (they carry a real ?scope= URL so the panel works without JS), so
   they need the anchor resets the <button> segments get for free. */
a.view-switch__btn { text-decoration: none; display: inline-block; }

/* Cards/Table segmented toggle in the Equipment toolbar. Theme-aware via the shared tokens, so it
   needs no separate dark-mode block; the active segment carries the hi-vis accent. */
.view-switch { display: inline-flex; border: 1px solid var(--bs-line); border-radius: 9px; overflow: hidden; background: var(--surface, #fff); }
.view-switch__btn {
    appearance: none;
    border: 0;
    background: transparent;
    cursor: pointer;
    font: inherit;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--bs-muted);
    padding: 0.4rem 0.8rem;
    line-height: 1;
}
.view-switch__btn + .view-switch__btn { border-inline-start: 1px solid var(--bs-line); }
.view-switch__btn:hover { color: var(--bs-text); }
.view-switch__btn.is-active { background: var(--bs-hi-vis); color: var(--bs-ink-2); }
.view-switch__btn:focus-visible { outline: 2px solid var(--bs-hi-vis); outline-offset: 2px; }
.vehicle-card__badges .badge-flag { align-self: flex-start; font-size: 0.62rem; padding: 0.12rem 0.4rem; }
@media (max-width: 480px) { .vehicle-card-grid { grid-template-columns: 1fr 1fr; } }

/* RSPH-3: view switcher — the active view's button reads as pressed (filled). Ink on the light theme
   is the darkest thing in the toolbar, so the pressed button is unmistakably the one you are on; the
   dark theme needs its own fill (below), where ink is the page itself. */
.view-toggle .btn[aria-pressed="true"] {
    background: var(--bs-ink);
    color: #fff;
    border-color: var(--bs-ink);
}
.view-toggle .btn[aria-pressed="true"]:hover { background: var(--bs-ink-2); border-color: var(--bs-ink-2); }

/* ---- RSPH-3 Live Job Status board: stage columns + park strip + drag-drop ---- */
/* The board is width-hungry (ten columns), so this page gets more room than the default column.
   Shared with My Loading Queue, which is the same shape. */
.app__main--board { max-width: 1600px; }

/* The park strip: drop chips for the two holding stages, which get no column of their own. Sits in
   the toolbar row beside the search box, pushed to the far end so the two never crowd each other.
   A chip is a <span>, not a button — it is a drag target only, and nothing happens on click. */
.park-strip { display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem; margin-left: auto; }
.park-strip__label {
    font-family: var(--bs-font-mono); font-size: 0.62rem; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--bs-muted);
}
.park-strip__chip {
    display: inline-flex; align-items: center; gap: 0.4rem;
    font-size: 0.78rem; font-weight: 600; color: var(--bs-text);
    padding: 0.35rem 0.7rem; border-radius: 999px;
    border: 1.5px dashed var(--bs-line); background: #fff;
    transition: background 0.12s ease, border-color 0.12s ease, box-shadow 0.12s ease;
}
/* The wait picker (.park-strip__chip--pick) deliberately has no rule of its own: it is the same chip
   as its neighbours, minus the dot they each carry for the one status they are. What says it behaves
   differently — that it asks rather than moves — is the ellipsis in its label, which is what an
   ellipsis means. Muting it was tried and rejected: a grey chip beside two black ones reads as
   disabled, and this is the only route on the board to most of the waits. */
/* Same hi-vis drop treatment as .mq-tile.is-drop, so a card reads the same wherever it hovers. */
.park-strip__chip.is-drop {
    border-color: var(--bs-hi-vis); background: rgba(0, 222, 145, 0.16);
    box-shadow: 0 0 0 2px rgba(0, 222, 145, 0.28);
}
/* The rework counter beside the board search box (why it exists: _vehicle_cards.html.twig). It wears
   .park-strip__chip's geometry on purpose — the toolbar should read as one family of chips, and the
   alarm state is the only thing here that has earned the right to stand out.
   `--bs-paper` rather than #fff so the chip follows the theme without a dark-mode rule of its own. */
.rework-flag {
    font: inherit; font-size: 0.78rem; font-weight: 600; cursor: pointer;
    display: inline-flex; align-items: center; gap: 0.4rem;
    padding: 0.35rem 0.7rem; border-radius: 999px;
    border: 1.5px solid var(--bs-line); background: var(--bs-paper); color: var(--bs-muted);
    transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
/* Ships disabled and is enabled by card-filter on connect, so until Stimulus boots it is an
   indicator and must not offer itself as pressable. */
.rework-flag[disabled] { cursor: default; }
.rework-flag__dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; background: #9aa4b0; }
.rework-flag__count { font-family: var(--bs-font-mono); font-weight: 700; }
.rework-flag:focus-visible {
    outline: none; border-color: var(--bs-hi-vis); box-shadow: 0 0 0 4px rgba(0, 222, 145, 0.18);
}
.rework-flag--active { color: var(--rag-red); animation: rework-pulse 2s ease-in-out infinite; }
.rework-flag--active .rework-flag__dot { background: var(--rag-red); }
/* The pulse breathes the fill and border rather than throwing a box-shadow ring: the ring is the
   focus indicator above, and an animation on the same property would swallow it whole. */
@keyframes rework-pulse {
    0%, 100% {
        border-color: var(--rag-red);
        background: color-mix(in srgb, var(--rag-red) 16%, var(--bs-paper));
    }
    50% {
        border-color: color-mix(in srgb, var(--rag-red) 45%, var(--bs-line));
        background: color-mix(in srgb, var(--rag-red) 4%, var(--bs-paper));
    }
}
/* Red still says it, and so do the number and the word beside it — only the movement goes. */
@media (prefers-reduced-motion: reduce) {
    .rework-flag--active {
        animation: none;
        border-color: var(--rag-red);
        background: color-mix(in srgb, var(--rag-red) 16%, var(--bs-paper));
    }
}
.pill {
    font: inherit; font-size: 0.78rem; font-weight: 600; cursor: pointer;
    display: inline-flex; align-items: center; gap: 0.4rem;
    padding: 0.35rem 0.7rem; border-radius: 999px;
    border: 1px solid var(--bs-line); background: #fff; color: var(--bs-text);
    transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.pill__dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; background: #9aa4b0; }
.pill__dot--green { background: #1a7f3c; }
.pill__dot--amber { background: #c98a00; }
.pill__dot--red { background: #b3261e; }
.pill__dot--neutral { background: #9aa4b0; }

.vehicle-card__tech {
    margin-top: 0.3rem;
    padding-top: 0.3rem;
    border-top: 1px dashed var(--bs-line);
    font-size: 0.68rem;
    color: var(--bs-text);
}
.vehicle-card__tech.is-awaiting { color: #9a6700; font-weight: 600; }

/* Live Job Status: the car is being worked on right now (a technician is clocked on). Mirrors the
   technician-card running-clock light so "clock running" reads the same across the app. --due-green
   does not exist (only --due-amber/--due-red are defined near :root), so this uses the same literal
   green already used for on-job/success states (.tech-card--on_job, .pill__dot--green, badge-flag--green). */
.vehicle-card__tech { display: flex; align-items: center; gap: 0.3rem; }
.clock-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #1a7f3c;
    flex: 0 0 auto;
    animation: cardClockPulse 1.6s ease-in-out infinite;
}
@keyframes cardClockPulse {
    0%, 100% { opacity: 1; box-shadow: 0 0 0 0 color-mix(in srgb, #1a7f3c 60%, transparent); }
    50%      { opacity: 0.55; box-shadow: 0 0 0 3px transparent; }
}
@media (prefers-reduced-motion: reduce) {
    .clock-dot { animation: none; }
}
/* Keep the status cell a real table-cell (not display:flex, which detaches it from the row and
   staggers the location/status divider — same trap as .data-table__actions above). Lay the clock
   dot and badge out inline instead. */
.job-cell-status { white-space: nowrap; }
.job-cell-status > .clock-dot { display: inline-block; vertical-align: middle; margin-inline-end: 0.35rem; }
.job-cell-status > .badge-flag { vertical-align: middle; }
/* RSPH-179: same figure as .vehicle-card__consumed, which carries no bespoke rule of its own —
   sized down here only because the table packs several other cells onto the same row. */
.job-cell-status > .job-cell-consumed { display: inline-block; vertical-align: middle; margin-inline-end: 0.35rem; font-size: 0.72rem; color: var(--bs-muted); }

.vehicle-card[draggable="true"] { cursor: grab; }
.vehicle-card.is-dragging { opacity: 0.4; cursor: grabbing; }

/* ---- My Loading Queue tiles board (RSPH-69): status columns of vehicle cards; a dispatch surface for
   Team Leaders, a read-only stage view for technicians. Cards drag between columns to assign. ---- */
.mq-board__hint { color: var(--bs-muted); font-size: 0.85rem; margin: 0 0 0.85rem; }
/* min-width:0 is what keeps the columns' overflow HERE: as a flex item this would otherwise size to
   its dozen columns, widen the rail past the viewport and hand the page its own sideways scroll —
   taking the arrows with it. */
/* align-items: stretch, so every column runs the full height of the board. Each column IS a drop
   zone (`data-board-move-target="zone"`), and sized to its own contents a quiet stage ran out a few
   hundred pixels down while the busy stage beside it ran on for thousands — so a dispatcher scrolled
   down the board had nothing left to drop onto for most of the stages in front of them. */
.mq-tiles { display: flex; flex: 1 1 auto; min-width: 0; gap: 1rem; overflow-x: auto; padding-bottom: 0.6rem; align-items: stretch; scroll-behavior: smooth; }
/* The rail is wider than any screen, so it carries its own controls, in gutters either side: laid
   over the rail itself they covered the first and last column's status label — the one piece of text
   a column cannot spare. An arrow appearing is itself the signal that there is more board that way
   (the controller disables it otherwise).
   A flex row rather than absolute arrows in the rail's padding, because an out-of-flow arrow has
   nothing to stick to — and the arrows have to stick (see below). Row direction also mirrors itself
   under dir="rtl", which the hand-placed inset-inline pair it replaced had to spell out. */
.mq-rail { position: relative; display: flex; align-items: flex-start; gap: 0.4rem; }
/* queue-search clears the rail with the `hidden` attribute when nothing matches, and the `display`
   above is an author rule arguing with the browser's `[hidden] { display: none }`. Chromium and
   Firefox mark theirs !important and win; WebKit's does not, so without this Safari would keep an
   empty rail and two dead arrows above the no-match line. */
.mq-rail[hidden] { display: none; }
.mq-rail__arrow {
    /* The columns run well past the fold, so an arrow parked at the top of the rail is off screen
       exactly when you are deep in a column and want it. Sticky keeps both within reach for as long
       as any board is on screen, and stops at the foot of the rail rather than trailing past it.
       The rail is the containing block, so the travel is the board's height and no further. */
    position: sticky;
    inset-block-start: 0.75rem;
    flex: none;
    z-index: 2;
    width: 2rem;
    height: 5.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--bs-line);
    border-radius: 10px; /* the tile's 14px, tightened for the narrower box */
    background: var(--bs-surface, #fff);
    box-shadow: 0 2px 8px rgba(16, 24, 40, 0.14);
    cursor: pointer;
    transition: border-color 0.12s ease, background-color 0.12s ease;
}
/* Below this width the app bar is fixed across the top of the viewport (56px, plus its 3px hi-vis
   rule), so an arrow stuck at the desktop offset parks itself underneath it. */
@media (max-width: 860px) {
    .mq-rail__arrow { inset-block-start: 4.2rem; }
}
.mq-rail__arrow:focus-visible { outline: 2px solid var(--hover-ring); outline-offset: 2px; }
/* The dead end of the rail: still there so the controls never shift, visibly out of play. */
.mq-rail__arrow:disabled { opacity: 0.35; cursor: default; box-shadow: none; }
/* Same solid-triangle chevron the sidebar draws, pointed along the rail. */
.mq-rail__chevron {
    width: 0;
    height: 0;
    border-top: 4.5px solid transparent;
    border-bottom: 4.5px solid transparent;
    border-inline-start: 6px solid var(--bs-muted);
}
.mq-rail__arrow--prev .mq-rail__chevron { transform: rotate(180deg); }
.mq-rail__arrow:hover:not(:disabled) .mq-rail__chevron { border-inline-start-color: var(--bs-body-color); }
@media (prefers-reduced-motion: reduce) {
    .mq-tiles { scroll-behavior: auto; }
    .mq-rail__arrow { transition: none; }
}
[data-bs-theme="dark"] .mq-rail__arrow { background: #1b2230; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); }
.mq-tile {
    /* Sized so the card inside (tile width minus 0.8rem padding each side) lands at ~158px,
       matching the Live Job Status board's .vehicle-card-grid minmax(158px). */
    flex: 0 0 184px;
    max-width: 184px;
    background: rgba(16, 24, 40, 0.03);
    border: 1px solid var(--bs-line);
    border-radius: 14px;
    padding: 0.8rem;
    display: flex;
    flex-direction: column;
    gap: 0.65rem;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.mq-tile.is-drop { border-color: var(--bs-hi-vis); box-shadow: 0 0 0 2px rgba(0, 222, 145, 0.28); }
/* The heading follows the viewport down its own column (pinned_head_controller — sticky cannot do it
   from inside the rail's scroll container), so it has to be paintable over the cards that pass
   beneath it. The band's geometry is permanent and only its paint toggles: the negative inline
   margin is what lets it reach the column's edges past the tile's 0.8rem padding, and switching THAT
   on at the pin line would reflow the column under the pointer at the moment the eye is on it. Each
   negative margin is given straight back as padding, so at rest the text sits exactly where it did. */
.mq-tile__head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    position: relative;
    z-index: 1;
    margin-inline: -0.8rem;
    padding-inline: 0.8rem;
    margin-block: -0.35rem;
    padding-block: 0.35rem;
    border-radius: 8px;
}
/* The tile's own 3% fill layered over the panel it sits on, rather than a hand-mixed hex: the band
   is then the same colour as the column it belongs to, and stays that way if either changes. */
.mq-tile__head.is-pinned {
    background: linear-gradient(rgba(16, 24, 40, 0.03), rgba(16, 24, 40, 0.03)), #fff;
    box-shadow: 0 2px 6px rgba(16, 24, 40, 0.10);
}
[data-bs-theme="dark"] .mq-tile__head.is-pinned {
    background: linear-gradient(rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.03)), var(--surface);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
}
.mq-tile__dot { width: 0.55rem; height: 0.55rem; border-radius: 50%; flex: 0 0 auto; }
/* The stage name is the column heading, so it carries an accent rather than the muted grey the rest
   of the tile chrome uses. Light gets the hi-vis green, deepened to survive the pale tile fill:
   #00de91 as text there is ~1.6:1, invisible at 0.64rem, and even the Workshop TV board's already-
   darkened #0a9d68 only reaches 3.2:1 — fine at TV size, short of AA here. #0a7a53 is the same hue
   at 4.8:1. Dark carries the brand cyan, which has room to be bright on that surface. */
.mq-tile__name { font-family: var(--bs-font-mono); font-size: 0.64rem; letter-spacing: 0.1em; text-transform: uppercase; color: #0a7a53; font-weight: 700; }
.mq-tile__count { margin-left: auto; font-family: var(--bs-font-mono); font-size: 0.72rem; color: var(--bs-muted); font-variant-numeric: tabular-nums; }
.mq-tile__cards { display: flex; flex-direction: column; gap: 0.6rem; min-height: 2.5rem; }
.mq-tile__empty { color: var(--bs-muted); font-size: 0.78rem; text-align: center; padding: 0.6rem 0; margin: 0; }
.mq-tile__cards .vehicle-card { width: 100%; }
.mq-tile__cards .vehicle-card-wrap { width: 100%; }
[data-bs-theme="dark"] .mq-tile { background: rgba(255, 255, 255, 0.03); border-color: var(--bs-line); }
[data-bs-theme="dark"] .mq-tile__name { color: var(--tile-hover); }

/* Completion board card (RSPH-166 follow-on): the card wears .vehicle-card for its family, border and
   fill and .completion-card for its own layout only. Sizing and rhythm ONLY down here — no
   `background`, no `color` that restates what .vehicle-card already gives. The dark-theme counterpart
   (`[data-bs-theme="dark"] .vehicle-card`, near the bottom of this file) is what repaints the fill, and
   it outranks the light base rule: attribute + class is (0,2,0) against .vehicle-card's (0,1,0).
   A `background: #fff` on a bare `.completion-card` here is (0,1,0) — it would LOSE to that counterpart,
   so it is not what pins a card light. What is genuinely unsafe is a COMPOUND selector:
   `.vehicle-card.completion-card` reaches (0,2,0) and merely TIES, at which point nothing but source
   order decides it — safe today only because this block sits above the dark section, which is not a
   property any rule should be resting on. Add `[data-bs-theme]` to a compound and it wins outright and
   the card stays white in dark mode: the exact defect the rectification reason fields shipped (see
   RectificationReasonFieldThemeContractTest) and the board modal nearly repeated. So no fill is
   restated here at all; colour comes from var(--bs-muted) / var(--bs-text) / var(--bs-line) and always
   follows the active theme.
   The money/age rows sit OUTSIDE .vehicle-card__body (they are siblings of it, not children — see
   _completion_card.html.twig), so they need their own horizontal inset to line up with the reg/wip/
   model text above them instead of running flush to the card's edge. */
.completion-card__customer { font-size: 0.72rem; color: var(--bs-muted); }
.completion-card__money {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    align-items: center;
    margin-top: 0.5rem;
    padding-inline: 0.5rem;
}
.completion-card__balance { font-family: var(--bs-font-mono); font-variant-numeric: tabular-nums; }
.completion-card__excess { font-size: 0.78rem; color: var(--bs-muted); }
.completion-card__age { font-size: 0.72rem; color: var(--bs-muted); margin-top: 0.35rem; padding: 0 0.5rem 0.5rem; }

/* ── Technician Status tiles (RSPH-66) ──────────────────────────────────────────────
   The shop-floor version of the vehicle card: a technician ID badge with a portrait, a name, and a
   state-coloured "status light". Each state maps to one accent colour, carried by the tile's top bar
   and the status pill's dot; the on-job light pulses to signal a running clock. */
.tech-card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
    gap: 0.55rem;
}
.tech-card {
    /* The STATE's colour. Everything naming what the technician is doing reads it: the status
       badge, its dot, the top bar, the job link. */
    --tech-accent: #9aa4b0;
    /* The BAND's colour — the border only, i.e. whether this tile needs you. The two agree across
       most of the board, which is exactly why they have to be kept apart: they come apart on a
       technician who is genuinely on a job and past the estimate, where the badge must stay green
       and only the border goes red. */
    --tech-band: var(--tech-accent);
    position: relative;
    display: flex;
    flex-direction: column;
    background: #fff;
    border: 1px solid var(--bs-line);
    border-radius: 12px;
    overflow: hidden;
    /* The status light: a hairline accent across the top edge. */
    box-shadow: inset 0 3px 0 0 var(--tech-accent);
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.tech-card--on_job  { --tech-accent: #1a7f3c; }
.tech-card--idle    { --tech-accent: #c98a00; }
.tech-card--holiday { --tech-accent: #2563eb; }
.tech-card--unpaid,
.tech-card--other   { --tech-accent: #6b7280; }
/* Purple, and the only hue in the app that is nothing else: sick leave moved OFF red when the
   overrun band arrived, because one colour cannot mean both "this needs chasing" and "this person is
   at home". Told apart from holiday's blue by hue rather than by badge text. */
.tech-card--sick    { --tech-accent: #7c3aed; }
.tech-card--off     { --tech-accent: #9aa4b0; }
/* Off the clock recedes, like a total-loss vehicle does on the jobs board — but stays readable. */
.tech-card--off { opacity: 0.72; }

/* ── Clock bands ────────────────────────────────────────────────────────────────────
   The tile's whole outline carries the signal, so a manager reads the board from across the
   workshop rather than hunting for a 3px bar. A SECOND class alongside the state one, because Idle
   splits in two at ten minutes and On a job splits on whether the phase budget has run out — see
   Time\Service\TechnicianClockBand. Everything not listed here keeps the plain hairline, which is
   what "clocked off = same as now" means.

   Sick is in this list despite carrying no band: its purple border is permanent, and it is a state
   rather than a clock, so it stays keyed off its own class.

   Same construction as the vehicle card's deadline urgency: a 1px coloured border plus an ::after
   ring, which reads as 2px without the layout shift a real 2px border would cause, and — the part
   that matters here — paints ABOVE the tile's full-bleed photo. An inset box-shadow paints in the
   element's own background layer and the photo, a later-painted child, covers it. */
.tech-card--clock-running,
.tech-card--clock-idle,
.tech-card--clock-idle_long,
.tech-card--clock-overrun,
.tech-card--sick { border-color: var(--tech-band); }
.tech-card--clock-running::after,
.tech-card--clock-idle::after,
.tech-card--clock-idle_long::after,
.tech-card--clock-overrun::after,
.tech-card--sick::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    border: 1px solid var(--tech-band);
    border-radius: 11px;
    pointer-events: none;
}
/* The one band whose colour is not the state's: they ARE on a job, so the badge stays green and
   only the border turns red. Setting --tech-accent here instead — which is how this shipped first —
   repaints the badge and its dot red as well, and a technician hard at work reads as an alarm.
   Everything else needs no entry: --tech-band falls back to the accent. */
.tech-card--clock-overrun { --tech-band: var(--due-red); }
/* The one band that moves, and the reason nothing else does. A technician stood about is something
   a manager can act on in the next thirty seconds; a job running over its estimate usually isn't,
   and is often legitimate. Same 1.3s tempo as the jobs board's overdue pulse. */
.tech-card--clock-idle_long { animation: techIdlePulse 1.3s ease-in-out infinite; }
.tech-card--clock-idle_long::after { animation: techIdleRingPulse 1.3s ease-in-out infinite; }
/* Two boxes, one effect: keep the pair in step if either is ever retimed. */
@keyframes techIdlePulse {
    0%, 100% { border-color: var(--tech-band); }
    50% { border-color: var(--bs-line); }
}
@keyframes techIdleRingPulse {
    0%, 100% { border-color: var(--tech-band); }
    50% { border-color: transparent; }
}
@media (prefers-reduced-motion: reduce) {
    .tech-card--clock-idle_long,
    .tech-card--clock-idle_long::after { animation: none; }
}
.tech-card__photo {
    position: relative;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    background: #f4f6f8;
    display: flex;
    align-items: center;
    justify-content: center;
}
/* Headshots are portrait; anchor the cover-crop to the top so heads aren't cut off in the square tile. */
.tech-card__photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: top center; }
.tech-card__photo--empty { background: linear-gradient(135deg, #eef1f4, #e2e7ec); }
.tech-card__initial {
    font-family: var(--bs-font-display);
    font-size: 2rem;
    font-weight: 700;
    color: #9aa4b0;
}
.tech-card__body { padding: 0.45rem 0.55rem 0.55rem; display: flex; flex-direction: column; gap: 0.28rem; }
.tech-card__name {
    font-weight: 700;
    font-size: 0.82rem;
    line-height: 1.15;
    color: var(--bs-ink);
}
/* Site line matches the technician name's size (colour still comes from .text-secondary). */
.tech-card__site { font-size: 0.82rem; line-height: 1.15; }
.tech-card__badges { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.tech-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.32rem;
    font-size: 0.64rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    padding: 0.14rem 0.44rem;
    border-radius: 999px;
    color: var(--tech-accent);
    background: color-mix(in srgb, var(--tech-accent) 14%, transparent);
}
.tech-badge::before {
    content: "";
    width: 0.42rem;
    height: 0.42rem;
    border-radius: 50%;
    background: var(--tech-accent);
    flex: 0 0 auto;
}
/* The badge that names the overrun, and the only one that does not follow the state. On an
   overrunning tile --tech-accent is green — the technician IS on a job — so inheriting it would
   print the alarm in the colour of everything being fine. --due-red already resolves per theme,
   which is what keeps this right in dark mode without a second rule to forget. */
.tech-badge--over {
    color: var(--due-red);
    background: color-mix(in srgb, var(--due-red) 14%, transparent);
}
.tech-badge--over::before { background: var(--due-red); }
/* The running-clock light: the on-job dot pulses, echoing "clock running". Scoped to the STATE badge
   rather than every badge in the row — an overrunning tile carries a second one, and two pulsing
   dots side by side read as an alarm rather than a clock. */
.tech-card--on_job .tech-badge--on_job::before { animation: techClockPulse 1.6s ease-in-out infinite; }
@keyframes techClockPulse {
    0%, 100% { opacity: 1; box-shadow: 0 0 0 0 color-mix(in srgb, var(--tech-accent) 60%, transparent); }
    50%      { opacity: 0.55; box-shadow: 0 0 0 3px transparent; }
}
@media (prefers-reduced-motion: reduce) {
    .tech-card--on_job .tech-badge--on_job::before { animation: none; }
}
.tech-card__job {
    font-family: var(--bs-font-mono);
    font-weight: 700;
    font-size: 0.72rem;
    letter-spacing: 0.03em;
    color: var(--bs-ink);
    text-decoration: none;
}
.tech-card__job:hover { color: var(--tech-accent); text-decoration: underline; }
.tech-card__since { font-size: 0.66rem; color: var(--bs-muted); }
.tech-card__actions { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.15rem; }
/* Every button on a tile is set one step below `btn--sm` so the tile stays compact. One rule for all
   three regions — Clocking History, the clock actions and "Put on a job" — because sizing them apart
   was exactly how the history button ended up visibly bigger than the buttons stacked under it. */
.tech-card__history > .btn,
.tech-card__actions .btn,
.tech-card__start > .btn { font-size: 0.66rem; padding: 0.16rem 0.45rem; }
/* "Put on a job" trigger + overlay modal. The trigger is the small ghost button on the tile; the
   overlay (a plain fixed div, not a native <dialog>) lists the technician's assigned work as
   selectable rows plus a search for any other active job. */
.tech-card__start { margin-top: 0.3rem; }
.tech-modal {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 1000;
    align-items: flex-start;
    justify-content: center;
    padding: 6vh 1rem 1rem;
    background: rgba(16, 24, 40, 0.45);
    overflow-y: auto;
}
.tech-modal.is-open { display: flex; }
.tech-modal__panel .field { margin-bottom: 1rem; }
.tech-modal__panel .field label { display: block; margin-bottom: 0.35rem; font-weight: 600; }
.tech-modal__panel {
    width: 100%;
    max-width: 560px;
    background: #fff;
    color: var(--bs-text);
    border-radius: 14px;
    box-shadow: 0 20px 50px rgba(16, 24, 40, 0.25);
    padding: 1.25rem 1.4rem 1.4rem;
}
.tech-start__subhead { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--bs-muted); margin: 0 0 0.4rem; }
.tech-start__jobs { display: flex; flex-direction: column; gap: 0.35rem; max-height: 34vh; overflow: auto; }
.tech-start__job {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    text-align: left;
    padding: 0.5rem 0.65rem;
    background: #fff;
    border: 1.5px solid var(--bs-line);
    border-radius: 9px;
    cursor: pointer;
}
.tech-start__job[aria-pressed="true"] { border-color: var(--bs-hi-vis); box-shadow: inset 0 0 0 1px var(--bs-hi-vis); }
.tech-start__wip { font-family: var(--bs-font-mono); font-weight: 700; letter-spacing: 0.02em; }
.tech-start__veh { font-size: 0.8rem; color: var(--bs-muted); }
.tech-start__or { font-size: 0.8rem; color: var(--bs-muted); margin: 0.8rem 0 0.4rem; }
/* Search results dropdown for the modal's job typeahead. */
.tech-start__job + .tech-start__job { margin-top: 0; }
.typeahead__option { display: block; width: 100%; text-align: left; padding: 0.4rem 0.6rem; background: none; border: none; cursor: pointer; font-size: 0.85rem; color: var(--bs-text); }
.typeahead__option:hover { background: #f1f3f5; }
@media (max-width: 480px) { .tech-card-grid { grid-template-columns: 1fr 1fr; } }

/* ── Staff Training tiles (RSPH-140) ────────────────────────────────────────────────
   The same staff tile as the Technician Status board — a person looks like the same person
   wherever the app shows them — re-keyed to a different question. The accent light answers
   "is anything lapsing?" instead of "are they clocked on?", so the state modifiers are the
   training bands and never collide with the time-clock ones above.

   The record COUNT is the tile's headline, not a footnote: the whole point of the card view is
   that you can read how much training someone holds without opening them. */
.tech-card--expired       { --tech-accent: #b3261e; }
.tech-card--expiring_soon { --tech-accent: #c98a00; }
.tech-card--current       { --tech-accent: #1a7f3c; }
.tech-card--no_expiry     { --tech-accent: #6b7280; }
/* Nobody has recorded anything for this person yet — the tile recedes rather than alarming. */
.tech-card--none          { --tech-accent: #9aa4b0; }
.tech-card--none .tech-card__count { color: var(--bs-muted); }
.tech-card__count {
    display: flex;
    align-items: baseline;
    gap: 0.3rem;
    font-family: var(--bs-font-display);
    font-size: 1.45rem;
    font-weight: 700;
    line-height: 1;
    color: var(--bs-ink);
}
.tech-card__count-label {
    font-family: var(--bs-font-ui);
    font-size: 0.66rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--bs-muted);
}
/* The whole tile is the target — a person's training page is the only thing it goes to. */
.tech-card__link { text-decoration: none; color: inherit; display: block; }
.tech-card__link:hover .tech-card__name { text-decoration: underline; }
.tech-card__link:focus-visible { outline: 2px solid var(--bs-hi-vis); outline-offset: 2px; border-radius: 12px; }

/* Technician photo field on the user edit form: a round preview beside the file input. */
.photo-field { display: flex; gap: 1rem; align-items: flex-start; margin-top: 0.5rem; }
.photo-field__preview {
    flex: 0 0 auto;
    width: 72px;
    height: 72px;
    border-radius: 50%;
    overflow: hidden;
    border: 1px solid var(--bs-line);
    display: flex;
    align-items: center;
    justify-content: center;
    background: #f4f6f8;
}
.photo-field__preview img { width: 100%; height: 100%; object-fit: cover; }
.photo-field__preview--empty { background: linear-gradient(135deg, #eef1f4, #e2e7ec); }
.photo-field__preview span { font-family: var(--bs-font-display); font-size: 1.75rem; font-weight: 700; color: #9aa4b0; }
.photo-field__controls { flex: 1 1 auto; display: flex; flex-direction: column; gap: 0.5rem; }

/* Compact drag image: just the number plate, so it lines up with a section. */
.drag-chip {
    position: fixed; top: -200px; left: 0;
    font-family: var(--bs-font-mono); font-weight: 700; font-size: 1rem; letter-spacing: 0.08em;
    color: #fff; background: var(--bs-ink);
    padding: 0.5rem 0.85rem; border-radius: 8px;
    border: 2px solid var(--bs-hi-vis);
    box-shadow: 0 10px 24px rgba(0,0,0,0.4);
    white-space: nowrap; pointer-events: none;
}

/* Assignment modal */
.board-modal {
    position: fixed; inset: 0; z-index: 50; background: rgba(14,15,17,0.55);
    display: flex; align-items: center; justify-content: center; padding: 1rem;
}
.board-modal[hidden] { display: none; }
.board-modal__card {
    width: 100%; max-width: 480px; background: var(--bs-paper); color: var(--bs-text);
    border-radius: 16px; box-shadow: 0 30px 70px rgba(0,0,0,0.4); overflow: hidden;
}
/* RSPH-184: the day window lists cars, not form fields — 480px is a form width. Capped against the
   viewport so a long day scrolls inside the card rather than pushing the page sideways. */
.board-modal__card--wide { max-width: 780px; }
.bk-dayview__body { max-height: min(60vh, 34rem); overflow-y: auto; padding: 1rem 1.25rem; }
.board-modal__head { padding: 1.1rem 1.25rem 0.9rem; border-bottom: 1px solid var(--bs-line); }
.board-modal__eyebrow {
    font-family: var(--bs-font-mono); font-size: 0.64rem; letter-spacing: 0.14em; text-transform: uppercase;
    color: var(--bs-muted); margin: 0 0 0.3rem;
}
.board-modal__title { font-family: var(--bs-font-display); font-size: 1.6rem; letter-spacing: 0.03em; margin: 0; }
.board-modal__sub { margin: 0.35rem 0 0; font-family: var(--bs-font-mono); font-weight: 700; }
/* The status row had no rule of its own, so it sat hard against the header rule and the card's left
   edge while everything around it was inset. 1.25rem matches where the rest of the modal's text
   starts — the head's padding, and the technician names once the list's 0.6rem and the button's
   0.65rem are added up — so the three read down one line. */
.board-modal__status {
    display: flex; align-items: center; gap: 0.6rem;
    padding: 0.9rem 1.25rem 0.2rem;
}
/* A drag already knows its target and hides this row with [hidden]. A display value in a class beats
   the attribute's UA rule, so without this the picker would reappear on every drop — the same trap
   the note above .btn[hidden] describes. */
.board-modal__status[hidden] { display: none; }
.board-modal__status select { flex: 1; min-width: 0; }

/* RSPH-147: the reason capture, shown in the assign pane only for a move into Rectification Work.
   Same left-rail device as the job page's copy of this field, in the red the rectification status
   already carries, so the two surfaces read as one feature. `[hidden]` needs its own rule for the
   same reason .board-modal__status above does. */
.board-modal__reason {
    display: flex; flex-direction: column; gap: 0.5rem;
    margin: 0.6rem 1.25rem 0.2rem;
    padding: 0.7rem 0.85rem;
    border-inline-start: 3px solid var(--due-red);
    border-radius: 0 9px 9px 0;
    background: color-mix(in srgb, var(--due-red) 6%, transparent);
}
.board-modal__reason[hidden] { display: none; }
.board-modal__reason-label {
    font-family: var(--bs-font-mono); font-size: 0.7rem; text-transform: uppercase;
    letter-spacing: 0.1em; color: var(--bs-muted);
}
/* Sizing only, deliberately. These two controls sit inside .shell, so the base field rule already gives
   them their family, border, fill and text colour — and it is written at zero specificity so the dark
   theme can restate the fill. Repeating any of that here (this rule once carried the base rule's own
   `background: #fff`) outranks the theme and pins the field white, which is invisible against the
   near-white text dark mode gives it. What's left is what actually differs from the base: the modal's
   tighter rhythm, matching .board-modal__tech beneath it. */
.board-modal__reason select, .board-modal__reason textarea {
    width: 100%; font-size: 0.9rem; padding: 0.5rem 0.65rem; border-radius: 8px;
}
.board-modal__reason textarea { resize: vertical; min-height: 3rem; }

/* Task 11: the rework trade capture, shown in the assign pane only for a move into Rectification or
   Warranty Work — rework carries no fixed trade of its own, so the modal asks here. Same left-rail
   device as .board-modal__reason above, but in a neutral border rather than the rectification red:
   this field is informational (name the trade), not a warning. */
.board-modal__rework {
    display: flex; flex-direction: column; gap: 0.5rem;
    margin: 0.6rem 1.25rem 0.2rem;
    padding: 0.7rem 0.85rem;
    border-inline-start: 3px solid var(--bs-border-color);
    border-radius: 0 9px 9px 0;
}
.board-modal__rework[hidden] { display: none; }
/* Sizing only, deliberately — see the identical note on .board-modal__reason select/textarea above:
   the base field rule already gives this control its fill and text colour, at zero specificity so the
   dark theme can restate it. */
.board-modal__rework select {
    width: 100%; font-size: 0.9rem; padding: 0.5rem 0.65rem; border-radius: 8px;
}
.board-modal__list {
    list-style: none; margin: 0; padding: 0.6rem; max-height: 46vh; overflow: auto;
    display: flex; flex-direction: column; gap: 0.35rem;
}
/* A refused move (RSPH-152). Sits between the pool and the actions, so the message and the pick that
   answers it are read in one glance. Reuses the badge red rather than a new token. */
.board-modal__error {
    margin: 0 0.6rem 0.6rem; padding: 0.5rem 0.65rem; border-radius: 10px;
    background: #fdecec; color: #b3261e; border: 1px solid #f3b4b4; font-size: 0.86rem;
}
/* Each name is a toggle, not a readout: it needs to look pressable at rest, not only on hover.
   Until RSPH-152 that did not matter — picking nobody was a valid "leave it awaiting" choice, so a
   name that read as static text cost nothing. Now that a working status refuses an empty crew, an
   invisible affordance is a dead end: the dispatcher sees two names, an error telling them to assign
   a technician, and nothing that looks clickable. */
.board-modal__tech {
    width: 100%; font: inherit; text-align: left; cursor: pointer;
    padding: 0.55rem 0.65rem; border-radius: 10px; border: 1px solid var(--bs-line); background: transparent;
}
.board-modal__tech:hover { background: #f1f3f5; }
/* A wait in the park picker's pane. Same button as a technician name — one look for "a thing you
   pick in this modal" — plus the row layout its rag dot needs, since .board-modal__tech is a plain
   block and the dot would otherwise sit on the text baseline. */
.board-modal__wait { display: flex; align-items: center; gap: 0.5rem; }
/* The pane opens with focus on the first wait, so the ring is not optional here: it is the only
   thing saying where the keyboard is. Matches the rail arrows' treatment. */
.board-modal__tech:focus-visible { outline: 2px solid var(--bs-hi-vis); outline-offset: 2px; }
.board-modal__tech.is-selected { background: rgba(0,222,145,0.1); border-color: rgba(0,222,145,0.45); font-weight: 600; }
/* Four actions now (Cancel / Send to awaiting / Clock Now / Assign) where there were three. In a
   420px card they no longer fit on one line, and a plain flex row answers that by breaking the
   labels mid-word ("Send to / awaiting"). Wrap the ROW instead and keep each label intact: the
   spacer collapses to nothing once wrapped, so the buttons flow to a second line as a group. */
.board-modal__actions {
    display: flex; align-items: center; flex-wrap: wrap; gap: 0.6rem;
    padding: 0.85rem 1.25rem 1.1rem; border-top: 1px solid var(--bs-line);
}
.board-modal__actions .btn { white-space: nowrap; }
/* The booking calendar's edit modal puts a form where the assign modal puts a list, so it needs the
   padding .board-modal__list would otherwise have provided. Everything inside is .form-grid/.field,
   which brings its own styling and dark-mode tokens. */
.bk-modal__form { padding: 0.9rem 1.25rem; }
.bk-modal__form .form-grid { gap: 0.85rem; }
.board-modal__spacer { flex: 1; }
.board-modal__workers { list-style: none; margin: 0; padding: 0.7rem 1.25rem 0; display: flex; flex-direction: column; gap: 0.45rem; }
.board-modal__worker { display: flex; flex-direction: column; gap: 0.1rem; }
.board-modal__worker-name { font-weight: 600; font-size: 0.9rem; }
.board-modal__worker-busy { font-size: 0.76rem; color: #9a6700; }
.board-modal__note { margin: 0; padding: 0.8rem 1.25rem; font-size: 0.85rem; color: var(--bs-muted); }
.board-modal__lines { padding: 0.7rem 1.25rem 0.2rem; max-height: 40vh; overflow: auto; display: flex; flex-direction: column; gap: 0.4rem; }

/* RSPH-166: the completion board's drop modal, where the assign modal puts a technician list. A drop
   can carry three whole forms at once (record the bill, take the payment, release the car), so it is
   wider than the 480px assign card and the steps scroll INSIDE it — .board-modal__card is
   overflow:hidden, so a container left to grow would push the Confirm row out of the card entirely
   rather than off the bottom of the screen where it could at least be scrolled to. Everything inside a
   step is .form-grid/.field, which brings its own field styling and dark-mode tokens (the same reason
   .bk-modal__form above adds padding and nothing else). */
.board-modal--steps .board-modal__card { max-width: 580px; }
/* No `color` here on purpose: a colour on the container is inherited by every field label and value
   inside it, so muting the chrome muted the whole form and dropped the contrast of text the advisor is
   reading numbers off. The step chrome carries its own colour below; the hint sentences come with
   `.text-secondary` from _board_step_fields.html.twig. */
.board-modal__steps {
    padding: 0.9rem 1.25rem; max-height: 56vh; overflow: auto;
    display: flex; flex-direction: column; gap: 1.2rem;
    font-size: 0.88rem;
}
.board-modal__steps .form-grid { gap: 0.85rem; }
/* A step whose whole content is a sentence — mark_cleaned's "nothing to fill in", release's
   "everything is clear" — has no .field wrapper to carry .field--full, so the two-column grid gave it
   half the width and broke one short sentence over three lines beside a column of dead space. */
.board-modal__steps .form-grid > p { grid-column: 1 / -1; margin: 0; }
/* One step's heading ("Step 2 of 3 · Record the final bill"), in the same mono eyebrow treatment as
   .board-modal__eyebrow so a stack of three reads as a sequence rather than three separate dialogs. */
.board-modal__step-title {
    font-family: var(--bs-font-mono); font-size: 0.7rem; text-transform: uppercase;
    letter-spacing: 0.1em; color: var(--bs-muted); margin: 0 0 0.6rem;
}
.board-modal__step + .board-modal__step { border-top: 1px solid var(--bs-line); padding-top: 1.1rem; }

/* Move confirmation toast */
.board-toast {
    position: fixed; left: 50%; bottom: 1.5rem; transform: translateX(-50%); z-index: 60;
    background: var(--bs-ink); color: #fff; font-size: 0.85rem; font-weight: 600;
    padding: 0.7rem 1.1rem; border-radius: 10px; box-shadow: 0 14px 34px rgba(0,0,0,0.35);
}
.board-toast[hidden] { display: none; }

/* Work-stream brand emblem (RSPH-50): a small coloured make logo inline before the vehicle in board
   and pipeline rows. Unknown makes fall back to a neutral initial chip — never a broken image. */
.make-emblem {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    margin-right: 0.45rem;
    vertical-align: middle;
    object-fit: contain;
    border-radius: 5px;
    flex: 0 0 auto;
}
.make-emblem--empty {
    background: linear-gradient(135deg, #eef1f4, #e2e7ec);
    color: #9aa4b0;
    font-family: var(--bs-font-display);
    font-size: 0.72rem;
    font-weight: 700;
    line-height: 1;
}
/* On the Live Job Status table the emblem stands alone in its own Make column rather than sitting
   inline beside text, so give it more room than the 22px inline badge. */
.job-cell-make .make-emblem {
    width: 40px;
    height: 40px;
    margin-right: 0;
    border-radius: 7px;
}

/* ---- Dashboard: LIVE WORK SITUATION ---- */
.kpi-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 0.85rem;
    margin-bottom: 1.1rem;
}
.kpi-stat {
    background: #fff;
    border: 1px solid var(--bs-line);
    border-radius: 14px;
    padding: 1.05rem 1.2rem;
}
.kpi-stat--hero { background: var(--bs-ink); border-color: var(--bs-ink); }
.kpi-stat--hero .kpi-stat__n { color: var(--hero-accent); }
.kpi-stat--hero .kpi-stat__label { color: #aeb6c2; }
.kpi-stat__n {
    font-family: var(--bs-font-display);
    font-size: 2.9rem;
    line-height: 0.95;
    letter-spacing: 0.01em;
    color: var(--bs-ink);
}
.kpi-stat__n--green { color: #1a7f3c; }
.kpi-stat__n--amber { color: #e8a400; }
.kpi-stat__n--red { color: #b3261e; }
.kpi-stat__n--neutral { color: var(--bs-muted); }
.kpi-stat__n--value { font-size: 2.2rem; }
/* A short caption below the headline figure (e.g. "jobs", "hrs per day on site") — same small,
   muted treatment as .kpi-stat__label, deliberately NOT the size of .kpi-stat__n: a unit folded
   into the headline number would inherit its huge bold size/weight/RAG colour and read as part of
   the figure rather than a caption for it. See SummaryStat::$unit's own docblock. */
.kpi-stat__unit {
    font-family: var(--bs-font-mono);
    font-size: 0.64rem;
    color: var(--bs-muted);
    margin-top: 0.4rem;
}
.kpi-stat__label {
    font-family: var(--bs-font-mono);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    font-size: 0.64rem;
    color: var(--bs-muted);
    margin-top: 0.4rem;
}
/* RSPH-128: a situation tile links to the matching /jobs list. It stays the same card — only the
   cursor, a subtle lift, and a focus ring mark it clickable; the number keeps its RAG colour. */
a.kpi-stat--link {
    display: block;
    text-decoration: none;
    color: inherit;
    cursor: pointer;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
/* The hover highlight lives in the end-of-file affordance block (green in light, blue in dark) so it
   survives the dark-surface reset — the same treatment the board tiles below use. Focus keeps its
   own stronger ring here. */
a.kpi-stat--link:focus-visible { outline: 2px solid var(--hover-ring); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
    a.kpi-stat--link { transition: none; }
}

/* Revenue strip (page-load only; never broadcast) */
.kpi-revenue { display: flex; align-items: baseline; gap: 0.6rem; margin: 0 0 1.1rem; }
.kpi-revenue__n { font-family: var(--bs-font-display); font-size: 2.1rem; line-height: 1; color: var(--bs-ink); }
.kpi-revenue__l { font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.64rem; color: var(--bs-muted); }

/* Signature: the proportional status board (hazard-tape RAG split) */
.kpi-board {
    display: flex;
    height: 16px;
    border-radius: 999px;
    overflow: hidden;
    margin-bottom: 1.6rem;
    background: #eef0f3;
    box-shadow: inset 0 0 0 1px var(--bs-line);
}
.kpi-board__seg { height: 100%; }
.kpi-board__seg--green { background: #1a7f3c; }
.kpi-board__seg--amber { background: #ffb81c; }
.kpi-board__seg--red { background: #b3261e; }
.kpi-board__seg--neutral { background: #98a2b3; }
.kpi-board__seg--empty { background: #eef0f3; }

/* Board grid sits between the KPI headline and the team panels — give it room to read as its own band. */
.board-grid-section { margin: 1.4rem 0; }
.board-grid-section > .section-heading { margin-bottom: 0.75rem; }
/* Heading beside an inline control (e.g. the closing-soon window toggle) — wraps on narrow screens. */
.section-head-row { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
.section-head-row > .section-heading { margin-bottom: 0; }

/* The remaining KPI panels, full width beneath the board grid: two compact team mixes and a wider
   leaders table. Collapses to two-up then one-up, with the table spanning full width once wrapped. */
.kpi-teams { display: grid; grid-template-columns: 1fr 1fr 1.4fr; gap: 1.1rem; align-items: start; margin-top: 1.1rem; }
.kpi-panel { padding: 1.25rem 1.4rem; }
.kpi-empty { color: var(--bs-muted); font-size: 0.9rem; margin: 0; }

.kpi-pipeline { list-style: none; margin: 0; padding: 0; }
.kpi-pipeline__row {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    padding: 0.6rem 0;
    border-bottom: 1px solid #eef0f3;
}
.kpi-pipeline__row:last-child { border-bottom: none; }
/* RAG indicator: colour only — the status label is carried on title/aria-label, not shown. */
.kpi-pipeline__rag { flex: 0 0 auto; width: 0.85rem; height: 0.85rem; border-radius: 50%; }
.kpi-pipeline__rag--green { background: #1a7f3c; }
.kpi-pipeline__rag--amber { background: #9a6700; }
.kpi-pipeline__rag--red { background: #b3261e; }
.kpi-pipeline__rag--neutral { background: #4b5563; }
.kpi-pipeline__label { flex: 1; font-weight: 600; }
.kpi-pipeline__count {
    font-family: var(--bs-font-display);
    font-size: 1.5rem;
    line-height: 1;
    color: var(--bs-ink);
    min-width: 1.5ch;
    text-align: end;
}

.kpi-mix { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; }
.kpi-mix__cell { display: block; text-decoration: none; color: inherit; text-align: center; border: 1px solid var(--bs-line); border-radius: 10px; padding: 0.75rem 0.5rem; transition: border-color 0.12s ease, box-shadow 0.12s ease; }
.kpi-mix__n { font-family: var(--bs-font-display); font-size: 2rem; line-height: 1; color: var(--bs-ink); }
.kpi-mix__l { font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.6rem; color: var(--bs-muted); margin-top: 0.3rem; }

.kpi-leaders { margin-top: 0.25rem; }
.kpi-leaders thead th { padding: 0.5rem 0.6rem; }
.kpi-leaders tbody td { padding: 0.55rem 0.6rem; }
/* Shared "clickable entity" affordance, used by the dashboard's team-leader names and the
   notification inbox's message links. Style them as deliberate, clickable entities rather than raw
   underlined hyperlinks: strong text with a trailing arrow that slides in on hover. The accent
   colour and the arrow reveal live in the end-of-file affordance block (green in light, blue in
   dark), so they resolve per theme and survive the dark-surface reset. */
.entity-link {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    color: var(--bs-text);
    font-weight: 600;
    text-decoration: none;
    transition: color 0.12s ease;
}
.entity-link::after {
    content: "\2192";
    font-weight: 400;
    opacity: 0;
    transform: translateX(-4px);
    transition: opacity 0.12s ease, transform 0.12s ease;
}
.entity-link:focus-visible {
    color: var(--hover-ring);
    outline: 2px solid var(--hover-ring);
    outline-offset: 2px;
    border-radius: 3px;
}
@media (prefers-reduced-motion: reduce) {
    .entity-link,
    .entity-link::after { transition: none; }
}
/* The in-sentence counterpart to .entity-link. An .entity-link cannot be used inside running prose:
   its inline-flex box reserves the arrow's width even while the arrow is hidden, which shows up as a
   gap mid-sentence, and it refuses to break across lines, so a long link gets pushed onto its own
   row. A prose link stays inline and KEEPS its underline — surrounded by text, the underline is the
   only thing marking it as clickable. Colour matches the help manual's body links, which is where
   this codebase already settled the question of what a link inside a paragraph looks like. */
.text-link {
    color: var(--bs-hi-vis-press);
    text-underline-offset: 2px;
}
.text-link:hover { color: var(--bs-text); }
.text-link:focus-visible {
    outline: 2px solid var(--hover-ring);
    outline-offset: 2px;
    border-radius: 3px;
}

@media (max-width: 980px) {
    .kpi-teams { grid-template-columns: 1fr 1fr; }
    .kpi-panel--leaders { grid-column: 1 / -1; }
}
@media (max-width: 560px) { .kpi-teams { grid-template-columns: 1fr; } }

/* Status badges */
.badge-flag {
    display: inline-block;
    font-family: var(--bs-font-mono);
    font-size: 0.64rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    padding: 0.2rem 0.45rem;
    border-radius: 6px;
    margin-inline-end: 0.3rem;
}
.badge-flag--stop { background: #fdecec; color: #b3261e; border: 1px solid #f3b4b4; }
.badge-flag--credit { background: #fff4e0; color: #9a6700; border: 1px solid #f3d9a8; }
.badge-flag--ok { background: #d7efdf; color: #146c37; border: 1px solid #8ecaa1; }
.badge-flag--pending { background: #fff4e0; color: #9a6700; border: 1px solid #f3d9a8; }
.badge-flag--authorised { background: #d7efdf; color: #146c37; border: 1px solid #8ecaa1; }
.badge-flag--declined { background: #fdecec; color: #b3261e; border: 1px solid #f3b4b4; }
/* Cancelled is terminal like declined, but it is a withdrawal rather than a refusal — it borrows
   the neutral treatment so the Status column never reads a cancellation as a rejection. */
.badge-flag--cancelled { background: #eef0f3; color: #4b5563; border: 1px solid #d7dbe0; }
/* RAG status colours */
.badge-flag--green { background: #d7efdf; color: #146c37; border: 1px solid #8ecaa1; }
.badge-flag--amber { background: #fff4e0; color: #9a6700; border: 1px solid #f3d9a8; }
.badge-flag--red { background: #fdecec; color: #b3261e; border: 1px solid #f3b4b4; }
.badge-flag--neutral { background: #eef0f3; color: #4b5563; border: 1px solid #d7dbe0; }
/* Due-date semantics (overdue / due-soon), same weight as --red / --amber above. Already referenced
   by templates/workshop/equipment/show.html.twig, _equipment_card.html.twig, index.html.twig and
   templates/workforce/training/show.html.twig, index.html.twig before this rule existed — those
   chips were rendering colourless. Courtesy car checks (CheckStatus::chipClass, RSPH-178) use the
   same pair, which is what surfaced the gap. Do not delete these as unused. */
.badge-flag--danger { background: #fdecec; color: #b3261e; border: 1px solid #f3b4b4; }
.badge-flag--warning { background: #fff4e0; color: #9a6700; border: 1px solid #f3d9a8; }
/* Outside the RAG set on purpose: "all parts in" is not a health rating, it is a readiness one. Green
   would read as "job done" next to the stage badge, so this borrows the secondary-button blue (#02b3fe)
   tinted to chip weight — the same blue the running-clock dot uses for "this is live and in hand". */
.badge-flag--blue { background: #cfe8fa; color: #064e73; border: 1px solid #6fbde8; }
/* Task 12: which trade a rework car is back under, in the card's badge row rather than its
   technician line — a rework car can be traded but not yet crewed (queued on My Loading Queue, its
   only screen — see BoardAssignmentView's crew-state rule), and the badge row is the one place on the
   card that renders whether or not anyone is assigned. Violet outside the RAG set on purpose, same as
   `--blue` above: this reports a TRADE, not a health rating, and must read as its own category next to
   the status/parts/waiting badges rather than agreeing or disagreeing with any of them. */
.badge-flag--violet { background: #ede7fb; color: #4a2f9e; border: 1px solid #c7b8ef; }

/* RSPH-116: a work line signed off as finished. The row stays fully legible — its time still counts
   toward the phase — but drops back visually so a part-finished phase reads at a glance. Strikethrough
   is deliberately not used: the work happened, it is done, not cancelled. */
.phase-line-row--complete > td:first-child { opacity: 0.65; }
/* The sign-off stacks: badge, then who and when beneath it, then the way back. A column rather than a
   row because the three side by side made the completion cell wide enough to push the phase table out
   of its panel. */
.phase-line-signoff { display: flex; flex-direction: column; align-items: flex-start; gap: 0.2rem; margin-bottom: 0.3rem; }
.phase-line-signoff .text-secondary { font-size: 0.8em; line-height: 1.3; }
/* Keep "Complete phase" on one line — the column is narrow enough to break it otherwise. */
.data-table .phase-complete-action { white-space: nowrap; }

/* Status pill (larger, for headers) */
.status-pill {
    display: inline-block;
    font-weight: 700;
    font-size: 0.82rem;
    padding: 0.3rem 0.7rem;
    border-radius: 999px;
}
.status-pill--green { background: #1a7f3c; color: #fff; }
/* Deeper orange (not the #ffb81c golden-yellow it was): that yellow sat too close to the yellow
   .reg-plate beside it in the job header. Dark ink text keeps ~6.4:1 contrast (AA). */
.status-pill--amber { background: #e07a12; color: var(--bs-ink-2); }
.status-pill--red { background: #b3261e; color: #fff; }
.status-pill--neutral { background: #4b5563; color: #fff; }

/* In the job header, the status pill and the due-date flag share one shape: the
   rounded-rectangle of the flag (6px), keeping each its own colour. */
.job-status-block .status-pill { border-radius: 6px; }

/* …and one size. The two read as a pair — the workflow status and whether it is running to time — so
   the flag matches the pill's type size and box rather than sitting beside it as an afterthought. It
   keeps its mono uppercase face, which is what still tells the two apart. The border the flag carries
   and the pill does not comes out of its padding, so both boxes end up the same height. */
.job-status-block .badge-flag {
    font-size: 0.82rem;
    padding: calc(0.3rem - 1px) calc(0.7rem - 1px);
}

/* Detail header + grid */
.detail-head { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.4rem; }

/* Job identity pair: the WIP number and the vehicle's plate are the two things that identify a job,
   so they lead the header as a matched, high-emphasis pair. The WIP is set in the workshop display
   face (signage lettering); the plate is rendered as a physical, embossed number plate. */
.detail-head .page-title--wip {
    font-family: var(--bs-font-display);
    font-weight: 400;
    font-size: 2.5rem;
    line-height: 0.92;
    letter-spacing: 0.02em;
}
/* Yellow rear-plate face with near-black text and border. Colours are pinned (not theme tokens)
   so the plate reads identically in both light and dark themes. */
.reg-plate {
    display: inline-flex;
    align-items: center;
    font-family: var(--bs-font-mono);
    font-weight: 700;
    font-size: 1.2rem;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: #15171a;
    background: linear-gradient(180deg, #ffe14d 0%, #ffd400 100%);
    border: 2px solid #15171a;
    border-radius: 7px;
    padding: 0.32rem 0.72rem;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45), 0 1px 2px rgba(0, 0, 0, 0.18);
    line-height: 1.05;
}
/* On the job header the plate doubles as the route back to the customer record. It keeps the plate
   face exactly — no link colour, no underline — and earns its affordance from the object metaphor
   instead: hovering lifts it off the page the way a real plate catches the light. The focus ring is
   drawn outside the plate's own black border so it reads against the yellow. */
a.reg-plate {
    text-decoration: none;
    transition: transform 120ms ease, box-shadow 120ms ease;
}
a.reg-plate:hover {
    transform: translateY(-1px);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55), 0 3px 6px rgba(0, 0, 0, 0.26);
}
a.reg-plate:focus-visible {
    outline: 2px solid var(--hover-ring);
    outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
    a.reg-plate { transition: none; }
    a.reg-plate:hover { transform: none; }
}
.detail-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem 1.5rem; margin: 0; }
.detail-grid dt { font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.64rem; color: var(--bs-muted); }
.detail-grid dd { margin: 0.15rem 0 0; font-size: 0.98rem; }
/* A detail row whose value is not a value yet, but a pointer to where the answer is — the key-to-key
   panel's delivery date before the repair has started. Set down and muted so it never reads as the
   figure it stands in for. Beats the bare `dd` rule on specificity, so the size sticks. */
.detail-grid dd.is-pending { font-size: 0.82rem; color: var(--bs-muted); line-height: 1.35; }
.two-col { display: grid; grid-template-columns: 1.4fr 1fr; gap: 1.25rem; align-items: start; }
/* Grid items default to min-width:auto, so a column refuses to shrink below its
   content's min-content width. A wide table inside one then pushes the whole
   page into horizontal scroll instead of scrolling within its own .table-scroll
   (RSPH-103: the customer-detail claims panel forced the page to 1312px at a
   1280 viewport). min-width:0 lets the column shrink and hands the overflow
   back to the scroll container that is there to absorb it. */
.two-col > * { min-width: 0; }
/* One column when the second one has nothing to put in it. A .two-col holding a single child leaves
   its 1fr track standing empty, which is the same hole this grid exists to avoid. */
.two-col--single { grid-template-columns: minmax(0, 1fr); }
/* Customer detail: the claims table (vehicle/insurer/status/date) is the column that actually needs
   the room, so it gets the larger track instead of the default's left-favoured 1.4fr/1fr split. */
.two-col--claims-wide { grid-template-columns: 1fr 1.3fr; }
@media (max-width: 860px) { .two-col { grid-template-columns: 1fr; } .detail-grid { grid-template-columns: 1fr 1fr; } }

/* ---- RSPH-172 Estimate detail ----
   The page is a single full-width column. It used to be a 1.4fr/1fr grid with the summary in a
   right-hand rail, and that rail was the defect: the lines table needs 829px to draw its seven
   columns and the grid never gave it more than 593px — at 1280, 1440, 1920 AND 2560 — so Labour and
   Parts lived behind a horizontal scrollbar while a 465x906px block of the same page stood empty.
   The summary is now the figures strip below, which costs no width at all.

   min-width:0 stays: the wide line table must scroll inside its own container rather than stretching
   the page, which is what keeps the narrow breakpoint honest. */
.estimate-detail__lines,
.estimate-detail__supplements,
.estimate-settings { min-width: 0; }
.estimate-detail__lines,
.estimate-settings { margin-bottom: 1.25rem; }
.estimate-detail__supplements > .section-heading { margin-bottom: 0.75rem; }

/* A subheading inside a panel — "Hours by phase", "Add line". Replaces a hairline <hr> plus an
   <h2>: the rule was drawing a second horizontal line directly under the table's own bottom border. */
.estimate-detail__subheading {
    font-size: 0.9rem; font-weight: 700; color: var(--bs-text);
    margin: 1.4rem 0 0.7rem; padding-top: 1.1rem; border-top: 1px solid var(--bs-line);
}

/* The estimate's headline numbers. Wears .panel for its surface, so light/dark theming and the
   border radius come from there and this only owns the layout. The total is separated by a rule and
   set large — it is the number the page is read for. */
.estimate-figures {
    display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 2rem;
    padding: 1.15rem 1.4rem; margin-bottom: 1.25rem;
}
.estimate-figures__set {
    display: flex; flex-wrap: wrap; gap: 0.9rem 2.2rem; margin: 0; flex: 1 1 22rem;
}
.estimate-figures__item dt {
    font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.1em;
    font-size: 0.62rem; color: var(--bs-muted); white-space: nowrap;
}
.estimate-figures__item dd { margin: 0.2rem 0 0; font-size: 1rem; font-variant-numeric: tabular-nums; }
.estimate-figures__total {
    display: flex; flex-direction: column; flex: 0 0 auto;
    padding-inline-start: 2rem; border-inline-start: 1px solid var(--bs-line);
}
.estimate-figures__total-label {
    font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.1em;
    font-size: 0.62rem; color: var(--bs-muted);
}
.estimate-figures__total-value {
    font-size: 1.75rem; font-weight: 700; letter-spacing: -0.015em; line-height: 1.15;
    margin-top: 0.15rem; font-variant-numeric: tabular-nums;
}
.estimate-figures__note { font-size: 0.76rem; color: var(--bs-muted); margin-top: 0.2rem; }
.estimate-figures__meta {
    flex: 1 1 100%; margin: 0; padding-top: 0.85rem; border-top: 1px solid var(--bs-line);
    font-size: 0.82rem; color: var(--bs-muted);
}
/* A supplement's strip carries four figures and no meta line, and sits inside a panel rather than
   being one, so it drops the surface and keeps only the arrangement. */
.estimate-figures--compact {
    background: none; border: 0; box-shadow: none; border-radius: 0;
    padding: 1rem 0 0; margin-bottom: 0; border-top: 1px solid var(--bs-line);
}
/* A supplement's total is a part of the estimate, not the estimate — at the parent's size the two
   competed and the eye landed on whichever was lower down the page. */
.estimate-figures--compact .estimate-figures__total-value { font-size: 1.2rem; }
@media (max-width: 700px) {
    .estimate-figures__total { padding-inline-start: 0; border-inline-start: 0; flex-basis: 100%; }
}

/* Hours by phase. Five phases as a two-column definition list ran to ~180px of page for five short
   numbers; as chips they are one line. */
.phase-chips { display: flex; flex-wrap: wrap; gap: 0.45rem; margin: 0; padding: 0; list-style: none; }
.phase-chip {
    display: inline-flex; align-items: baseline; gap: 0.5rem;
    border: 1px solid var(--bs-line); border-radius: 999px; padding: 0.28rem 0.75rem;
}
.phase-chip__label {
    font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.08em;
    font-size: 0.62rem; color: var(--bs-muted);
}
.phase-chip__value { font-size: 0.88rem; font-weight: 600; font-variant-numeric: tabular-nums; }

/* The draft-only editors — labour rate, paint & materials, insurance claim — as one panel of three
   columns instead of a vertical stack in the old rail. Each stays its own <form> because each posts
   to its own endpoint; the grid is what makes them read as one set. */
/* Two columns, not three: paint & materials owns three stacked fields and a mode toggle, so beside
   two single-field forms it set the panel's height and left both neighbours sitting over a hole. It
   takes the second column outright and the short forms stack down the first. Placing it explicitly
   rather than by source order means it lands in the same place whether or not the viewer has the
   rate permission that draws the form above it. */
.estimate-settings__grid {
    display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 1.4rem 1.75rem; align-items: start;
}
.estimate-settings__form { display: flex; flex-direction: column; align-items: flex-start; gap: 0.6rem; }
.estimate-settings__form .field { width: 100%; display: grid; gap: 0.35rem; }
.estimate-settings__form label,
.estimate-settings__legend {
    font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.1em;
    font-size: 0.68rem; color: var(--bs-muted);
}
/* The .field wrapper in the markup is load-bearing, not decoration: the dark theme reaches these
   controls through `[data-bs-theme="dark"] .field input/select`, which outranks the #fff below.
   Drop the wrapper and the inputs stay white on a dark surface. */
.estimate-settings__form input,
.estimate-settings__form select {
    width: 100%; font-family: var(--bs-font-ui); font-size: 0.92rem; color: var(--bs-text);
    padding: 0.5rem 0.65rem; border: 1.5px solid var(--bs-line); border-radius: 10px;
    background-color: #fff;
}
.estimate-settings__form input:focus,
.estimate-settings__form select:focus {
    outline: none; border-color: var(--bs-hi-vis); box-shadow: 0 0 0 4px rgba(0, 222, 145, 0.18);
}
.estimate-settings__form--pm { grid-column: 2; grid-row: 1 / span 3; }
@media (max-width: 860px) {
    .estimate-settings__grid { grid-template-columns: minmax(0, 1fr); }
    .estimate-settings__form--pm { grid-column: 1; grid-row: auto; }
}
.estimate-settings__form--pm .pm-modes { display: flex; gap: 1.1rem; margin-top: 0.1rem; }
.estimate-settings__form--pm .radio-inline {
    display: inline-flex; align-items: center; gap: 0.4rem;
    font-size: 0.85rem; font-weight: 500; color: var(--bs-text); cursor: pointer;
    font-family: var(--bs-font-ui); text-transform: none; letter-spacing: normal;
}
.estimate-settings__form--pm .radio-inline input[type="radio"] {
    width: auto; margin: 0; padding: 0; border: 0; background: none; box-shadow: none;
    accent-color: var(--bs-hi-vis);
}
.estimate-settings__form [data-paint-materials-mode-target][hidden] { display: none; }

/* A toolbar inside a panel — the supplement's title, status and its own actions. */
.toolbar--panel { margin-bottom: 1rem; align-items: flex-start; }
.toolbar--panel .detail-head { margin-bottom: 0; }
.toolbar--panel .section-heading { margin-bottom: 0; }

.supplement-panel__empty { margin: 0; }

/* What the workshop typed, kept as they typed it — line breaks in a damage note carry meaning. */
.additional-finding__note { white-space: pre-line; margin: 0; }

/* A supplement panel is a scroll anchor: raising one, or opening one by URL, jumps the page here
   (RSPH-132). scroll-margin keeps it off the top edge on arrival. It carries tabindex="-1" so focus
   can follow the scroll for keyboard and screen-reader users, which means suppressing the :focus
   outline a mouse user never asked for while keeping the :focus-visible one that a keyboard user
   needs. The flag is the arrival cue — an accent ring that fades in as the scroll settles and is
   dropped a moment later, layered over .panel's own shadow rather than replacing it. */
.supplement-panel { scroll-margin-top: 1.5rem; transition: box-shadow 900ms ease; margin-bottom: 1.25rem; }
.supplement-panel:focus { outline: none; }
.supplement-panel:focus-visible { outline: 2px solid var(--bs-hi-vis); outline-offset: 2px; }
.supplement-panel--flagged { box-shadow: 0 1px 2px rgba(16,24,40,0.04), 0 0 0 3px rgba(0,222,145,0.55); }
@media (prefers-reduced-motion: reduce) {
    .supplement-panel { transition: none; }
}

/* Send-for-approval split button: a primary action plus a caret that reveals delivery channels
   (email insurer/customer, download PDF) via a native <details> menu — no JS, CSRF kept per form.
   The two halves must read as one control: align-items:stretch + flex wrappers make the caret take
   the main button's exact height, and hovering either half lights up both. */
.send-split { position: relative; display: inline-flex; align-items: stretch; }
.send-split > form { display: flex; }
.send-split__more { display: flex; }
.send-split__main { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.send-split__caret {
    display: inline-flex; align-items: center; justify-content: center;
    padding: 0 0.6rem; font-size: 0.7rem; line-height: 1;
    border-top-left-radius: 0; border-bottom-left-radius: 0;
    border-left: 1.5px solid rgba(0,0,0,0.14);
    list-style: none; cursor: pointer;
}
.send-split__caret::-webkit-details-marker { display: none; }
.send-split:hover .send-split__main,
.send-split:hover .send-split__caret,
.send-split__more[open] .send-split__caret { background: var(--bs-hi-vis-press); }
.send-split__panel {
    position: absolute; top: calc(100% + 6px); right: 0; z-index: 20;
    min-width: 190px; padding: 0.35rem;
    background: #fff; border: 1px solid var(--bs-line); border-radius: 10px;
    box-shadow: 0 8px 24px rgba(16,24,40,0.14);
}
.send-split__panel form { margin: 0; }
.send-split__item {
    display: block; width: 100%; text-align: left;
    padding: 0.5rem 0.65rem; border: none; border-radius: 7px;
    background: transparent; color: var(--bs-text); font: inherit; cursor: pointer;
    text-decoration: none;
}
.send-split__item:hover { background: var(--bs-surface-2, #f3f5f8); }

/* Status-change form (inline) */
.status-form { display: flex; flex-direction: column; gap: 0.6rem; }
/* .dialog-modal--compact select rides along because the crew dialog sits OUTSIDE any form (it writes
   its pick into whichever form opened it), so it never inherits .status-form's control styling the
   way the Add-technician modal's own select does. */
.status-form select, .status-form input, .status-form textarea, .dialog-modal--compact select {
    width: 100%; font-family: var(--bs-font-ui); font-size: 0.92rem; padding: 0.55rem 0.7rem;
    border: 1.5px solid var(--bs-line); border-radius: 9px; background: #fff;
}

/* RSPH-147: the rectification reason capture, revealed under the status row when Rectification Work
   is the chosen status.

   The left rail is --due-red, the same red the Rectification status pill and RAG Red already carry,
   so the group reads as belonging to the rework path rather than as a second generic form. That is
   the one piece of colour spent here; everything else is the panel's existing type and spacing.

   The `flex: 0 0 auto` reset is the trap .dialog-modal__card documents above: .clock-panel__field
   carries `flex: 1 1 9rem`, which is a width basis in the row-direction clocking form but becomes a
   MINIMUM HEIGHT in a flex column like this one — without the reset the label/control pairs float
   apart with a 9rem gap beneath each. */
.status-form__reason {
    display: flex; flex-direction: column; gap: 0.6rem;
    padding: 0.7rem 0.85rem; margin-top: 0.1rem;
    border-inline-start: 3px solid var(--due-red);
    border-radius: 0 9px 9px 0;
    background: color-mix(in srgb, var(--due-red) 5%, transparent);
}
/* The display value in the class above beats the `hidden` attribute, exactly as .board-modal__status
   documents — without this the group renders on every status, not just Rectification. */
.status-form__reason[hidden] { display: none; }
.status-form__reason .clock-panel__field { flex: 0 0 auto; }
.status-form__reason textarea { resize: vertical; min-height: 3.4rem; }

/* Why this car has been back, newest first. The meta line takes the mono micro-label voice the rest
   of the workshop UI uses for data (timestamp, the stage it failed at, who sent it back) so a run of
   entries scans as a column — reading "three times, all from Quality Control" is the whole point of
   keeping the history. */
.rectification-history { margin: 0 0 0.9rem; }
.rectification-history__list { list-style: none; margin: 0.4rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.rectification-history__item {
    display: flex; flex-direction: column; align-items: flex-start; gap: 0.25rem;
    padding-inline-start: 0.7rem;
    border-inline-start: 2px solid color-mix(in srgb, var(--due-red) 35%, transparent);
}
.rectification-history__meta {
    font-family: var(--bs-font-mono); font-size: 0.72rem; letter-spacing: 0.04em;
    color: var(--bs-muted);
}
.rectification-history__note { margin: 0; font-size: 0.85rem; color: var(--bs-text); }

/* RSPH-55/per-technician clock controls: the technician self-clock form (trade + optional work
   lines) on the job page. The status picker and the crew bubbles' play/pause/stop each submit their
   own form directly, outside .clock-panel — see job-crew in show.html.twig. */
.clock-panel { flex-direction: row; flex-wrap: wrap; align-items: flex-end; gap: 0.75rem; }
.clock-panel__field { display: flex; flex-direction: column; gap: 0.3rem; flex: 1 1 9rem; min-width: 0; margin: 0; }
.clock-panel__label { font-size: 0.72rem; font-weight: 600; color: var(--bs-muted); text-transform: uppercase; letter-spacing: 0.04em; }

/* The `1 1 9rem` above is a WIDTH basis, written for the row-wrapping .clock-panel. In a flex COLUMN
   the same shorthand makes 9rem a minimum HEIGHT and the field grows to fill its container, so the
   label/control pairs float apart with huge empty gaps beneath them. The Add-technician modal stacks
   two of these in a column card, so reset them here instead of repeating the patch per field.
   Scoped to the card, NOT to .status-form: `.status-form.clock-panel` is the row-direction clocking
   form, where the 9rem basis is doing its intended job and must survive. */
.dialog-modal__card .clock-panel__field,
/* Same trap, same fix, for the status form's crew dialog: --compact makes its panel a flex column
   too, so the 9rem width basis would become a 9rem minimum height there as well. */
.dialog-modal--compact .clock-panel__field { flex: 0 0 auto; }

/* RSPH-92/99: optional work-line checkboxes. The field spans the full panel width so the list
   drops to its own row beneath the trade/person selects (see the template note); each check lays
   its box beside its label instead of stacking. */
.clock-panel__field--lines { flex: 1 1 100%; }
.clock-panel__lines { display: flex; flex-direction: column; gap: 0.4rem; }
.clock-panel__check { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; color: var(--bs-text); }
/* Undo the full-width bordered text-field sizing .status-form input applies (this form is a
   .status-form) so the checkbox renders native size beside its label instead of stretching the
   whole row — same reset as .form-grid input[type="checkbox"]. */
.clock-panel__check input[type="checkbox"] { flex: 0 0 auto; width: auto; margin: 0; padding: 0; border: 0; border-radius: 0; box-shadow: none; }

/* Who is on the car right now, above the trade/person pickers. Reuses .clock-dot so a running clock
   reads the same here as on the Live Job Status cards it mirrors. */
.clock-panel__on-job { list-style: none; margin: 0 0 0.75rem; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; }
.clock-panel__on-job-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.82rem; color: var(--bs-text); }

/* Timeline */
.timeline { list-style: none; margin: 0; padding: 0; }
.timeline li { position: relative; padding: 0 0 1.1rem; padding-inline-start: 1.4rem; border-inline-start: 2px solid var(--bs-line); }
.timeline li:last-child { border-inline-start-color: transparent; padding-bottom: 0; }
.timeline li::before {
    content: ''; position: absolute; inset-inline-start: -6px; top: 3px; width: 10px; height: 10px;
    border-radius: 50%; background: var(--bs-hi-vis); box-shadow: 0 0 0 3px rgba(0,222,145,0.18);
}
.timeline__when { font-family: var(--bs-font-mono); font-size: 0.66rem; letter-spacing: 0.08em; color: var(--bs-muted); }
.timeline__what { font-size: 0.95rem; margin-top: 0.15rem; }
.timeline__note { color: var(--bs-muted); font-size: 0.88rem; }

/* Checkbox row */
.check-row { display: flex; align-items: center; gap: 0.5rem; }
.check-row input { width: auto; }
.check-row label { font-family: var(--bs-font-ui); text-transform: none; letter-spacing: 0; font-size: 0.92rem; color: var(--bs-text); }
label.check-row { font-family: var(--bs-font-ui); text-transform: none; letter-spacing: 0; font-size: 0.92rem; color: var(--bs-text); cursor: pointer; padding: 0.2rem 0; }

/* Access-group form */
.section-heading { font-size: 1rem; font-weight: 700; margin: 0 0 0.75rem; color: var(--bs-text); }
/* A section heading with a count beside it (My Loading Queue's List pane, Live Job Status's grouped
   Table pane). Written as a modifier rather than folded into .section-heading itself, same reasoning
   as .estimate-bucket__heading: it must live on the <h2>, not on a wrapper, because the dark theme's
   accent bar pads .section-heading itself and would outrank a gap set anywhere else. Without this,
   the count sits jammed against the heading with no space at all. */
.section-heading--counted { display: flex; align-items: baseline; gap: 0.6rem; }
.section-intro { margin: 0; color: var(--bs-muted); font-size: 0.9rem; }
.toolbar--section { margin-top: 2rem; }
.data-table__num { text-align: end; font-variant-numeric: tabular-nums; }
/* The heading over those figures, for the same reason `.data-table thead th.data-table__actions`
   exists: `.data-table thead th` sets `text-align: start` and outranks a bare class, so AGE and TOTAL
   printed at the far left of columns whose every cell is at the far right — TOTAL sat over the end of
   the PAYER column and read as its label. Reaching `thead th` is what wins the alignment back; a
   second `.data-table__num { text-align: end }` would not, however late it were written. */
.data-table thead th.data-table__num { text-align: end; }
.status-link { text-decoration: none; }

/* Client-side table search + sort (table_controller.js) */
.table-tools {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    /* The panel has no padding and the table is full-bleed, so inset the tools row from the panel
       edges to give the search box breathing room from the container corner. */
    margin: 1rem 1rem 1rem;
}
/* The tools row on a page with no panel to sit inside (My Loading Queue): drop the panel inset so the
   search box lines up with the page title, and keep the count beside the box rather than flung to the
   far edge of a 1600px board. */
.table-tools--page { margin-inline: 0; justify-content: flex-start; }
/* The tools row above the Live Job Status column board: three children rather than two, so
   space-between would strand the "3 / 12" count in the middle of the row instead of beside the box it
   belongs to. Pack them left and let .park-strip's own auto margin push the chips to the far end. */
.table-tools--board { justify-content: flex-start; }

.table-tools__search {
    width: min(420px, 100%);
    font-family: var(--bs-font-ui);
    font-size: 0.92rem;
    color: var(--bs-text);
    padding: 0.6rem 0.9rem 0.6rem 2.4rem;
    border: 1.5px solid var(--bs-line);
    border-radius: 10px;
    background-color: #fff;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%239aa1ab' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='7' cy='7' r='5'/%3E%3Cline x1='10.5' y1='10.5' x2='15' y2='15'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: 0.8rem center;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.table-tools__search::placeholder { color: #aab0bb; }
.table-tools__search:focus {
    outline: none;
    border-color: var(--bs-hi-vis);
    box-shadow: 0 0 0 4px rgba(0, 222, 145, 0.18);
}
/* Search box + any column facet dropdowns share one left-aligned row; the count sits opposite.
   `flex: 1 1 auto` is load-bearing: without a definite width here the search box's own
   `width: min(420px, 100%)` resolves against a shrink-to-fit parent, takes the whole row and wraps
   every facet onto a line of its own. */
.table-tools__controls {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
    flex: 1 1 auto;
}
.table-tools__filter {
    font-family: var(--bs-font-ui);
    font-size: 0.9rem;
    color: var(--bs-text);
    padding: 0.55rem 2.1rem 0.55rem 0.85rem;
    border: 1.5px solid var(--bs-line);
    border-radius: 10px;
    background-color: #fff;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%239aa1ab' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='2,4 6,8 10,4'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.75rem center;
    appearance: none;
    cursor: pointer;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.table-tools__filter:focus {
    outline: none;
    border-color: var(--bs-hi-vis);
    box-shadow: 0 0 0 4px rgba(0, 222, 145, 0.18);
}
.table-tools__count {
    font-family: var(--bs-font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--bs-muted);
    white-space: nowrap;
}
.data-table th.is-sortable { cursor: pointer; user-select: none; white-space: nowrap; }
.data-table th.is-sortable::after { content: '↕'; opacity: 0.35; margin-inline-start: 0.4em; font-size: 0.85em; }
.data-table th.is-sorted-asc::after { content: '↑'; opacity: 0.9; }
.data-table th.is-sorted-desc::after { content: '↓'; opacity: 0.9; }
.perm-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 1.75rem; }
.perm-area { border: 1px solid var(--bs-line); border-radius: 10px; padding: 0.75rem 0.9rem; margin: 0; min-width: 0; }
.perm-area legend {
    font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.1em;
    font-size: 0.66rem; color: var(--bs-muted); padding: 0 0.35rem;
}
.member-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.4rem 1rem; margin-bottom: 0.5rem; }

@media (max-width: 720px) { .perm-grid { grid-template-columns: 1fr 1fr; } .member-grid { grid-template-columns: 1fr; } }
@media (max-width: 480px) { .perm-grid { grid-template-columns: 1fr; } }

/* Forms */

/* Base field styling. Every text-like control inside the admin app shell (.shell) or the admin
   auth screens (.auth) gets the standard field look by default, so a control is never left with
   raw browser chrome just because it sits outside a styled container (.form-grid, .status-form,
   .inline-form, a bare .field, a class-less table-row <form>, …). Wrapped in :where() so its
   specificity is zero — every existing container/utility rule overrides it, and this only fills the
   gap for controls nothing else matches. Deliberately does NOT set width, so table-row and inline
   controls keep their natural size (sizes may differ by context; the look does not). It also does
   NOT reset `appearance` / draw a chevron: that stays scoped to .form-grid selects, because the
   arrow-only selects (.inline-form/.status-form/.filter-bar__field) use a `background` shorthand
   and no appearance reset, so a base chevron would strip their native arrow without replacing it.
   Scope is .shell/.auth only — marketing (.mk) and the customer portal (.pt-*) keep their own CSS. */
:where(.shell, .auth) :where(
    input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="image"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="color"]):not([type="range"]),
    select,
    textarea
) {
    font-family: var(--bs-font-ui);
    font-size: 0.95rem;
    padding: 0.6rem 0.7rem;
    border: 1.5px solid var(--bs-line);
    border-radius: 9px;
    background: #fff;
    color: var(--bs-text);
}
:where(.shell, .auth) :where(
    input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="image"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="color"]):not([type="range"]),
    select,
    textarea
):focus {
    outline: none;
    border-color: var(--bs-hi-vis);
    box-shadow: 0 0 0 4px rgba(0, 222, 145, 0.18);
}
/* Dark-mode counterpart of the base field styling (same zero-specificity :where() approach). The
   focus rule above is theme-independent, so it is not repeated here. */
:where([data-bs-theme="dark"]) :where(.shell, .auth) :where(
    input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="image"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="color"]):not([type="range"]),
    select,
    textarea
) {
    background: var(--surface-3);
    color: var(--bs-text);
    border-color: var(--bs-line);
}

/* Row-scale controls: a control that sits inline with an action button should match the button's
   height rather than the taller full-form field height the base rule gives. All three tiers are
   wrapped in :where() (zero specificity) and placed after the base rule, so they override it here
   while still losing to any real control class (.inline-form/.status-form/.table-tools__search keep
   their own look). Tier 1 gives data-table controls a regular .btn height. Tiers 2/3 drop to a small
   .btn--sm height wherever a control shares its row (data-table tr) or its form with a .btn--sm —
   e.g. the job view's inline "Save delay" form, whose inputs sit in .clock-panel__field labels. */
:where(.shell) :where(.data-table) :where(
    input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="image"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="color"]):not([type="range"]),
    select,
    textarea
) {
    font-size: 0.9rem;
    padding: 0.55rem 0.6rem;
    border-radius: 8px;
}
:where(.shell) :where(.data-table tr:has(.btn--sm), form:has(.btn--sm)) :where(
    input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="image"]):not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="color"]):not([type="range"]),
    select,
    textarea
) {
    font-size: 0.82rem;
    padding: 0.4rem 0.55rem;
    border-radius: 8px;
}

.form-card { max-width: 720px; padding: 1.75rem; }

/* Groups related fields inside a long form. Field labels are already the mono micro-label voice, so
   a section heading cannot outrank them by being another one — it uses the UI face in sentence case
   above a hairline, making the division itself the device. */
.form-section {
    margin: 2rem 0 1.25rem;
    padding-top: 1.5rem;
    border-top: 1px solid var(--bs-line);
}
.form-section__title {
    font-family: var(--bs-font-ui);
    font-size: 1rem;
    font-weight: 700;
    color: var(--bs-text);
    margin: 0;
}
.form-section__note { margin: 0.35rem 0 0; }

.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.1rem; }
.form-grid .field--full { grid-column: 1 / -1; }
/* .form-grid sets `display: grid`, which beats the UA's `[hidden] { display: none }` — the same trap
   .btn[hidden] and .estimate-settings__form [data-paint-materials-mode-target][hidden] document
   elsewhere. The Refinity material-match row toggles its "create new stock item" fields with plain
   `hidden`, so without this an author rule leaves a hidden create form on screen. */
[data-refinity-material-map-target="newFields"][hidden] { display: none; }

/* Adding an estimate line (RSPH-172). Twelve tracks so the fields can be sized by what they hold:
   an operation is prose, an hours value is four characters. Stacked full-width they ran to ~500px of
   page for six inputs. The spans are scoped to this variant — `.field--num` is a name any form could
   want, and outside here it should mean nothing. */
.form-grid--line { grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 0.9rem 1.1rem; }
/* Spans chosen so both rows break on the same grid lines — 6|9|12 above, 3|6|9|12 below. Sizing each
   field only by its content gave two rows whose edges lined up with nothing, which reads as an
   accident even when every field is the right width. */
.form-grid--line .field--op { grid-column: span 6; }
.form-grid--line .field--panel { grid-column: span 3; }
.form-grid--line .field--phase { grid-column: span 3; }
.form-grid--line .field--num { grid-column: span 3; }
.form-grid--line .field--search { grid-column: span 3; }
@media (max-width: 1100px) {
    .form-grid--line .field--op,
    .form-grid--line .field--panel,
    .form-grid--line .field--phase,
    .form-grid--line .field--search { grid-column: span 6; }
    .form-grid--line .field--num { grid-column: span 3; }
}
@media (max-width: 700px) {
    .form-grid--line { grid-template-columns: 1fr 1fr; }
    .form-grid--line .field--op,
    .form-grid--line .field--panel,
    .form-grid--line .field--phase,
    .form-grid--line .field--search { grid-column: 1 / -1; }
    .form-grid--line .field--num { grid-column: span 1; }
}

/* The stock form's charge-out answer (RSPH-142). Not another field — the figure the two boxes above
   are producing — so it is stated rather than boxed: the money at display size, the rule behind it
   underneath. A below-cost price takes the same red as the inventory table's markup and a job's
   parts table, because one fact must not look different on three screens. */
.price-preview__line { margin: 0.1rem 0 0.15rem; display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.6rem; }
.price-preview__amount { font-size: 1.45rem; font-weight: 600; line-height: 1.2; }
/* The rule behind the figure is a sentence, not a label, so it takes none of the .eyebrow
   uppercase treatment the heading above it wears. */
.price-preview__basis { color: var(--bs-muted); font-size: 0.9rem; }
.price-preview--loss .price-preview__amount { color: #b3261e; }
[data-bs-theme="dark"] .price-preview--loss .price-preview__amount { color: #ff9d97; }
.form-grid label { font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.68rem; color: var(--bs-muted); }
/* A <label> that WRAPS its control instead of sitting beside it. Use it wherever a `for` attribute
   cannot honestly be spelled — a field rendered by a macro called more than once on a page, or one
   inside a {% for %} loop where a unique id would have to be invented. A bare <label> sibling of an
   unlabelled input names nothing: assistive tech reads the field as unnamed and clicking the label
   does not focus it. The courtesy car modals (_loan_modal, _off_road_modal) already wrap; this is the
   same shape for a .form-grid .field.

   The eyebrow treatment MUST live on the inner span, not on the wrapper: `text-transform` and
   `letter-spacing` above are inherited properties and `.form-grid input` does not reset them, so a
   wrapping label styled by that rule makes the user type in uppercase at 0.1em tracking. */
.form-grid label.field__wrap { display: block; font-family: inherit; font-size: inherit; color: inherit; text-transform: none; letter-spacing: normal; }
.form-grid .field__label { display: block; font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.68rem; color: var(--bs-muted); }
/* The sentence under a field that qualifies it: why a control is locked, why a list is capped, why a
   technician cannot be removed. Live in four templates and defined in no stylesheet, so it inherited
   body size, body ink and the UA's <p> margins — louder than the 0.68rem uppercase mono label above
   the field it belongs to, and tall enough to knock its own grid row out of step with the column
   beside it. Sized and coloured like the other asides in the app (.price-preview__basis,
   .stage-rail__seam): a note ABOUT a field, never mistakable for another field. Colour comes from
   --bs-muted, so dark mode follows without a second rule. */
.form-hint { margin: 0.35rem 0 0; font-size: 0.8rem; line-height: 1.4; color: var(--bs-muted); }
.form-grid input, .form-grid textarea, .form-grid select,
.tech-modal__panel input, .tech-modal__panel textarea, .tech-modal__panel select {
    width: 100%;
    font-family: var(--bs-font-ui);
    font-size: 0.95rem;
    padding: 0.6rem 0.7rem;
    border: 1.5px solid var(--bs-line);
    border-radius: 9px;
    background: #fff;
    color: var(--bs-text);
}
.form-grid input:focus, .form-grid textarea:focus, .form-grid select:focus,
.tech-modal__panel input:focus, .tech-modal__panel textarea:focus, .tech-modal__panel select:focus {
    outline: none;
    border-color: var(--bs-hi-vis);
    box-shadow: 0 0 0 4px rgba(0,222,145,0.18);
}
/* Checkboxes and radios are not text fields: undo the full-width / bordered text-input sizing above
   so they render at their native size and sit inline with their labels (see .checkbox-field and
   .radio-list__item, which lay out the row). */
.form-grid input[type="checkbox"], .form-grid input[type="radio"] {
    width: auto;
    padding: 0;
    border: 0;
    border-radius: 0;
    box-shadow: none;
}
/* Prefixed input: a prefix addon (e.g. a currency symbol) joined to the start of a text input.
   Named to avoid Bootstrap's own .input-group (which sets flex-wrap: wrap and would wrap the field). */
.prefixed-input { display: flex; flex-wrap: nowrap; align-items: stretch; }
.prefixed-input__prefix {
    display: inline-flex;
    align-items: center;
    padding: 0 0.7rem;
    font-family: var(--bs-font-ui);
    font-size: 0.95rem;
    color: var(--bs-muted);
    background: #f4f6f8;
    border: 1.5px solid var(--bs-line);
    border-right: 0;
    border-radius: 9px 0 0 9px;
    white-space: nowrap;
}
.form-grid .prefixed-input input { flex: 1 1 auto; min-width: 0; border-radius: 0 9px 9px 0; }
/* Reset native select chrome so dropdowns match the text inputs, then draw our own chevron. */
.form-grid select,
.tech-modal__panel select {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    padding-right: 2.2rem;
    cursor: pointer;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.75rem center;
    background-size: 0.7rem;
}
.form-grid select:disabled {
    background-color: #f4f6f8;
    color: var(--bs-muted);
    cursor: not-allowed;
}

/* Address blocks (main / billing / delivery): the fieldset is a plain grid cell, and its
   "Same as main address" toggle is a left-aligned checkbox + label row — overriding both the
   uppercase field-label styling and the width:100% input rule that would otherwise stretch and
   centre the checkbox. */
.form-grid .address-block { border: 0; margin: 0; padding: 0; min-width: 0; }
/* A group of radios/checkboxes that needs an accessible group name has to be a <fieldset>/<legend>
   — a bare <label> with no `for` and no wrapped control names nothing at all. This strips the UA
   fieldset chrome so the group lays out exactly like the sibling .field divs, and dresses the legend
   in the same eyebrow treatment as `.form-grid label` (which cannot match a <legend>, and must not
   be widened to: .address-block's legend deliberately wears .section-heading instead). */
.form-grid .field-group { border: 0; margin: 0; padding: 0; min-inline-size: 0; }
.form-grid .field-group__legend {
    display: block;
    padding: 0;
    font-family: var(--bs-font-mono);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    font-size: 0.68rem;
    color: var(--bs-muted);
}
.form-grid .checkbox-field {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-family: var(--bs-font-ui);
    text-transform: none;
    letter-spacing: 0;
    font-size: 0.92rem;
    color: var(--bs-text);
    cursor: pointer;
    margin-bottom: 0.9rem;
}
.form-grid .checkbox-field input[type="checkbox"] {
    width: auto;
    flex: 0 0 auto;
    margin: 0;
    padding: 0;
}

/* Select2 (searchable dropdowns) restyled to match the form inputs. */
.form-grid .select2-container { width: 100% !important; }
.select2-container--default .select2-selection--single {
    height: auto;
    min-height: calc(1.4em + 1.2rem + 3px);
    display: flex;
    align-items: center;
    padding: 0.1rem 2rem 0.1rem 0.7rem;
    border: 1.5px solid var(--bs-line);
    border-radius: 9px;
    background: #fff;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
    width: 100%;
    padding: 0;
    margin: 0;
    line-height: 1.4;
    color: var(--bs-text);
    font-family: var(--bs-font-ui);
    font-size: 0.95rem;
}
.select2-container--default .select2-selection--single .select2-selection__placeholder { color: #aab0bb; }
.select2-container--default .select2-selection--single .select2-selection__arrow {
    position: absolute;
    top: 0;
    right: 0.6rem;
    height: 100%;
    display: flex;
    align-items: center;
}
.select2-container--disabled .select2-selection--single { background-color: #f4f6f8; cursor: not-allowed; }
.select2-container--default.select2-container--focus .select2-selection--single,
.select2-container--default.select2-container--open .select2-selection--single {
    border-color: var(--bs-hi-vis);
    box-shadow: 0 0 0 4px rgba(0, 222, 145, 0.18);
}
.select2-dropdown {
    border: 1.5px solid var(--bs-line);
    border-radius: 9px;
    font-family: var(--bs-font-ui);
    font-size: 0.92rem;
}
.select2-search--dropdown .select2-search__field {
    border: 1.5px solid var(--bs-line);
    border-radius: 7px;
    padding: 0.45rem 0.6rem;
}
.select2-container--default .select2-results__option--highlighted[aria-selected] {
    background: var(--bs-hi-vis);
    color: var(--bs-ink-2);
}
.select2-container--default .select2-results__option--selected { background: #eef0f3; }
.form-actions { display: flex; gap: 0.6rem; margin-top: 1.4rem; }
/* RSPH-75: top-of-card Save mirroring the bottom .form-actions. Right-aligned, sits above the
   first field with clear separation. Colour comes from .btn--primary, so it needs no dark-mode
   override of its own. */
.form-card__top-actions { display: flex; justify-content: flex-end; gap: 0.6rem; margin-bottom: 1.25rem; }
.form-error {
    margin: 0 0 1.1rem;
    padding: 0.7rem 0.9rem;
    border-radius: 9px;
    background: #fdecec;
    border: 1px solid #f3b4b4;
    color: #8f1d1d;
    font-size: 0.9rem;
}

.form-success {
    margin: 0 0 1.1rem;
    padding: 0.7rem 0.9rem;
    border-radius: 9px;
    background: #e8f8f1;
    border: 1px solid #9fe3c7;
    color: #0f6b48;
    font-size: 0.9rem;
}

/* Possible-duplicate warning (RSPH-156) — sits above the form fields on the customer and vehicle
   forms, drawn from the same partial whether it arrived live or because a save was held back.
   Red-toned on both tones (changed from an earlier amber/grey pair) so a duplicate is never mistaken
   for routine page chrome; a saturated border/text against a pale background keeps it vivid without
   the fill itself fighting the form's white cards. --possible stays a lighter tint of the same red
   so a stronger match still reads as more urgent. */
.duplicate-warning {
    margin: 0 0 1.1rem;
    padding: 0.75rem 0.9rem;
    border-radius: 9px;
    background: #fee2e2;
    border: 1px solid #ef4444;
    color: #b91c1c;
    font-size: 0.9rem;
}
.duplicate-warning--possible { background: #fef2f2; border-color: #f5a3a3; color: #c0392b; }

.duplicate-warning__title { margin: 0 0 0.55rem; font-weight: 600; }
.duplicate-warning__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.duplicate-warning__item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    flex-wrap: wrap;
    padding: 0.5rem 0.6rem;
    border-radius: 7px;
    background: rgba(255, 255, 255, 0.75);
}
.duplicate-warning--possible .duplicate-warning__item { background: #fff; }
.duplicate-warning__who { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; min-width: 0; }
.duplicate-warning__name { font-weight: 600; color: var(--bs-text); }
.duplicate-warning__detail { color: var(--bs-muted); font-size: 0.82rem; }
.duplicate-warning__reasons { display: inline-flex; gap: 0.3rem; flex-wrap: wrap; }
.duplicate-warning__reason {
    font-size: 0.66rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 0.12rem 0.4rem;
    border-radius: 999px;
    background: #ef4444;
    color: #fff;
    white-space: nowrap;
}
.duplicate-warning--possible .duplicate-warning__reason { background: #f5a3a3; color: #7a1f1f; }
.duplicate-warning__hint { margin: 0.6rem 0 0; font-size: 0.82rem; opacity: 0.85; }

@media (max-width: 620px) {
    .duplicate-warning__item { align-items: flex-start; flex-direction: column; }
}

@media (max-width: 620px) { .form-grid { grid-template-columns: 1fr; } }

/* DataTables (vehicles list) — align the generated search box, info and pagination with the
 * app theme on the white panel. DataTables 2.x emits dt-* class names. */
.dt-container { margin-top: 0.1rem; }
.dt-container .dt-layout-row { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; }
.dt-container .dt-search { margin-bottom: 0.75rem; margin-inline-start: auto; }
.dt-container .dt-search label { display: inline-flex; align-items: center; gap: 0.5rem; color: var(--bs-muted); font-size: 0.85rem; }
.dt-container .dt-search input {
    border: 1px solid var(--bs-line);
    border-radius: 8px;
    padding: 0.45rem 0.7rem;
    font: inherit;
    color: var(--bs-text);
}
.dt-container .dt-search input:focus {
    outline: none;
    border-color: var(--bs-hi-vis);
    box-shadow: 0 0 0 3px rgba(0, 222, 145, 0.18);
}
.dt-container .dt-info { color: var(--bs-muted); font-size: 0.85rem; padding-top: 0.85rem; }
.dt-container .dt-paging { padding-top: 0.6rem; }
.dt-container .dt-paging .dt-paging-button {
    padding: 0.35rem 0.7rem;
    border-radius: 8px;
    border: 1px solid transparent;
    color: var(--bs-text);
}
.dt-container .dt-paging .dt-paging-button.current,
.dt-container .dt-paging .dt-paging-button.current:hover {
    background: var(--bs-hi-vis);
    border-color: var(--bs-hi-vis);
    color: var(--bs-ink-2);
}
.dt-container .dt-paging .dt-paging-button:hover:not(.current):not(.disabled) {
    background: #f2f4f7;
    border-color: var(--bs-line);
}
.dt-container .dt-paging .dt-paging-button.disabled { color: var(--bs-muted); opacity: 0.5; }

/* Real-time new-message toasts (bottom-right), Mercure-appended into #toasts.
   Named .rs-toast (not the bare .toast Bootstrap already owns): bootstrap.min.css ships
   `.toast:not(.show){display:none}` at specificity (0,2,0), which beats any (0,1,0) override
   here regardless of stylesheet order, silently hiding every one of these — fixed by not
   colliding with Bootstrap's class in the first place, same fix as .dialog-modal vs .modal. */
.toasts { position: fixed; inset-block-end: 1rem; inset-inline-end: 1rem; display: flex; flex-direction: column; gap: 0.5rem; z-index: 1080; max-width: 340px; }
.rs-toast {
    display: flex; align-items: flex-start; gap: 0.5rem;
    background: #15171a; color: #fff; border-radius: 12px; padding: 0.7rem 0.85rem;
    box-shadow: 0 8px 24px rgba(16, 24, 40, 0.18);
    opacity: 0; transform: translateY(8px); transition: opacity .2s ease, transform .2s ease;
}
.rs-toast--in { opacity: 1; transform: translateY(0); }
.rs-toast--out { opacity: 0; transform: translateY(8px); }
/* Tone accents for 'success'/'error' flashes. Colours picked to read on the toast's always-dark
   surface (unlike .form-error/.form-success, which sit on a light card background) — success reuses
   the brightened Completed-tick green, error reuses --due-red's dark-mode value. */
.rs-toast--success { border-left: 3px solid #12b76a; }
.rs-toast--error { border-left: 3px solid #ff8a80; }
.rs-toast__body { display: flex; flex-direction: column; gap: 0.15rem; color: #fff; text-decoration: none; flex: 1; }
.rs-toast__title { font-size: 0.85rem; font-weight: 600; }
.rs-toast__meta { font-size: 0.78rem; color: #b9c0c9; }
.rs-toast__close { background: none; border: 0; color: #b9c0c9; font-size: 1.15rem; line-height: 1; cursor: pointer; padding: 0 0.15rem; }
.rs-toast__close:hover { color: #fff; }

/* Message view + crew quick-message buttons */
.message-body { white-space: pre-wrap; margin: 0.5rem 0 0; }
.message-photo { max-width: 100%; height: auto; border-radius: 10px; border: 1px solid var(--bs-line); margin-top: 0.5rem; }
.crew-message-list { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.notif__chip { font-size: 0.7rem; color: var(--bs-muted); }

/* Filter bar (Jobs / Vehicles list filters) */
.filter-bar { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.75rem 1rem; margin-bottom: 1rem; }
.filter-bar__field { display: flex; flex-direction: column; gap: 0.25rem; }
.filter-bar__field label {
    font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.1em;
    font-size: 0.66rem; color: var(--bs-muted);
}
.filter-bar__field select, .filter-bar__field input {
    font-family: var(--bs-font-ui); font-size: 0.9rem; color: var(--bs-text);
    padding: 0.5rem 0.65rem; border: 1.5px solid var(--bs-line); border-radius: 10px;
    background-color: #fff; min-width: 9.5rem;
}
.filter-bar__field select:focus, .filter-bar__field input:focus { outline: none; border-color: var(--bs-hi-vis); box-shadow: 0 0 0 4px rgba(0, 222, 145, 0.18); }
/* An inert filter field — the attended-hours date range on any period but Custom. Matches the
   .form-grid select:disabled treatment rather than inventing a second one. The label dims with it:
   a greyed input under a full-strength label reads as a rendering fault rather than a state. */
.filter-bar__field input:disabled,
.filter-bar__field select:disabled {
    background-color: #f4f6f8;
    color: var(--bs-muted);
    cursor: not-allowed;
}
.filter-bar__field:has(input:disabled) label,
.filter-bar__field:has(select:disabled) label { opacity: 0.55; }
.filter-bar__actions { display: flex; gap: 0.5rem; margin-inline-start: auto; align-self: flex-end; }
@media (max-width: 720px) { .filter-bar__actions { margin-inline-start: 0; } }

/* RSPH-128: the "viewing a dashboard bucket" chip — stands in for the filter bar on /jobs when a
   Live Work Situation tile was clicked. Same rhythm as the filter bar; the bucket keeps its RAG hue,
   and the clear link returns to the normal assigned board. */
.situation-filter { display: flex; align-items: center; flex-wrap: wrap; gap: 0.6rem; margin-bottom: 1rem; }
.situation-filter__label {
    font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.1em;
    font-size: 0.66rem; color: var(--bs-muted);
}
.situation-filter__bucket {
    font-family: var(--bs-font-ui); font-weight: 600; font-size: 0.85rem; color: var(--bs-ink);
    padding: 0.3rem 0.75rem; border: 1.5px solid var(--bs-line); border-radius: 999px; background: #fff;
}
.situation-filter__bucket--green { color: #1a7f3c; border-color: #1a7f3c; }
.situation-filter__bucket--amber { color: #9a6700; border-color: #e8a400; }
.situation-filter__bucket--red { color: #b3261e; border-color: #b3261e; }
.situation-filter__bucket--neutral { color: var(--bs-muted); }
.situation-filter__clear {
    font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.08em;
    font-size: 0.66rem; color: var(--bs-text); text-decoration: underline; text-underline-offset: 3px;
}
.situation-filter__clear:hover { color: var(--bs-ink); }
/* --bs-ink is near-black, so the hover would vanish against the dark surface. */
[data-bs-theme="dark"] .situation-filter__clear:hover { color: var(--heading); }
.situation-filter__clear:focus-visible { outline: 2px solid var(--bs-hi-vis); outline-offset: 2px; border-radius: 4px; }

/* Padded settings card that spans wider than the default 720px form — e.g. the Manufacturers grid,
   which needs the room for its two-column list of makes. */
.form-card--wide { max-width: 960px; }
/* Full-bleed card that keeps the form-card padding but drops the width cap, filling the content
   column (up to .app__main's 1200px) — for wide data tables like the Parts list, which needs the
   room to show every column without cramping. */
.form-card--full { max-width: 100%; }

/* Inline filter/search control living in a .toolbar (e.g. Manufacturers settings): matches the
   look of the list filter-bar inputs and grows to fill the row. */
.toolbar--filter { align-items: center; gap: 0.5rem; }
.toolbar-search {
    flex: 1 1 12rem;
    font-family: var(--bs-font-ui); font-size: 0.9rem; color: var(--bs-text);
    padding: 0.5rem 0.65rem; border: 1.5px solid var(--bs-line); border-radius: 10px;
    background-color: #fff;
}
.toolbar-search::placeholder { color: #6f7886; }
.toolbar-search:focus { outline: none; border-color: var(--bs-hi-vis); box-shadow: 0 0 0 4px rgba(0, 222, 145, 0.18); }

/* Countdown widget — per-phase remaining time ticker */
.countdown__time { font-size: 1.5rem; font-weight: 600; }
.countdown--over { color: var(--bs-danger); }

/* ---- My Loading Queue: the technician's personal work queue ----
   A priority-ordered stack of job cards. The card being actively worked lights up with the hi-vis
   accent and its clocked/remaining figures tick live (see queue_timer_controller). */
.queue { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.85rem; }
.queue-card {
    background: #fff;
    border: 1px solid var(--bs-line);
    border-left: 4px solid var(--bs-line);
    border-radius: 14px;
    padding: 1.15rem 1.3rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.5rem;
    flex-wrap: wrap;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
/* Active = on the clock. Accent rail + soft hi-vis glow, so a tech spots the live job at a glance. */
.queue-card--active {
    border-left-color: var(--bs-hi-vis);
    box-shadow: 0 0 0 1px var(--bs-hi-vis), 0 8px 22px -14px rgba(0,222,145,0.65);
}
.queue-card__head { display: flex; align-items: flex-start; gap: 1.25rem; min-width: 0; }
.queue-card__id { min-width: 0; }
.queue-card__vehicle { font-family: var(--bs-font-display); font-size: 1.9rem; line-height: 1; letter-spacing: 0.02em; margin: 0.3rem 0 0; color: var(--bs-ink); }
.queue-card__model { color: var(--bs-muted); font-size: 0.9rem; margin: 0.25rem 0 0; }
.queue-card__status { display: flex; flex-direction: column; align-items: flex-end; gap: 0.4rem; }

/* The signature: side-by-side live timers reading up (clocked) and down (remaining). */
.queue-card__timers { display: flex; align-items: center; gap: 1.75rem; margin-left: auto; }
.queue-timer { display: flex; flex-direction: column; gap: 0.2rem; text-align: right; }
.queue-timer__label { font-family: var(--bs-font-mono); font-size: 0.6rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--bs-muted); }
.queue-timer__value { font-family: var(--bs-font-display); font-size: 2rem; line-height: 1; color: var(--bs-ink); font-variant-numeric: tabular-nums; }
.queue-timer--remaining .queue-timer__value { color: var(--bs-hi-vis-press); }
.queue-timer__value--over { color: var(--bs-danger) !important; }
.queue-timer__value--muted { font-family: var(--bs-font-ui); font-size: 0.9rem; font-weight: 600; color: var(--bs-muted) !important; }

/* "On the clock" live tag — a quiet pulsing dot beside the pill on the active card. */
.queue-live { display: inline-flex; align-items: center; gap: 0.4rem; font-family: var(--bs-font-mono); font-size: 0.6rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--bs-hi-vis-press); font-weight: 700; }
.queue-live::before { content: ""; width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--bs-hi-vis); animation: queue-pulse 1.6s ease-in-out infinite; }
@keyframes queue-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
@media (prefers-reduced-motion: reduce) { .queue-live::before { animation: none; } }

.queue-card__open { font-family: var(--bs-font-mono); font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--bs-muted); text-decoration: none; white-space: nowrap; }
.queue-card__open:hover { color: var(--bs-hi-vis-press); }

@media (max-width: 720px) {
    .queue-card { align-items: flex-start; gap: 1rem; }
    .queue-card__timers { margin-left: 0; width: 100%; justify-content: space-between; gap: 1rem; }
    .queue-timer { text-align: left; }
}

/* ---- My Clock: the queue's "you, right now" card. Shares the .queue-card shell so the attendance
   clock reads as the header of the list below, not a separate panel. Rail colour tracks state:
   hi-vis when on a job (like an active queue-card), danger when idle, neutral when not clocked in. */
.clock-card {
    background: #fff;
    border: 1px solid var(--bs-line);
    border-left: 4px solid var(--bs-line);
    border-radius: 14px;
    padding: 1.15rem 1.3rem;
    margin-bottom: 1.1rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.5rem;
    flex-wrap: wrap;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.clock-card--active { border-left-color: var(--bs-hi-vis); box-shadow: 0 0 0 1px var(--bs-hi-vis), 0 8px 22px -14px rgba(0,222,145,0.65); }
.clock-card--idle { border-left-color: var(--bs-danger); }
.clock-card__id { min-width: 0; display: flex; flex-direction: column; gap: 0.3rem; }
.clock-card__state { font-family: var(--bs-font-display); font-size: 1.9rem; line-height: 1; letter-spacing: 0.02em; margin: 0; color: var(--bs-ink); }
.clock-card--idle .clock-card__state { color: var(--bs-danger) !important; }
.clock-card__detail { color: var(--bs-muted); font-size: 0.9rem; margin: 0; font-variant-numeric: tabular-nums; }
.clock-card__side { display: flex; align-items: center; gap: 1.75rem; margin-left: auto; }
.clock-card__side .btn { white-space: nowrap; }
.clock-timer { display: flex; flex-direction: column; align-items: flex-end; gap: 0.2rem; text-align: right; }
.clock-timer__value { font-family: var(--bs-font-display); font-size: 2rem; line-height: 1; color: var(--bs-ink); font-variant-numeric: tabular-nums; }
.clock-timer--idle .clock-timer__value { color: var(--bs-danger) !important; }

/* Shop-floor technician action bar (RSPH-102): the always-present quick-action buttons in a board
   toolbar. Each action is its own <form>, so lay them out inline and keep the labels on one line. */
.tech-action-bar { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.tech-action-bar form { margin: 0; }
.tech-action-bar .btn { white-space: nowrap; }

@media (max-width: 720px) {
    .clock-card { align-items: flex-start; gap: 1rem; }
    .clock-card__side { margin-left: 0; width: 100%; justify-content: space-between; }
    .clock-timer { align-items: flex-start; text-align: left; }
}

/* ---- Mobile spacing: reclaim horizontal room on small screens ---- */
@media (max-width: 720px) {
    .app__main { padding: 1.25rem 1rem; }
    /* Full-width primary actions read better than a stranded button on a phone. */
    .toolbar { align-items: stretch; }
}
@media (max-width: 480px) {
    .app__main { padding: 1rem 0.85rem; }
}

/* Wizard vehicle picker — a vertical list of selectable rows (estimate/job step 2). */
.radio-list { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.35rem; }
.radio-list__item {
    display: flex; align-items: center; gap: 0.6rem;
    padding: 0.6rem 0.8rem; border: 1px solid var(--bs-line); border-radius: 9px; cursor: pointer;
}

/* Customer typeahead (estimate/job wizard step 1) — native fetch-driven results list. */
.typeahead { position: relative; }
.typeahead__results {
    position: absolute; z-index: 30; inset-inline: 0; top: calc(100% + 2px);
    margin: 0; padding: 0.25rem; list-style: none;
    background: #fff; border: 1px solid var(--bs-line); border-radius: 9px;
    box-shadow: 0 6px 18px rgba(16, 24, 40, 0.10); max-height: 16rem; overflow-y: auto;
}
.typeahead__item { padding: 0.5rem 0.7rem; border-radius: 6px; cursor: pointer; }
.typeahead__item:hover,
/* Keyboard highlight. Matches :hover exactly so arrowing through the list reads the same as pointing
   at it — the arrow keys are the only route to this picker for a keyboard user. */
.typeahead__item.is-active { background: rgba(0, 222, 145, 0.10); }

/* Customer document drag-and-drop uploader. Degrades to a normal file input + button without JS. */
.dropzone { border: 2px dashed var(--bs-line, #ccc); border-radius: 8px; padding: 1.25rem; text-align: center; margin-top: 1rem; }
.dropzone--over { border-color: var(--bs-primary, #2b6cb0); background: rgba(43, 108, 176, 0.06); }
.dropzone__prompt { margin: 0 0 0.75rem; color: var(--bs-secondary, #555); }
.dropzone__fallback { margin-top: 0.75rem; }
.dropzone__bar { height: 8px; background: #eee; border-radius: 4px; overflow: hidden; }
.dropzone__bar > span { display: block; height: 100%; width: 0; background: var(--bs-primary, #2b6cb0); transition: width 0.15s; }
.dropzone__error { color: var(--bs-danger, #c0392b); }
.dropzone__list { list-style: none; margin: 0.75rem 0 0; padding: 0; text-align: left; }
.dropzone__row { display: flex; align-items: center; gap: 0.75rem; padding: 0.35rem 0; }
.dropzone__name { flex: 0 0 auto; max-width: 60%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dropzone__row .dropzone__bar { flex: 1 1 auto; }
/* Upload-complete marker. The tick is DRAWN, for the reason spelled out on .cover-option__check:
   U+2713 is in neither Archivo nor this server's sans-serif and monospace fallbacks, so the typed
   character painted zero pixels and a finished upload showed nothing at all.
   tests/Browser/glyph-coverage measures that. The span must stay EMPTY in
   job_document_dropzone_controller.js — a text node would sit beside this pseudo-element. */
.dropzone__done {
    color: var(--bs-success, #2e7d32);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 0.85rem;
    height: 0.85rem;
}
.dropzone__done::before {
    content: "";
    width: 0.28rem;
    height: 0.55rem;
    margin-block-start: -0.12rem;
    border: solid currentColor;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}

/* ---- Workforce: HR calendar ---- */
.wf-calendar-nav { display: flex; align-items: center; gap: 0.6rem; }
.wf-calendar-nav a {
    font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.08em;
    font-size: 0.7rem; color: var(--bs-text); text-decoration: none;
    padding: 0.55rem 0.9rem; border: 1px solid var(--bs-line); border-radius: 8px;
}
.wf-calendar-nav a:focus-visible { outline: 2px solid var(--hover-ring); outline-offset: 2px; border-color: var(--hover-ring); }

/* State legend: each swatch reuses the exact .wf-cell--* treatment used in the grid, with the same
   short marker letter, so the legend and the grid are visibly the same vocabulary. */
.wf-legend { list-style: none; display: flex; flex-wrap: wrap; gap: 0.5rem 1.1rem; margin: 0 0 1.25rem; padding: 0; font-size: 0.82rem; color: var(--bs-text); }
.wf-legend__item { display: flex; align-items: center; gap: 0.45rem; }
.wf-legend__swatch {
    width: 1.4rem; height: 1.4rem; flex: 0 0 auto; border-radius: 6px;
    display: flex; align-items: center; justify-content: center;
    font-family: var(--bs-font-mono); font-size: 0.6rem; font-weight: 700;
}

/* Claim decision buttons — the tick / X / slash that set a claim's authorisation straight from the
   list. The same class dresses the legend swatches above the table, so the key and the controls are
   literally one treatment rather than two that have to be kept in sync by hand.

   Icon-only by design, so every button carries an aria-label/title: colour is reinforcement here,
   never the sole carrier of meaning. Cancel is muted rather than a second red — a withdrawal is not
   a refusal, and it must not read as a sibling of the red Delete pill further along the row. */
.claim-decision {
    width: 1.6rem; height: 1.6rem; flex: 0 0 auto; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 7px; border: 1px solid transparent;
    font-size: 0.9rem; line-height: 1; cursor: pointer;
    background: transparent; transition: background-color 0.15s ease, border-color 0.15s ease;
}
.claim-decision--approve { color: #1a7f3c; border-color: #b7e0c4; background: #e9f6ee; }
.claim-decision--decline { color: #b3261e; border-color: #f3b4b4; background: #fdecec; }
.claim-decision--cancel { color: #4b5563; border-color: #d7dbe0; background: #eef0f3; }
.claim-decision--approve:hover { background: #d7eede; }
.claim-decision--decline:hover { background: #fbdcdc; }
.claim-decision--cancel:hover { background: #e2e6ea; }
/* The claim's current state: holds the slot so the View/Edit/Delete pills stay aligned across rows
   when one button is suppressed. Not a control — no pointer, no hover, not focusable. */
.claim-decision--current { visibility: hidden; cursor: default; }
/* Tighter than the 0.4rem the action cell puts between its children: these three belong together,
   and the grouping is what stops them reading as three more buttons in an already-long row. */
.claim-decisions { display: inline-flex; align-items: center; gap: 0.15rem; }
.claim-decisions form { display: inline-flex; }
/* One item per line. The panel has no padding and the table is full-bleed, so the key insets itself
   the same way .table-tools does. */
/* Smaller and quieter than .wf-legend's default: this is reference material you read once, not
   something that should compete with the rows for attention. */
.claim-legend { flex-direction: column; align-items: flex-start; gap: 0.3rem; margin: 0.9rem 1rem; font-size: 0.72rem; color: var(--bs-muted); }

/* The table controller injects .table-tools before the <table> at runtime, so source order alone
   cannot place the key relative to the search row. Grid can: the key sits in a second column
   BESIDE the filters, in the dead space to their right, so stacking it costs no vertical height —
   the filter block is already taller than the three stacked lines.

   minmax(0, 1fr) on the first column matters: without the 0 floor the track would grow to the
   table's max-content width and shove the key out of the visible area. */
.table-scroll--legend { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; }
.table-scroll--legend .table-tools { grid-area: 1 / 1; }
.table-scroll--legend .claim-legend { grid-area: 1 / 2; max-width: 24rem; }
.table-scroll--legend .data-table { grid-area: 2 / 1 / 3 / 3; }

/* Below the filters once there is no room beside them, rather than crushing both. */
@media (max-width: 1100px) {
    .table-scroll--legend { grid-template-columns: minmax(0, 1fr); }
    .table-scroll--legend .claim-legend { grid-area: 2 / 1; max-width: none; margin-top: 0; }
    .table-scroll--legend .data-table { grid-area: 3 / 1 / 4 / 2; }
}

[data-bs-theme="dark"] .claim-decision--approve {
    color: #3ff5b8; border-color: rgba(0, 222, 145, 0.55); background: rgba(0, 222, 145, 0.14);
}
[data-bs-theme="dark"] .claim-decision--decline {
    color: #ff8a80; border-color: rgba(255, 90, 76, 0.6); background: rgba(255, 76, 62, 0.15);
}
[data-bs-theme="dark"] .claim-decision--cancel {
    color: #c4cdda; border-color: rgba(255, 255, 255, 0.22); background: rgba(255, 255, 255, 0.07);
}
[data-bs-theme="dark"] .claim-decision--approve:hover { background: rgba(0, 222, 145, 0.24); }
[data-bs-theme="dark"] .claim-decision--decline:hover { background: rgba(255, 76, 62, 0.25); }
[data-bs-theme="dark"] .claim-decision--cancel:hover { background: rgba(255, 255, 255, 0.14); }

/* Horizontally-scrolling grid: staff name is a sticky first column; the header row stays visible
   as the panel scrolls vertically on long staff lists. Always scrolls (unlike .table-scroll, which
   only forces scroll below the tablet breakpoint) because a month of columns never fits any viewport. */
.wf-calendar-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; border: 1px solid var(--bs-line); border-radius: 10px; }
.wf-calendar-table { border-collapse: collapse; width: max-content; min-width: 100%; }
.wf-calendar-table th, .wf-calendar-table td {
    border-bottom: 1px solid #eef0f3; border-right: 1px solid #f4f5f7;
    padding: 0.3rem; text-align: center; vertical-align: middle;
}
.wf-calendar-table thead th {
    position: sticky; top: 0; z-index: 2; background: #fff;
    font-family: var(--bs-font-mono); font-weight: 600; color: var(--bs-muted);
    border-bottom: 1px solid var(--bs-line);
}
/* Weekends read as ONE BLOCK, not as two slightly-grey columns. The old #f7f8fa was a 1.5% step off
   white — invisible next to the booking colours it sits behind, and a manager counting days across a
   month grid had nothing to count against. A tint plus a 2px blue rule down each side of the Sat/Sun
   pair brackets the block: the eye finds the weekend by the edge, from any distance, and the days
   between two brackets are countable at a glance.

   The blue is #02b3fe, the app's secondary/structural blue, and it is deliberately the SAME hex in
   both themes — this line is structure, not state. It has to stay clear of the two cell states that
   are themselves blue (.fc-cell--booked and .wf-cell--absence_unpaid, both pale #a8dcf3 fills with
   #026e9e text); saturation is what separates them, so do not soften this to a pastel and do not
   introduce a blue *fill* here. The tint stays neutral grey for the same reason.

   The rules are drawn with sibling selectors rather than a `.is-weekend--first/--last` class so the
   three calendars that share this table (fleet availability, technician schedule, workforce) all get
   it from the markup they already emit. `:has()` is the only way to reach the LAST weekend cell —
   there is no previous-sibling combinator — and this codebase already relies on it
   (.cover-option:has(input:checked)). `:last-child` closes the case `:has()` cannot see: a weekend
   that ENDS the range has no weekday sibling after it, and a bracket left hanging open at the table
   edge reads as a rendering fault rather than as a boundary.

   THE BRACKET IS DRAWN WITH INSET box-shadow, NOT WITH border. Do not "simplify" it back to
   border-left/border-right. This table is `border-collapse: collapse`, and under collapsing the two
   edges are not equivalent problems: the CLOSING edge competes with nothing (the weekday after a
   Sunday declares no border-left at all, so the blue wins by default), while the OPENING edge
   competes with the 1px hairline that every cell declares as its border-right. Collapsing resolves
   that pair and paints ONE border straddling the grid line, and Chrome and Firefox were observed
   resolving it differently on a real month grid — Firefox drew the opening rule, Chrome dropped it,
   leaving every weekend bracketed on the right only. Both browsers reported 2px of blue from
   getComputedStyle; only one painted it.

   Inset shadows never enter that arbitration. They paint inside each weekend cell's own padding box,
   so the opening and closing rules are drawn by the same mechanism, are the same 2px on both sides,
   and no longer eat a pixel of the adjacent weekday.

   The two sides go through --wk-l/--wk-r rather than being two `box-shadow` declarations because a
   single cell can need BOTH: a month that starts on a Sunday puts one lone weekend day against the
   name column, matching the opening and closing selectors at once, and a second `box-shadow`
   declaration would simply overwrite the first.

   Covered by tests/Browser/calendar-weekend/check.mjs, which samples PAINTED PIXELS at three device
   scale factors — a computed-style assertion passes on the broken version. */
.wf-calendar-table th.is-weekend, .wf-calendar-table td.is-weekend {
    background: #e7eaf0;
    --wk-l: 0 0 0 0 transparent; --wk-r: 0 0 0 0 transparent;
    box-shadow: var(--wk-l), var(--wk-r);
}
.wf-calendar-table :not(.is-weekend) + .is-weekend { --wk-l: inset 2px 0 0 0 #02b3fe; }
.wf-calendar-table .is-weekend:has(+ :not(.is-weekend)),
.wf-calendar-table .is-weekend:last-child { --wk-r: inset -2px 0 0 0 #02b3fe; }
.wf-calendar-table thead th.is-weekend .wf-calendar__dow { color: var(--bs-muted); }
.wf-calendar__daynum { display: block; font-size: 0.78rem; color: var(--bs-text); }
.wf-calendar__dow { display: block; font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.06em; }
.wf-calendar-table .wf-calendar__name {
    position: sticky; left: 0; z-index: 1; background: #fff;
    text-align: left; white-space: nowrap; min-width: 11rem;
    padding-inline: 0.9rem; font-weight: 600; font-size: 0.85rem;
    border-right: 1px solid var(--bs-line); box-shadow: 2px 0 4px rgba(16, 24, 40, 0.04);
}
.wf-calendar-table thead th.wf-calendar__name { z-index: 3; }
.wf-calendar-group-heading td {
    position: sticky; left: 0; text-align: left; background: var(--bs-ink); color: #fff;
    font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.1em;
    font-size: 0.68rem; padding: 0.55rem 0.9rem; border: none;
}
.wf-calendar__empty { text-align: left; color: var(--bs-muted); font-style: italic; padding-inline-start: 0.9rem; }

/* Each day cell is a fixed-size chip: a background/border pair per state plus a short marker
   letter, so the state never depends on color alone. */
.wf-cell {
    display: flex; align-items: center; justify-content: center;
    width: 1.9rem; height: 1.9rem; margin: 0 auto; border-radius: 6px;
    border: 1px solid transparent;
}
.wf-cell__marker { font-family: var(--bs-font-mono); font-size: 0.62rem; font-weight: 700; letter-spacing: 0.01em; }
.wf-cell--present { background: transparent; }
/* Same green as the rest of the app's positive/RAG vocabulary (absence_holiday below,
   badge-flag--green, kpi-stat__n--green) — reused rather than invented so "present" reads as the
   same "good" as everywhere else. RSPH-28: RAG colours don't get a dark-mode pair, only neutrals do
   (see the booking-calendar comment above), so this stays the one value for both themes. */
.wf-cell--present .wf-cell__marker, .wf-legend__swatch.wf-cell--present { color: #1a7f3c; }
.wf-cell--off { background: #f1f2f4; border-color: #e7e9ec; }
.wf-cell--off .wf-cell__marker { color: #9aa1ab; }
.wf-cell--public_holiday { background: #fff4e0; border-color: #f3d9a8; color: #9a6700; }
.wf-cell--closure { background: var(--bs-ink); border-color: var(--bs-ink); color: #fff; }
.wf-cell--absence_holiday { background: #e9f6ee; border-color: #b7e0c4; color: #1a7f3c; }
.wf-cell--absence_sick { background: #fdecec; border-color: #f3b4b4; color: #b3261e; }
.wf-cell--absence_unpaid { background: #e6f6fd; border-color: #a8dcf3; color: #026e9e; }
.wf-cell--absence_other { background: #eef0f3; border-color: #d7dbe0; color: #4b5563; }
.wf-cell--absence_pending {
    color: #9a6700; border: 1px dashed #e8b23d;
    background-image: repeating-linear-gradient(45deg, #fff4e0, #fff4e0 4px, #ffe9c2 4px, #ffe9c2 8px);
}
@media (max-width: 480px) {
    .wf-calendar-table .wf-calendar__name { min-width: 8.5rem; }
}

/* =====================================================================   Booking calendar (RSPH-79)
   A traditional month grid (weeks x 7 days) rather than the HR calendar's staff-row timeline, but
   deliberately built from the same family of parts: RAG colouring lifted straight from the
   .wf-cell--{state} pairs above (green=absence_holiday, amber=public_holiday, red=absence_sick,
   neutral=off), and .status-pill for job status exactly as the rest of the app already renders it.
   Per the dark-mode convention documented below (RSPH-28: only neutrals re-tokenise, RAG colours
   stay as-is), only the neutral/no-capacity state gets a dark override.
   ============================================================================ */
.bk-month { font-weight: 400; color: var(--bs-muted); font-size: 0.65em; margin-inline-start: 0.4em; }

/* RSPH-91: the "which site am I viewing?" scope indicator, shown only to multi-site viewers. Built
   from the calendar's own idioms so it reads as native, not bolted on: the chip's border/paper fill
   and dark override, the .bk-day__flag pill radius, and the mono uppercase micro-label the rail's
   "open job" link already uses. Quiet by default; the site name is the one part that carries weight. */
.bk-scope {
    display: inline-flex; align-items: baseline; gap: 0.4rem; margin-top: 0.45rem;
    padding: 0.2rem 0.65rem; border: 1px solid var(--bs-line); border-radius: 999px;
    background: var(--bs-paper, #fff);
}
.bk-scope__label {
    font-family: var(--bs-font-mono); font-size: 0.6rem; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.12em; color: var(--bs-muted);
}
.bk-scope__value { font-size: 0.82rem; font-weight: 700; color: var(--bs-text); }

/* RSPH-95: capacity vs workload as a slim toolbar strip (moved off its own tab). Deliberately quiet
   and one line so it never competes with the calendar — same pill idiom and mono micro-label as
   .bk-scope above, just carrying four figures instead of one. */
.bk-capacity {
    display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem 0.9rem;
    margin-top: 0.45rem; padding: 0.3rem 0.75rem;
    border: 1px solid var(--bs-line); border-radius: 12px; background: var(--bs-paper, #fff);
}
.bk-capacity__title {
    font-family: var(--bs-font-mono); font-size: 0.6rem; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.12em; color: var(--bs-muted);
}
.bk-capacity__metric { display: inline-flex; align-items: baseline; gap: 0.3rem; }
.bk-capacity__v { font-size: 0.82rem; font-weight: 700; color: var(--bs-text); }
.bk-capacity__v--free { color: #1a7f3c; }
.bk-capacity__v--over { color: #b3261e; }
.bk-capacity__k { font-size: 0.68rem; color: var(--bs-muted); }

.bk-layout { display: grid; grid-template-columns: minmax(0, 1fr) 20rem; gap: 1.5rem; align-items: start; }
@media (max-width: 900px) {
    .bk-layout { grid-template-columns: 1fr; }
}

.bk-calendar__weekdays {
    display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 0.5rem;
    margin-bottom: 0.5rem; font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.06em; color: var(--bs-muted); text-align: center;
}
.bk-calendar__grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 0.5rem; }

.bk-day {
    min-height: 6.5rem; border-radius: 10px; border: 1px solid var(--bs-line);
    padding: 0.5rem 0.55rem; display: flex; flex-direction: column; gap: 0.35rem;
    background: transparent;
}
.bk-day--blank { border-style: dashed; border-color: var(--bs-line); opacity: 0.35; }
.bk-day--neutral { background: #f1f2f4; border-color: #e7e9ec; }
.bk-day--green { background: #e9f6ee; border-color: #b7e0c4; }
.bk-day--amber { background: #fff4e0; border-color: #f3d9a8; }
.bk-day--red { background: #fdecec; border-color: #f3b4b4; }
.bk-day--overloaded { border-width: 2px; }

.bk-day__head { display: flex; align-items: center; justify-content: space-between; }
.bk-day__num { font-size: 0.85rem; font-weight: 700; color: var(--bs-text); }
.bk-day__flag {
    font-size: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
    color: #fff; background: #b3261e; border-radius: 999px; padding: 0.1rem 0.4rem;
    /* Only ever the short generic word now (see the day cell template), but a tenant's locale could
       still make it long — truncate rather than wrap, or the cell's grid row grows. */
    max-width: 68%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* RSPH-174: which trade is holding the day up. Full cell width, because the trade LABEL is the
   actionable part and tenants rename trades freely — "Electrical Technician" truncated into the
   head row read as "ELE…" and told a planner nothing. The name may still ellipsis on a narrow
   viewport; the excess figure never does, being a separate flex item that keeps its intrinsic
   width, and the title carries every overloaded trade in full. */
.bk-day__reason {
    display: flex; align-items: baseline; gap: 0.25rem;
    font-size: 0.64rem; font-weight: 700; line-height: 1.25;
    color: #8f1d17;
}
.bk-day__reason-trade { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bk-day__reason-excess { flex: 0 0 auto; }

.bk-bar { height: 6px; border-radius: 999px; background: rgba(0,0,0,0.08); overflow: hidden; }
.bk-bar__fill { height: 100%; border-radius: 999px; }
.bk-bar__fill--green { background: #1a7f3c; }
.bk-bar__fill--amber { background: #e8a400; }
.bk-bar__fill--red { background: #b3261e; }
.bk-bar__fill--neutral { background: var(--bs-muted); }

.bk-day__hours { font-size: 0.72rem; color: var(--bs-text); display: flex; align-items: baseline; justify-content: space-between; gap: 0.35rem; }
.bk-day__hours--muted { color: var(--bs-muted); font-style: italic; }
.bk-day__pct { font-weight: 700; color: var(--bs-muted); }

.bk-day__chips { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.25rem; }
.bk-chip {
    display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: 0.68rem; font-weight: 700; padding: 0.12rem 0.4rem; border-radius: 6px;
    background: var(--bs-paper, #fff); border: 1px solid var(--bs-line); color: var(--bs-text);
    text-decoration: none;
}
.bk-chip--provisional { border-style: dashed; }
/* RSPH-184: the car needing a trade this day is short of. An INSET box-shadow, not border-left —
   the grid's collapsed borders make a real left border unreliable here, and getComputedStyle
   reports the declared border rather than the painted one, so a test would not catch it. */
.bk-chip--short { box-shadow: inset 3px 0 0 0 #d64545; }
[data-bs-theme="dark"] .bk-chip--short { box-shadow: inset 3px 0 0 0 #ff9a90; }
.bk-day__more { font-size: 0.66rem; color: var(--bs-muted); }

/* RSPH-184: the day number and the "+N more" line are now real links to the single-day window. */
.bk-day__num { text-decoration: none; color: var(--bs-text); }
.bk-day__num:hover, .bk-day__num:focus-visible { text-decoration: underline; }
.bk-day__more a { color: inherit; text-decoration: none; }
.bk-day__more a:hover, .bk-day__more a:focus-visible { text-decoration: underline; }
.bk-day[data-day-url] { cursor: pointer; }

.bk-dayview__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.75rem; }
.bk-daycard {
    border: 1px solid var(--bs-line); border-radius: 12px; padding: 0.75rem 0.9rem;
    background: var(--bs-paper); display: flex; flex-direction: column; gap: 0.4rem;
}
.bk-daycard--provisional { border-style: dashed; }
/* RSPH-184: the day window's card for a car needing a short-staffed trade. A card is a bigger
   surface than a chip, so the chip's 3px inset shadow would read as an accent nobody notices —
   this follows the grid's own .bk-day--red idiom instead (tinted background + tinted border),
   the same "something's wrong here" surface this feature already uses for an overloaded day. */
.bk-daycard--short { background: #fdecec; border-color: #f3b4b4; }
/* Same red as .bk-day__reason, so the day tile's flag and the day window's explanation read as
   one signal rather than two different reds. */
.bk-daycard__short { margin: 0; font-size: 0.78rem; font-weight: 700; color: #8f1d17; }
.bk-daycard__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.75rem; }
.bk-daycard__vehicle { font-family: var(--bs-font-mono); font-size: 1.05rem; margin: 0.15rem 0 0; }
.bk-daycard__model { margin: 0.15rem 0 0; font-size: 0.8rem; color: var(--bs-muted); }
.bk-daycard__meta { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; justify-content: flex-end; }
.bk-daycard__tag {
    font-family: var(--bs-font-mono); font-size: 0.64rem; letter-spacing: 0.12em; text-transform: uppercase;
    color: var(--bs-muted);
}
.bk-daycard__hours { margin: 0; font-size: 0.78rem; display: flex; flex-wrap: wrap; gap: 0.6rem; }
.bk-daycard__trade { color: var(--bs-muted); }
.bk-daycard__actions { display: flex; gap: 0.4rem; margin-top: 0.15rem; }

/* RSPH-91: in the all-sites view a chip stacks its site name beneath its label, so two cars from
   different sites on one day are distinguishable. The badge is deliberately quieter than the chip —
   the mono uppercase utility idiom, muted, one line that truncates — so it subtitles rather than
   competes. The li becomes a column only to seat it; a chip without a badge is unchanged. */
.bk-day__chips li { display: flex; flex-direction: column; gap: 0.12rem; min-width: 0; }
.bk-chip__site {
    font-family: var(--bs-font-mono); font-size: 0.58rem; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.06em; color: var(--bs-muted);
    max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    padding-inline-start: 0.1rem;
}

/* RSPH-114: the calendar's search. The input is the house search field (.table-tools__search, ring
   and magnifier included) at a width that sits beside the month nav rather than competing with it;
   only the dropdown is new, and it borrows the typeahead's paper/shadow so it reads as the same
   control the rest of the app uses. */
.bk-toolbar__right { display: flex; flex-direction: column; align-items: flex-end; gap: 0.5rem; }
.bk-search { position: relative; }
.bk-search__input { width: min(320px, 100%); padding-block: 0.45rem; font-size: 0.85rem; }
.bk-search__results {
    position: absolute; z-index: 30; inset-inline-end: 0; top: calc(100% + 4px);
    width: min(24rem, 90vw); max-height: 22rem; overflow-y: auto;
    padding: 0.3rem; text-align: start;
    background: #fff; border: 1px solid var(--bs-line); border-radius: 10px;
    box-shadow: 0 8px 22px rgba(16, 24, 40, 0.12);
}
/* The mono uppercase micro-label the calendar already uses for .bk-scope and .bk-chip__site, so the
   two groups read as labels rather than as results. */
.bk-search__group {
    margin: 0.35rem 0.5rem 0.2rem; font-family: var(--bs-font-mono); font-size: 0.6rem;
    font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em; color: var(--bs-muted);
}
.bk-search__item {
    display: block; padding: 0.4rem 0.5rem; border-radius: 7px;
    color: var(--bs-text); text-decoration: none;
}
a.bk-search__item:hover, a.bk-search__item:focus-visible, .bk-search__item.is-active {
    background: rgba(0, 222, 145, 0.10);
}
.bk-search__label { display: block; font-size: 0.85rem; font-weight: 700; }
.bk-search__meta {
    display: block; font-family: var(--bs-font-mono); font-size: 0.66rem; color: var(--bs-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bk-search__message { margin: 0.5rem; font-size: 0.82rem; color: var(--bs-muted); }

/* The one loud moment in this feature: the chip the search sent you to. A dense month grid is a lot
   of near-identical chips, so it pulses for a full 5s to catch the eye on arrival and then KEEPS the
   ring — the pulse ends but the user still has to be able to see which one it was.
   Blue (the app's secondary #02b3fe), not the signature green: green reads as "done", and this is a
   "look here" marker. 7 × 0.7s = 4.9s. */
.bk-chip--found {
    border-color: #02b3fe; box-shadow: 0 0 0 3px rgba(2, 179, 254, 0.22);
    animation: bk-found-pulse 0.7s ease-out 7;
}
@keyframes bk-found-pulse {
    0% { box-shadow: 0 0 0 0 rgba(2, 179, 254, 0.55); }
    100% { box-shadow: 0 0 0 8px rgba(2, 179, 254, 0); }
}
@media (prefers-reduced-motion: reduce) {
    .bk-chip--found { animation: none; }
}

/* RSPH-184: a chip can be BOTH the search's found target and short-staffed at once — the day-cell
   template's chip-ordering comment says search wins the front slot "when both apply", so this is a
   real, reachable state, not a hypothetical. `box-shadow` does not compose across separate rules:
   whichever selector wins the cascade replaces the WHOLE property, so without this rule the two
   single-state declarations above fight over one chip and one signal silently disappears — the
   blue ring in light mode (.bk-chip--found is later in source, same specificity), the red inset in
   dark mode (the theme-gated .bk-chip--short outranks the ungated .bk-chip--found there). This
   selector's higher specificity (two classes) wins outright in both themes, so do not delete it as
   "redundant" with the rules above — it is the only rule that keeps both signals at once. Values
   copied from .bk-chip--found's own ring and .bk-chip--short's own inset, not reinvented. */
.bk-chip--found.bk-chip--short { box-shadow: 0 0 0 3px rgba(2, 179, 254, 0.22), inset 3px 0 0 0 #d64545; }
[data-bs-theme="dark"] .bk-chip--found.bk-chip--short { box-shadow: 0 0 0 3px rgba(2, 179, 254, 0.22), inset 3px 0 0 0 #ff9a90; }

/* The jump landed on a grid with no chip for that booking. Quiet, one line, the calendar's own pill
   idiom — it explains, it does not alarm. */
.bk-notfound {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.4rem;
    margin: 0 0 1rem; padding: 0.45rem 0.8rem; font-size: 0.85rem;
    border: 1px solid var(--bs-line); border-radius: 999px; background: var(--bs-paper, #fff);
}

.bk-rail { padding: 1.25rem 1.5rem; align-self: start; }
.bk-rail__count { font-size: 0.8rem; color: var(--bs-muted); white-space: nowrap; }
.bk-rail__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.75rem; }
.bk-rail__card { border: 1px solid var(--bs-line); border-radius: 10px; padding: 0.75rem 0.9rem; }
/* RSPH-183: the card opens its job. A table row gets this for free — it sits in a striped table that
   already hover-tints, and `.data-table tbody tr.is-row-link` only has to add the cursor. A rail card
   is an isolated bordered box with no hover state of its own, and the "Open job" pill that used to
   advertise where it went is gone, so the border has to do that work instead. :focus-within, not just
   :hover, because the registration anchor inside is the keyboard's way through the same card.
   Deliberately the plain border colour rather than --bs-hi-vis: the rail already spends the signature
   green on drop targets during a drag, and a whole list of cards glowing at rest would drown it.
   --bs-muted and not --bs-ink-2, which is the obvious "darken it" pick and wrong: ink-2 is not
   re-tokenised for the portal's dark theme, so it stays #0e0f11 and the hover border would disappear
   into a dark card. --bs-muted flips with the theme and lifts off --bs-line in both directions.
   Left of `.bk-rail__card[draggable="true"] { cursor: grab }` below on purpose — equal specificity, so
   source order settles it and a planner who can drag keeps grab as that card's primary gesture. */
.bk-rail__card.is-row-link { cursor: pointer; }
.bk-rail__card.is-row-link:hover,
.bk-rail__card.is-row-link:focus-within { border-color: var(--bs-muted); }
/* The rail card is a ~300px column, so the status sits on its own row beneath the vehicle instead of
   competing with it for width. Status labels come from the per-tenant catalog and run long ("Awaiting
   Workshop Inspection For Estimate"), so they wrap rather than truncate — the rail is triaged by
   status, so the words matter more than a tidy single line. A wrapped pill is several lines tall,
   which the .status-pill 999px radius would render as an ellipse; 6px squares it off, the same way
   .job-status-block does where a pill sits alongside other rectangles. */
.bk-rail__head { display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem; }
.bk-rail__head .status-pill { border-radius: 6px; }
.bk-rail__vehicle { margin: 0.1rem 0 0.15rem; font-size: 0.95rem; }
/* RSPH-183: the registration is the card's anchor now, and it has to keep reading as the card's
   heading rather than as a hyperlink — a rail of 99 cards each with a blue underlined plate is a wall
   of link. Same treatment the row-linked tables give their WIP anchor (`.wip`): invisible at rest,
   underlined the moment it is hovered or focused, which is also the keyboard's only sign of where it
   has landed. */
.bk-rail__vehicle a { color: inherit; text-decoration: none; }
.bk-rail__vehicle a:hover, .bk-rail__vehicle a:focus-visible { text-decoration: underline; }
.bk-rail__model { margin: 0; font-size: 0.78rem; color: var(--bs-muted); }
.bk-rail__open {
    display: inline-block; margin-top: 0.6rem; font-family: var(--bs-font-mono); font-size: 0.62rem;
    text-transform: uppercase; letter-spacing: 0.1em; font-weight: 700; text-decoration: none; color: var(--bs-muted);
}
.bk-rail__open:hover, .bk-rail__open:focus-visible { color: var(--bs-hi-vis-press); }

[data-bs-theme="dark"] .bk-day--neutral,
[data-bs-theme="dark"] .bk-day--blank { background: var(--surface-3); border-color: var(--bs-line); }
[data-bs-theme="dark"] .bk-chip { background: var(--surface-3); }
[data-bs-theme="dark"] .bk-scope { background: var(--surface-3); }
[data-bs-theme="dark"] .bk-notfound { background: var(--surface-3); }
[data-bs-theme="dark"] .bk-capacity { background: var(--surface-3); }
[data-bs-theme="dark"] .bk-daycard { background: var(--surface-3); }

/* The RAG fills are light pastels chosen against a white page. Left alone under dark mode they keep
   their near-white background while .bk-day__num / .bk-day__hours flip to the near-white --bs-text —
   so every day that HAS capacity data becomes unreadable, while the neutral/blank days above (which
   are re-toned) still look right. Re-tone the fills as translucent washes of the same hue over the
   dark surface, following the .badge-flag treatment further down but without its glow: a day is a
   6.5rem surface rather than a chip, and a month of glowing cells would shout. Text keeps --bs-text
   and now has something to sit on. */
[data-bs-theme="dark"] .bk-day--green { background: rgba(0, 222, 145, 0.12); border-color: rgba(0, 222, 145, 0.4); }
[data-bs-theme="dark"] .bk-day--amber { background: rgba(255, 184, 28, 0.12); border-color: rgba(255, 184, 28, 0.45); }
[data-bs-theme="dark"] .bk-day--red { background: rgba(255, 76, 62, 0.13); border-color: rgba(255, 90, 76, 0.45); }
/* RSPH-174: #8f1d17 is near-black on a dark cell — the reason line has to lift, not deepen. */
[data-bs-theme="dark"] .bk-day__reason { color: #ff9a90; }

/* RSPH-184: same wash as .bk-day--red above, and after the general dark .bk-daycard background
   rule in source order so it wins at equal specificity — a short-staffed card must not fall back
   to the plain --surface-3 every other card gets. #8f1d17 is near-black on it, same as the reason
   line, so the text lifts to the same #ff9a90. */
[data-bs-theme="dark"] .bk-daycard--short { background: rgba(255, 76, 62, 0.13); border-color: rgba(255, 90, 76, 0.45); }
[data-bs-theme="dark"] .bk-daycard__short { color: #ff9a90; }

/* The capacity bar's track is a black wash for a white cell; on the dark surface it disappears into
   the fill it is meant to be the backdrop for. The --fill colours are saturated enough to keep. */
[data-bs-theme="dark"] .bk-bar { background: rgba(255, 255, 255, 0.12); }

/* ---- Booking calendar drag accelerator (RSPH-79 Task 13) ---- */
.bk-rail__card[draggable="true"] { cursor: grab; }
.bk-rail__card.is-dragging { opacity: 0.4; cursor: grabbing; }
.bk-day__chips li[draggable="true"] { cursor: grab; }
.bk-day__chips li.is-dragging { opacity: 0.4; cursor: grabbing; }
.bk-day.is-drop { border-color: var(--bs-hi-vis); box-shadow: 0 0 0 2px rgba(0, 222, 145, 0.28); }

/* ---- Workshop technician schedule (RSPH-79 Task 14) ----
   Same staff-rows x day-columns table as the HR calendar (.wf-calendar-table / .wf-calendar__name),
   a richer cell that shows spare hours (or an allocation chip) rather than a bare marker letter, and
   the booking calendar's .bk-layout/.bk-rail pairing reused wholesale for the "unallocated trades"
   rail. The one new idea here is .ts-cell--available: the system's signature hi-vis green (used
   nowhere else in either calendar's palette — it is reserved for "this is the actionable thing to
   notice", same as an active nav link or a drag drop-target) marks the exact cells a planner is
   scanning the grid for: working, spare hours, nobody allocated yet. */
.ts-layout { display: grid; grid-template-columns: minmax(0, 1fr) 20rem; gap: 1.5rem; align-items: start; }
@media (max-width: 900px) {
    .ts-layout { grid-template-columns: 1fr; }
}

.ts-cell {
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.1rem;
    min-width: 2.6rem; height: 2rem; padding: 0.15rem 0.3rem; margin: 0 auto; border-radius: 6px;
    border: 1px solid transparent;
}
.ts-cell__hours { font-family: var(--bs-font-mono); font-size: 0.66rem; font-weight: 700; color: var(--bs-muted); line-height: 1; }
.ts-cell__marker { font-family: var(--bs-font-mono); font-size: 0.62rem; font-weight: 700; letter-spacing: 0.01em; }
.ts-cell__chip {
    display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-family: var(--bs-font-mono); font-size: 0.56rem; font-weight: 700; line-height: 1.2;
    padding: 0 0.3rem; border-radius: 4px; background: rgba(16, 24, 40, 0.06); color: var(--bs-text);
}
.ts-cell--working { background: transparent; }
.ts-cell--weekend { background: #f1f2f4; border-color: #e7e9ec; }
.ts-cell--weekend .ts-cell__marker { color: #9aa1ab; }
.ts-cell--holiday { background: #fff4e0; border-color: #f3d9a8; color: #9a6700; }
.ts-cell--closure { background: var(--bs-ink); border-color: var(--bs-ink); color: #fff; }
.ts-cell--absence { background: #e6f6fd; border-color: #a8dcf3; color: #026e9e; }
/* The one accent this whole screen exists to make findable — a soft hi-vis wash plus a solid
   hi-vis ring, never solid-fill, so the hours figure underneath stays legible. */
.ts-cell--available { background: rgba(0, 222, 145, 0.12); border-color: var(--bs-hi-vis); }
.ts-cell--available .ts-cell__hours { color: var(--bs-hi-vis-press); font-weight: 700; }

.ts-rail__trades { list-style: none; margin: 0.5rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.35rem; }
.ts-rail__trade {
    font-family: var(--bs-font-mono); font-size: 0.62rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.04em; padding: 0.18rem 0.45rem; border-radius: 999px;
    background: rgba(0, 222, 145, 0.10); border: 1px solid rgba(0, 222, 145, 0.35); color: var(--bs-hi-vis-press);
}

[data-bs-theme="dark"] .ts-cell--weekend { background: var(--surface-3); border-color: var(--bs-line); }
[data-bs-theme="dark"] .ts-cell__chip { background: var(--surface-3); color: var(--bs-text); }

/* ---- Courtesy car availability calendar (RSPH-178 Task 11) ----
   Same cars-rows x day-columns table as the technician schedule (.wf-calendar-table /
   .wf-calendar__name), with a bare-marker cell since there is no hours figure or allocation chip to
   show here — just which of four states a car is in on a given day. Reuses the technician
   schedule's washes for the states closest in spirit: booked mirrors absence's blue (reserved, not
   yet occupying anything), on_loan mirrors holiday's amber (the car is out, not free for a NEW
   booking), off_road mirrors closure's solid ink (genuinely unusable, whatever the diary says), and
   available reuses the same hi-vis green accent as the technician schedule's one findable state. */
.fc-cell {
    display: flex; align-items: center; justify-content: center;
    min-width: 2.6rem; height: 2rem; padding: 0.15rem 0.3rem; margin: 0 auto; border-radius: 6px;
    border: 1px solid transparent;
}
/* An available cell a courtesy_car.loan holder can click is a real <button> (RSPH-178 Task 12), not
   the plain <span> every other cell state renders — strip the native button chrome .fc-cell's own
   rules above don't reach (font, background, cursor) so it still reads as the same swatch. */
button.fc-cell { font: inherit; cursor: pointer; appearance: none; -webkit-appearance: none; }
button.fc-cell:hover, button.fc-cell:focus-visible { outline: 2px solid var(--bs-hi-vis-press); outline-offset: 1px; }
.fc-cell__marker { font-family: var(--bs-font-mono); font-size: 0.62rem; font-weight: 700; letter-spacing: 0.01em; }
.fc-cell--available { background: rgba(0, 222, 145, 0.12); border-color: var(--bs-hi-vis); }
/* --bs-hi-vis-press (#00bd7b) is an accent, not an ink: on the pale green wash it measured 2.25:1,
   so the marker read as an empty swatch next to B (5.09:1), L (4.47:1) and × (17.96:1). This deep
   green is the green sibling of the blue and amber inks those two already use, and clears 4.5:1. */
.fc-cell--available .fc-cell__marker { color: #05654a; font-weight: 700; }
/* Booked and on-loan are DELIBERATELY no longer the workforce grid's absence/holiday washes, and
   must not be "resynced" with them. Those washes were built to sit quietly behind an hours figure on
   a screen you read a row at a time. This calendar is scanned, a month of cells at once, looking for
   the days a car is not free — and at #e6f6fd on white, booked measured 1.11:1 against the cell
   around it. Its LABEL was fine (5.17:1); the chip was the thing that disappeared, which is why no
   text-contrast rule ever flagged it and why it took someone saying "washed out" to find. On-loan
   was the same defect at 1.09:1, with a label at 4.47:1, just under AA.

   Both fills are now deep enough to read as blocks while their inks clear 4.5:1 on them, giving
   every state the same construction as .fc-cell--available: a real fill, a saturated border of the
   same hue, and a dark ink. Contrast floors are enforced by tests/Browser/calendar-weekend.

   Booked's blue stays distinguishable from the weekend bracket's #02b3fe: this is a filled chip in a
   deeper azure, that is a 2px cyan rule on a column edge. Do not converge them. */
.fc-cell--booked { background: #c9e6fb; border-color: #2f93cf; color: #064e73; }
.fc-cell--on_loan { background: #ffdb9c; border-color: #d99b2b; color: #7a4f00; }
.fc-cell--off_road { background: var(--bs-ink); border-color: var(--bs-ink); color: #fff; }

/* Dark mode. Booked and on-loan keep their light fills — they are pale blocks on a dark panel and
   survive as they are — but the two states defined against theme tokens do not:

   available is the only translucent state, so its wash composites DOWN onto the dark panel instead
   of staying pale; the deep green ink above would then be dark-on-dark. It needs the light ink.

   off_road is `var(--bs-ink)`, which is a near-black *constant*, not a themed token — it is not
   re-tokenised under [data-bs-theme="dark"], so the cell and its legend swatch became #15171a on a
   #1c1f24 panel: 1.09:1, a black box invisible on a black panel with only a faint × left. In light
   mode this is the heaviest, most obvious state on the grid and it has to stay the heaviest here,
   so it inverts to the one solid neutral block that still out-weighs the pale washes around it. */
[data-bs-theme="dark"] .fc-cell--available .fc-cell__marker { color: var(--bs-hi-vis); }
[data-bs-theme="dark"] .fc-cell--off_road { background: #6b7480; border-color: #838c98; color: #fff; }

/* ---- Workshop technician schedule drag accelerator (RSPH-79 Task 15) ----
   Extends the booking calendar's drag idiom (.bk-rail__card[draggable], .bk-day__chips
   li[draggable], is-dragging/is-drop) to the tighter .ts-cell grid rather than inventing a second
   visual language. The one new idea here is the delete control: a .ts-cell is only ~2rem tall, so a
   permanent visible "×" would clutter every occupied cell — it stays invisible until the chip is
   hovered or focused, same "reveal on intent" pattern already used for row actions elsewhere. */
.ts-rail__trade[draggable="true"] { cursor: grab; }
.ts-rail__trade.is-dragging { opacity: 0.4; cursor: grabbing; }

.ts-cell-chip-slot { display: block; min-height: 0.9rem; }
.ts-cell__chip-wrap { position: relative; display: inline-block; max-width: 100%; }
.ts-cell__chip[draggable="true"] { cursor: grab; }
.ts-cell__chip.is-dragging { opacity: 0.4; cursor: grabbing; }
.ts-cell__chip--multi { cursor: default; }

.ts-cell__delete-form { position: absolute; top: -0.4rem; right: -0.4rem; margin: 0; line-height: 0; }
.ts-cell__delete {
    width: 0.85rem; height: 0.85rem; padding: 0; border-radius: 50%; font-size: 0.6rem; line-height: 1;
    border: 1px solid var(--bs-line); background: var(--bs-paper, #fff); color: var(--bs-muted);
    cursor: pointer; opacity: 0; transition: opacity 0.12s ease;
}
.ts-cell__chip-wrap:hover .ts-cell__delete,
.ts-cell__chip-wrap:focus-within .ts-cell__delete { opacity: 1; }
.ts-cell__delete:hover, .ts-cell__delete:focus-visible { color: #b3261e; border-color: #b3261e; opacity: 1; }

td[data-allocation-move-target="day"].is-drop { outline: 2px solid var(--bs-hi-vis); outline-offset: -2px; }

[data-bs-theme="dark"] .ts-cell__delete { background: var(--surface-3); }

/* ---- Dark-mode switch (RSPH-28) ----
   Two icon buttons — sun and moon — riding the same line as Sign out, anchored to its far end. The
   sidebar is dark in both themes, so this control keeps one look regardless of the active theme; the
   chosen option lights up hi-vis, which is the only thing distinguishing them at a glance.

   The pair is still two buttons rather than one flip-switch: each states a destination, so nobody has
   to work out which theme a single icon would take them to. They carry aria-label and aria-pressed —
   the icons are decorative to assistive tech, and the group's own label supplies the context the
   removed "Appearance" caption used to. */
.theme-toggle { display: inline-flex; align-items: center; gap: 0.15rem; }
.theme-toggle__opt {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.85rem; height: 1.85rem; padding: 0;
    color: #7e879a; background: none; border: none; border-radius: 8px; cursor: pointer;
    transition: background-color 0.12s ease, color 0.12s ease;
}
.theme-toggle__icon { width: 1.05rem; height: 1.05rem; display: block; }
/* One icon set, two weights: the sun's rays are strokes and its core is filled, the moon is a single
   filled crescent. Both inherit the button's colour, so the active state needs no second rule. */
.theme-toggle__icon circle { fill: currentColor; }
.theme-toggle__icon path {
    fill: currentColor; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round;
}
.theme-toggle__opt[data-theme="light"] .theme-toggle__icon path { fill: none; }
.theme-toggle__opt:hover { color: #fff; background: rgba(255, 255, 255, 0.07); }
.theme-toggle__opt.is-active { background: var(--bs-hi-vis); color: var(--bs-ink-2); }
.theme-toggle__opt:focus-visible { outline: 2px solid var(--bs-hi-vis); outline-offset: 1px; }

/* ===================================================================   Dark mode (RSPH-28)
   Bootstrap 5.3 flips its own components under [data-bs-theme="dark"]; this block re-tokenises the
   bespoke brand layer under the same scope. Per the ticket only neutrals (backgrounds / text / lines)
   change — the hi-vis accent, RAG status colours and coloured badges are kept as they are. The
   sidebar, auth panel and marketing site are already dark, so most of the work is the app content.
   Overriding the shared tokens (--bs-paper / --bs-line / --bs-muted / --bs-text) cascades through
   every rule already built on them; the explicit rules below cover surfaces that hardcoded a light
   value (mostly #fff panels) or that use the near-black --bs-ink token as *text*.
   ========================================================================== */
[data-bs-theme="dark"] {
    --bs-paper: #1c1f24;
    --table-scroll-cover: #1c1f24;  /* match the dark panel bg so the edge mask stops smearing light */
    --bs-line: #333a43;
    --bs-muted: #98a2b0;
    --bs-text: #e6e9ee;
    --due-amber: #ffcf4d;    /* readable amber/red on the dark surface (match badge-flag) */
    --due-red: #ff8a80;

    /* The light-mode rag set is far too dark to read on this surface — lifted to the same values
       the TV board uses, which were picked for exactly this background. */
    --rag-green: #2ec77e;
    --rag-amber: #f0b429;
    --rag-red: #f2685f;

    --surface: #1c1f24;      /* raised cards / panels / inputs */
    --surface-2: #14161a;    /* page background */
    --surface-3: #23272e;    /* subtle fill: hovers, insets, chips */
    --heading: #e9ecf1;      /* big display figures that use --bs-ink as text in light mode */

    /* Clickable-tile hover accent (RSPH-93). Light mode highlights hovered tiles with the
       green hi-vis border, but the dark surface override below resets that border, so hover
       looked flat. This brand cyan restores the highlight on dark. Deliberately #00b3ff (the
       claim cyan), NOT #02b3fe (the secondary-button blue) — see the button-palette notes. */
    --tile-hover: #00b3ff;

    /* Hover affordance, dark. The drop-shadow lift is invisible against the dark surface, so the
       lift becomes a 1px ring instead; the tint uses the inset surface rather than a light wash. */
    --hover-ring: var(--tile-hover);
    --hover-lift: 0 0 0 1px var(--tile-hover);
    --hover-tint: var(--surface-3);
}

[data-bs-theme="dark"] body { background: var(--surface-2); color: var(--bs-text); }

/* Raised surfaces: cards, panels, tiles, modals, menus, dropdowns. */
[data-bs-theme="dark"] .panel,
[data-bs-theme="dark"] .board-tile,
[data-bs-theme="dark"] .kpi-stat,
[data-bs-theme="dark"] .kpi-mix__cell,
[data-bs-theme="dark"] .vehicle-card,
[data-bs-theme="dark"] .queue-card,
[data-bs-theme="dark"] .clock-card,
[data-bs-theme="dark"] .radio-list__item,
[data-bs-theme="dark"] .perm-area,
/* `dialog.dialog-modal`, not a bare `.dialog-modal`: the job page's Add-technician modal is a
   `div.dialog-modal` acting as the full-viewport OVERLAY (see the rules near .dialog-modal__card),
   and a bare class here — (0,2,0) with the theme attribute — outranks `div.dialog-modal` (0,1,1),
   so it repainted that translucent scrim as an opaque surface and the whole screen went flat dark
   with the card invisible against it. The element qualifier keeps this to the real <dialog>s. */
[data-bs-theme="dark"] dialog.dialog-modal,
[data-bs-theme="dark"] .dialog-modal__card,
[data-bs-theme="dark"] .send-split__panel,
[data-bs-theme="dark"] .typeahead__results,
[data-bs-theme="dark"] .bk-search__results,
[data-bs-theme="dark"] .select2-dropdown {
    background: var(--surface);
    border-color: var(--bs-line);
    color: var(--bs-text);
}

/* Deadline urgency, dark mode. The block above resets .vehicle-card's border-color at (0,2,0) — the same
   specificity as `.vehicle-card.vehicle-card--due-amber` up near the due rules — and being later in the
   sheet it won, so a near-due card lost the border half of its 2px accent and kept only the ::after ring.
   Half the weight of the light-mode card, and easy to miss because the greyed border it left behind
   barely registers against a dark surface. Restoring it here rather than raising the specificity up
   there keeps the fix next to the rule that caused it — and this is the same source-order trap the hover
   block at the bottom of the file already documents for :hover.
   Red is listed too even though its pulse currently overrides the reset from keyframes (animations
   outrank normal declarations): that is luck, not intent, and it would vanish with the animation. */
[data-bs-theme="dark"] .vehicle-card--due-amber { border-color: var(--due-amber); }
[data-bs-theme="dark"] .vehicle-card--due-red { border-color: var(--due-red); }

/* Big display figures coloured with the near-black ink token must go light on a dark surface. */
[data-bs-theme="dark"] .board-tile__count,
[data-bs-theme="dark"] .kpi-stat__n,
[data-bs-theme="dark"] .kpi-revenue__n,
[data-bs-theme="dark"] .kpi-pipeline__count,
[data-bs-theme="dark"] .kpi-mix__n,
[data-bs-theme="dark"] .queue-card__vehicle,
[data-bs-theme="dark"] .queue-timer__value,
[data-bs-theme="dark"] .clock-card__state,
[data-bs-theme="dark"] .clock-timer__value,
[data-bs-theme="dark"] .vehicle-card__reg {
    color: var(--heading);
}

/* Hero KPI card stays ink but gains a faint accent edge so it separates from the dark page. On dark
   the Open Jobs hero reads in the brand blue rather than the signature green. */
[data-bs-theme="dark"] .kpi-stat--hero { border-color: rgba(0, 179, 255, 0.35); }

/* The KPI headline numbers stay RAG-coloured on dark rather than going flat white with the block
   above. The light-mode inks (#1a7f3c / #b3261e) would sit too dark on the raised card to read, so
   these use the dark-surface RAG tokens the deadline chips already use. The Open Jobs hero is not a
   RAG status but a total, so it takes the brand blue (--hero-accent) in both themes — restated here
   because the flat-white rule above otherwise wins on the dark surface by source order. */
[data-bs-theme="dark"] .kpi-stat--hero .kpi-stat__n { color: var(--hero-accent); }
[data-bs-theme="dark"] .kpi-stat__n--green { color: var(--bs-hi-vis); }
[data-bs-theme="dark"] .kpi-stat__n--amber { color: var(--due-amber); }
[data-bs-theme="dark"] .kpi-stat__n--red { color: var(--due-red); }
[data-bs-theme="dark"] .kpi-stat__n--neutral { color: var(--bs-muted); }

/* Form controls & search inputs that hardcoded a white face. */
[data-bs-theme="dark"] .form-grid input,
[data-bs-theme="dark"] .form-grid textarea,
[data-bs-theme="dark"] .form-grid select,
[data-bs-theme="dark"] .tech-modal__panel input,
[data-bs-theme="dark"] .tech-modal__panel textarea,
[data-bs-theme="dark"] .tech-modal__panel select,
[data-bs-theme="dark"] .status-form select,
[data-bs-theme="dark"] .status-form input,
[data-bs-theme="dark"] .status-form textarea,
[data-bs-theme="dark"] .dialog-modal--compact select,
[data-bs-theme="dark"] .inline-form input[type="date"],
[data-bs-theme="dark"] .inline-form input[type="text"],
[data-bs-theme="dark"] .inline-form select,
[data-bs-theme="dark"] .field input,
/* .field input was here but textarea/select were not, so a notes box stayed white on the dark
   surface — visible on the booking edit page and the booking calendar's edit modal, both of which
   pair a .field with a <textarea>. .form-grid hardcodes `background: #fff`, so without these two the
   token override never reaches them. */
[data-bs-theme="dark"] .field textarea,
[data-bs-theme="dark"] .field select,
[data-bs-theme="dark"] .table-tools__search,
[data-bs-theme="dark"] .table-tools__filter,
[data-bs-theme="dark"] .filter-bar__field select,
[data-bs-theme="dark"] .filter-bar__field input,
[data-bs-theme="dark"] .toolbar-search,
[data-bs-theme="dark"] .dt-container .dt-search input,
[data-bs-theme="dark"] .select2-container--default .select2-selection--single {
    background: var(--surface-3);
    color: var(--bs-text);
    border-color: var(--bs-line);
}
/* The rule above uses the `background` shorthand, which resets background-image to none — that wipes
   the search box's magnifying-glass icon in dark mode. Restore it (the #9aa1ab stroke reads fine on
   the dark surface) using background-color so the icon, repeat and position survive. */
[data-bs-theme="dark"] .table-tools__search {
    background-color: var(--surface-3);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%239aa1ab' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='7' cy='7' r='5'/%3E%3Cline x1='10.5' y1='10.5' x2='15' y2='15'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: 0.8rem center;
}
[data-bs-theme="dark"] .select2-container--default .select2-selection--single .select2-selection__rendered { color: var(--bs-text); }

/* Neutral "face" buttons that assumed white. */
[data-bs-theme="dark"] .btn--ghost { background: var(--surface-3); color: var(--bs-text); }

/* RSPH-3: filter pills & modal rows share the ghost-control face in dark mode (were hardcoded white). */
[data-bs-theme="dark"] .pill { background: var(--surface-3); border-color: var(--bs-line); color: var(--bs-text); }
[data-bs-theme="dark"] .park-strip__chip { background: var(--surface-3); border-color: var(--bs-line); color: var(--bs-text); }
/* The Cards/Table switcher had the inverse problem in dark mode: its pressed fill is ink, which IS the
   page here, so the view you were already on lost its face while the other one kept the raised ghost
   button — the toolbar invited you to click the view you were looking at. Pressed takes the hi-vis
   fill, the same "this one is selected" answer the pills give. */
[data-bs-theme="dark"] .view-toggle .btn[aria-pressed="true"],
[data-bs-theme="dark"] .view-toggle .btn[aria-pressed="true"]:hover {
    background: var(--bs-hi-vis);
    border-color: var(--bs-hi-vis);
    color: var(--bs-ink-2);
}
[data-bs-theme="dark"] .board-modal__tech:hover { background: var(--surface-3); }
[data-bs-theme="dark"] .board-modal__tech.is-selected { background: rgba(0, 222, 145, 0.14); }
[data-bs-theme="dark"] .board-modal__worker-busy { color: var(--due-amber); }
[data-bs-theme="dark"] .btn--danger { background: var(--surface-3); color: #ff9d97; border-color: #6d2b2b; }
[data-bs-theme="dark"] .btn--danger:hover { background: #3a2626; }

/* Disabled fields & prefix chips. */
[data-bs-theme="dark"] .form-grid select:disabled,
[data-bs-theme="dark"] .filter-bar__field input:disabled,
[data-bs-theme="dark"] .filter-bar__field select:disabled,
[data-bs-theme="dark"] .prefixed-input__prefix,
[data-bs-theme="dark"] .select2-container--disabled .select2-selection--single { background: var(--surface-2); color: var(--bs-muted); }

/* Tables: row dividers and hover. */
[data-bs-theme="dark"] .data-table tbody td,
[data-bs-theme="dark"] .data-table tbody th { border-bottom-color: var(--bs-line); }
/* #b3261e is unreadable on a dark surface; this is the same lightened red the danger button uses. */
[data-bs-theme="dark"] .data-table__reg--loss { color: #ff9d97; }
[data-bs-theme="dark"] .data-table tbody tr:hover { background: var(--surface-3); }

/* Dashboard neutral fills. */
[data-bs-theme="dark"] .kpi-board { background: var(--surface-3); box-shadow: inset 0 0 0 1px var(--bs-line); }
[data-bs-theme="dark"] .kpi-board__seg--empty { background: var(--surface-3); }
[data-bs-theme="dark"] .kpi-pipeline__row { border-bottom-color: var(--bs-line); }

/* Notifications. */
[data-bs-theme="dark"] .notif { border-bottom-color: var(--bs-line); }
/* Dark flips the unread wash to green rather than following light's blue: on this canvas the blue
   tint is nearly the surface colour and the row stopped reading as unread at all, while the mint
   green already carries "live" here. 0.10 matches the lift the old amber wash had against the panel
   (measured, not guessed) while staying below the 0.14 green chip so the chip still chips. */
[data-bs-theme="dark"] .notif--unread { background: rgba(0, 222, 145, 0.10); }

/* Vehicle-card photo well. */
[data-bs-theme="dark"] .vehicle-card__photo { background: var(--surface-3); }
[data-bs-theme="dark"] .vehicle-card__photo--empty { background: linear-gradient(135deg, #23272e, #1a1d22); }
/* Inventory row thumbnail placeholder (RSPH-138) — same well treatment as the empty card slot. */
[data-bs-theme="dark"] .thumb-placeholder { background: linear-gradient(135deg, #23272e, #1a1d22); color: #6d7783; }

/* Technician tiles on the dark surface: same dark card shell as the vehicle cards; the accent tokens
   already read well on dark, and color-mix keeps the pill tint subtle. */
/* Only the surface. The border is deliberately NOT restated: `--bs-line` is already redefined for
   the dark theme, so `.tech-card`'s own rule resolves correctly — and restating it here, at higher
   specificity and further down the file, would beat the clock-band border colours and leave every
   banded tile grey in dark mode. */
[data-bs-theme="dark"] .tech-card { background: var(--surface); }
[data-bs-theme="dark"] .tech-card--on_job  { --tech-accent: #4ade80; }
[data-bs-theme="dark"] .tech-card--idle    { --tech-accent: #ffcf4d; }
[data-bs-theme="dark"] .tech-card--holiday { --tech-accent: #7aa2ff; }
/* A lighter tint of the same purple, matching how every other accent is handled on the dark
   surface (holiday's #2563eb becomes #7aa2ff). Overrun needs no entry: it resolves --tech-accent to
   var(--due-red), which is already themed. */
[data-bs-theme="dark"] .tech-card--sick    { --tech-accent: #c4b5fd; }
[data-bs-theme="dark"] .tech-card__name { color: var(--bs-text); }
[data-bs-theme="dark"] .tech-card__job { color: var(--bs-text); }
[data-bs-theme="dark"] .tech-card__photo { background: var(--surface-3); }
[data-bs-theme="dark"] .tech-card__photo--empty { background: linear-gradient(135deg, #23272e, #1a1d22); }
/* Staff Training bands on the dark surface (RSPH-140): the light-theme reds and greens go muddy
   against it, so they lift the same way the clock states above do. The count is the tile's headline
   and --bs-ink is not theme-aware, so it needs the same explicit swap .tech-card__name gets. */
[data-bs-theme="dark"] .tech-card--expired       { --tech-accent: #ff8a80; }
[data-bs-theme="dark"] .tech-card--expiring_soon { --tech-accent: #ffcf4d; }
[data-bs-theme="dark"] .tech-card--current       { --tech-accent: #4ade80; }
[data-bs-theme="dark"] .tech-card--no_expiry     { --tech-accent: #9aa4b0; }
[data-bs-theme="dark"] .tech-card--none          { --tech-accent: #6b7280; }
[data-bs-theme="dark"] .tech-card__count { color: var(--bs-text); }
[data-bs-theme="dark"] .photo-field__preview { background: var(--surface-3); border-color: var(--bs-line); }
[data-bs-theme="dark"] .photo-field__preview--empty { background: linear-gradient(135deg, #23272e, #1a1d22); }
[data-bs-theme="dark"] .tech-modal__panel { background: var(--surface); }
[data-bs-theme="dark"] .tech-start__job { background: var(--surface-3); border-color: var(--bs-line); }
[data-bs-theme="dark"] .typeahead__option { color: var(--bs-text); }
[data-bs-theme="dark"] .typeahead__option:hover { background: var(--surface-3); }

/* Near-white hovers / selected rows. */
[data-bs-theme="dark"] .select2-container--default .select2-results__option--selected { background: var(--surface-3); }
[data-bs-theme="dark"] .dt-container .dt-paging .dt-paging-button:hover:not(.current):not(.disabled) { background: var(--surface-3); border-color: var(--bs-line); }

/* Workforce HR calendar surfaces (sticky header + name column hardcoded #fff). */
[data-bs-theme="dark"] .wf-calendar-table thead th,
[data-bs-theme="dark"] .wf-calendar-table .wf-calendar__name { background: var(--surface); }
[data-bs-theme="dark"] .wf-calendar-table th,
[data-bs-theme="dark"] .wf-calendar-table td { border-bottom-color: var(--bs-line); border-right-color: rgba(255, 255, 255, 0.05); }
[data-bs-theme="dark"] .wf-calendar-table th.is-weekend,
[data-bs-theme="dark"] .wf-calendar-table td.is-weekend { background: rgba(255, 255, 255, 0.07); }
/* No border override here on purpose. The bracket blue carries across unchanged from the light
   rules: #02b3fe holds up on both surfaces, and giving dark mode its own edge colour is what made
   the block dissolve when the earlier grey hairline resolved to --bs-line. If you add a rule here,
   tests/Browser/calendar-weekend/check.mjs runs both themes and will tell you. */
[data-bs-theme="dark"] .wf-cell--off { background: var(--surface-3); border-color: var(--bs-line); }

/* Secondary editorial accent — dark mode only. A magenta highlight on the labelling elements
   (subsection headings, eyebrow kickers) adds a second colour to the dark theme without touching the
   green, which stays reserved for interactive state (nav, links, focus, CTAs). #9c009f reads well as a
   solid accent bar; the eyebrow uses a lightened tint of the same hue so the small uppercase text keeps
   AA contrast on the dark surfaces. */
[data-bs-theme="dark"] {
    --accent-magenta: #9c009f;
}
[data-bs-theme="dark"] .section-heading {
    position: relative;
    padding-inline-start: 0.7rem;
}
[data-bs-theme="dark"] .section-heading::before {
    content: "";
    position: absolute;
    inset-inline-start: 0;
    top: 0.15em;
    bottom: 0.15em;
    width: 3px;
    border-radius: 2px;
    background: var(--accent-magenta);
}
[data-bs-theme="dark"] .eyebrow { color: #cf63d1; }

/* Toast lifts a touch above the page. */
[data-bs-theme="dark"] .rs-toast { background: #23272e; }

/* Neon status badges — dark mode only. In light mode the coloured badges are pale-tint chips on a
   white page; dropped onto the dark surface that same tint reads flat and washed-out. Re-tone them
   as glowing chips instead: a translucent fill of the hue, a saturated edge, bright text, and one
   restrained outer glow — the single bold move. Neutral stays quiet (no glow) so the accent hues
   carry the signal. Covers the RAG variants and their semantic aliases. */
[data-bs-theme="dark"] .badge-flag { font-weight: 600; }
[data-bs-theme="dark"] .badge-flag--green,
[data-bs-theme="dark"] .badge-flag--ok,
[data-bs-theme="dark"] .badge-flag--authorised {
    background: rgba(0, 222, 145, 0.14);
    color: #3ff5b8;
    border-color: rgba(0, 222, 145, 0.55);
    box-shadow: 0 0 10px rgba(0, 222, 145, 0.25);
}
[data-bs-theme="dark"] .badge-flag--amber,
[data-bs-theme="dark"] .badge-flag--credit,
[data-bs-theme="dark"] .badge-flag--pending,
[data-bs-theme="dark"] .badge-flag--warning {
    background: rgba(255, 184, 28, 0.14);
    color: #ffcf4d;
    border-color: rgba(255, 184, 28, 0.6);
    box-shadow: 0 0 10px rgba(255, 184, 28, 0.25);
}
[data-bs-theme="dark"] .board-modal__error,
[data-bs-theme="dark"] .badge-flag--red,
[data-bs-theme="dark"] .badge-flag--stop,
[data-bs-theme="dark"] .badge-flag--declined,
[data-bs-theme="dark"] .badge-flag--danger {
    background: rgba(255, 76, 62, 0.15);
    color: #ff8a80;
    border-color: rgba(255, 90, 76, 0.6);
    box-shadow: 0 0 10px rgba(255, 76, 62, 0.28);
}
[data-bs-theme="dark"] .badge-flag--blue {
    background: rgba(2, 179, 254, 0.14);
    color: #7fd8ff;
    border-color: rgba(2, 179, 254, 0.6);
    box-shadow: 0 0 10px rgba(2, 179, 254, 0.25);
}
[data-bs-theme="dark"] .badge-flag--violet {
    background: rgba(155, 109, 255, 0.16);
    color: #cdb4ff;
    border-color: rgba(155, 109, 255, 0.6);
    box-shadow: 0 0 10px rgba(155, 109, 255, 0.25);
}
[data-bs-theme="dark"] .badge-flag--neutral,
[data-bs-theme="dark"] .badge-flag--cancelled {
    background: rgba(255, 255, 255, 0.07);
    color: #c4cdda;
    border-color: rgba(255, 255, 255, 0.22);
}

/* =====================================================================   Workshop TV "Live View" (RSPH-64)
   A wall-mounted, full-screen board: the curated workshop stages as tiles, each
   auto-scrolling its vehicles, pulsing when one moves. Ported from the signed-off
   demo. Everything is scoped under `.workshop-tv` and its own tokens (incl.
   --rag-*). The board defaults to the dark palette below and follows the portal's
   theme selection: a `[data-bs-theme="light"]` block further down re-tokenises it
   to a light palette (the shared theme-init script stamps the choice on <html>).
   ============================================================================ */
.workshop-tv {
    --bs-ink: #15171a;
    --bs-ink-2: #0e0f11;
    --bs-steel: #21252b;
    --bs-steel-2: #2b3037;
    --bs-hi-vis: #00de91;
    --bs-line: #333a43;
    --bs-muted: #98a2b0;
    --rag-green: #2ec77e;
    --rag-amber: #f0b429;
    --rag-red: #f2685f;
    /* Neutral-stage row accents read as the board's signature green rather than a dead grey. */
    --rag-neutral: var(--bs-hi-vis);
    /* Custom properties resolve at their declaring element, not at point of use — --hover-ring is
       declared on :root as var(--bs-hi-vis), so without this re-declaration it would keep resolving
       to the portal's root green instead of following this board's own re-tokenised --bs-hi-vis. */
    --hover-ring: var(--bs-hi-vis);

    position: fixed;
    inset: 0;
    z-index: 1;
    overflow: hidden;
    background: radial-gradient(120% 120% at 50% 0%, #1a1d22 0%, var(--bs-ink-2) 70%);
    color: #e9ecf1;
    font-family: var(--bs-font-ui);
    -webkit-font-smoothing: antialiased;
    display: flex;
    flex-direction: column;
}

/* Header ------------------------------------------------------------------ */
.workshop-tv .tv-head {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: clamp(0.5rem, 1.2vh, 1rem) clamp(1rem, 2vw, 2rem);
    border-bottom: 3px solid var(--bs-hi-vis);
}
.workshop-tv .tv-brand { display: flex; align-items: baseline; gap: 0.8rem; }
.workshop-tv .tv-brand__logo {
    height: clamp(1.6rem, 2.8vw, 2.6rem);
    width: auto;
    display: block;
}
.workshop-tv .tv-brand__site {
    font-family: var(--bs-font-mono);
    font-size: clamp(0.6rem, 0.95vw, 0.85rem);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--bs-muted);
}
.workshop-tv .tv-head__right { display: flex; align-items: center; gap: clamp(1rem, 2.2vw, 2.2rem); }
.workshop-tv .tv-live {
    display: flex; align-items: center; gap: 0.5rem;
    font-family: var(--bs-font-mono);
    font-size: clamp(0.62rem, 1vw, 0.9rem);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--bs-hi-vis);
}
.workshop-tv .tv-live__dot {
    width: 0.7em; height: 0.7em; border-radius: 50%;
    background: var(--bs-hi-vis);
    box-shadow: 0 0 0 0 rgba(0, 222, 145, 0.6);
    animation: wtv-live-pulse 2s ease-out infinite;
}
@keyframes wtv-live-pulse {
    0% { box-shadow: 0 0 0 0 rgba(0, 222, 145, 0.55); }
    70% { box-shadow: 0 0 0 0.7em rgba(0, 222, 145, 0); }
    100% { box-shadow: 0 0 0 0 rgba(0, 222, 145, 0); }
}
.workshop-tv .tv-clock {
    font-family: var(--bs-font-display);
    font-size: clamp(1.6rem, 3vw, 2.8rem);
    letter-spacing: 0.04em;
    line-height: 1;
    color: #fff;
    font-variant-numeric: tabular-nums;
}
.workshop-tv .tv-clock small {
    display: block;
    font-family: var(--bs-font-mono);
    font-size: 0.32em;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--bs-muted);
    text-align: right;
}
.workshop-tv .tv-btn {
    font: inherit; cursor: pointer; text-decoration: none;
    background: var(--bs-steel); color: #e9ecf1;
    border: 1px solid var(--bs-line); border-radius: 8px;
    padding: 0.45rem 0.75rem; font-size: 0.85rem; font-weight: 600;
    display: inline-flex; align-items: center; gap: 0.4rem;
    transition: border-color 0.15s, background 0.15s;
}

/* Stage grid -------------------------------------------------------------- */
.workshop-tv .tv-grid {
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    grid-auto-rows: 1fr;
    gap: clamp(0.4rem, 0.9vw, 0.9rem);
    padding: clamp(0.4rem, 0.9vw, 0.9rem);
}
.workshop-tv .tile {
    display: flex;
    flex-direction: column;
    min-height: 0;
    background: var(--bs-steel);
    border: 1.5px solid var(--bs-line);
    border-radius: 12px;
    overflow: hidden;
    position: relative;
}
.workshop-tv .tile__head {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: clamp(0.35rem, 0.9vh, 0.6rem) clamp(0.5rem, 0.9vw, 0.85rem);
    border-bottom: 1px solid var(--bs-line);
    background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent);
}
.workshop-tv .tile__name {
    font-family: var(--bs-font-display);
    font-size: clamp(1rem, 1.5vw, 1.7rem);
    letter-spacing: 0.03em;
    line-height: 1;
    color: #02b3fe;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.workshop-tv .tile__count {
    flex: 0 0 auto;
    font-family: var(--bs-font-mono);
    font-size: clamp(0.7rem, 1.1vw, 1rem);
    font-weight: 700;
    color: var(--bs-hi-vis);
    background: rgba(0, 222, 145, 0.12);
    border-radius: 999px;
    padding: 0.1rem 0.55rem;
    min-width: 1.8em;
    text-align: center;
}
/* Scrollable so a person on a computer can wheel/drag through a busy stage (the JS auto-scroll pauses
   on interaction and resumes after 15s). The scrollbar is hidden so the TV board still looks clean;
   overscroll-behavior keeps a wheel scroll inside the tile from scrolling the page behind it. */
.workshop-tv .tile__list { flex: 1 1 auto; min-height: 0; overflow-y: auto; position: relative; overscroll-behavior: contain; scrollbar-width: none; }
.workshop-tv .tile__list::-webkit-scrollbar { width: 0; height: 0; }
.workshop-tv .tile__scroll { display: flex; flex-direction: column; gap: 0.3rem; padding: 0.35rem; }
.workshop-tv .tile__empty {
    height: 100%;
    display: flex; align-items: center; justify-content: center;
    color: #5b6470;
    font-family: var(--bs-font-mono);
    font-size: 0.7rem; letter-spacing: 0.1em; text-transform: uppercase;
}

/* Vehicle card ------------------------------------------------------------ */
.workshop-tv .veh {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: 0.5rem;
    padding: 0.35rem 0.45rem;
    border-radius: 9px;
    background: var(--bs-ink);
    border-left: 4px solid var(--rag, var(--rag-neutral));
}
/* Quality Control rows get a royal-blue left accent (its stage RAG is amber, shared with Awaiting
   Loading, so override the row colour directly rather than the tile's --rag token). */
.workshop-tv .tile[data-code="quality_control"] .veh { border-left-color: #4169e1; }
/* Deadline urgency recolours the row's RAG accent (the left bar) rather than the whole row — it
   overrides the stage colour, incl. Quality Control's blue: amber 2-3 days from the requested
   completion date, red at 1 day away / due today / overdue. Only the red accent flashes; the row
   itself stays put. Specificity matches the QC rule and sits after it, so it wins. */
.workshop-tv .tile .veh.veh--due-amber { border-left-color: var(--rag-amber); }
.workshop-tv .tile .veh.veh--due-red {
    border-left-color: var(--rag-red);
    animation: tvDueAccentFlash 1.3s ease-in-out infinite;
}
@keyframes tvDueAccentFlash {
    0%, 100% { border-left-color: #f2685f; }
    50% { border-left-color: #000; }
}
@media (prefers-reduced-motion: reduce) {
    .workshop-tv .veh--due-red { animation: none; }
}
.workshop-tv .veh__emblem {
    width: clamp(24px, 2vw, 34px);
    height: clamp(24px, 2vw, 34px);
    object-fit: contain;
    border-radius: 6px;
    /* Dark grey to match the Live Job Status card emblem slot (--surface-3 in dark theme); the
       always-dark TV scope uses the literal since --surface-3 is only defined under data-bs-theme. */
    background: #23272e;
    padding: 2px;
    flex: 0 0 auto;
}
.workshop-tv .veh__emblem--empty {
    display: inline-flex; align-items: center; justify-content: center;
    background: linear-gradient(135deg, #2b3037, #21252b);
    color: #8a94a2;
    font-family: var(--bs-font-display);
    font-size: clamp(0.8rem, 1.1vw, 1.1rem);
}
/* Round 3 (NEW-5): the registration is the car's primary identifier on a read-at-distance screen and
   must never be the one that gives way. `.veh` is a CSS grid (`auto 1fr auto`) — the id column here is
   the `1fr` track, and grid gives an `1fr` track's automatic minimum size as `auto` (≈ its content's
   min-content) UNLESS the item overrides it, which `min-width: 0` used to do deliberately. That let the
   third (`auto`) column — `.veh__meta`, sized to its own max-content in the earlier maximize-tracks
   step — take however much space a long crew roster or trade needed, starving this one to nothing
   first. The floor below reserves enough room for a plate before that starvation can begin; `.veh__reg`
   keeps its own overflow handling as a last-resort backstop (a floor bounds the common case, not every
   possible one), but the crew/location lines are now the ones that yield — see .veh__tech/.veh__loc. */
.workshop-tv .veh__id { min-width: 9rem; }
.workshop-tv .veh__reg {
    font-family: var(--bs-font-mono);
    font-weight: 700;
    letter-spacing: 0.05em;
    font-size: clamp(0.85rem, 1.25vw, 1.4rem);
    color: #fff;
    line-height: 1.05;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.workshop-tv .veh__model {
    font-size: clamp(0.62rem, 0.9vw, 0.9rem);
    color: var(--bs-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.workshop-tv .veh__meta {
    flex: 0 0 auto;
    display: flex; flex-direction: column; align-items: flex-end;
    gap: 0.12rem; min-width: 0; text-align: right;
}
/* Task 12: which trade a rework car is back for — the tile heading (e.g. "Rectification Work") no
   longer implies one, since any trade can be the one sent back to fix it. The kind (a short, fixed,
   translated word) stays a pill; the trade — a tenant-custom code with no natural ceiling on its
   length — is its own plain-text row underneath rather than packed into the same pill, so it can wrap
   across lines instead of either truncating (round 1: unacceptable, the trade is the whole point of
   the card) or forcing the pill wide enough to starve .veh__id beside it (round 1's actual bug). Only
   this row is width-bounded — not the shared .veh__meta column .veh__tech/.veh__parts/.veh__loc also
   live in — so a busy Body Repair tile's crew list is untouched by this cap. */
.workshop-tv .veh__rework {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.15rem;
}
.workshop-tv .veh__rework-kind {
    display: inline-block;
    font-size: clamp(0.58rem, 0.8vw, 0.8rem);
    font-weight: 700;
    color: #fff;
    background: rgba(155, 109, 255, 0.18);
    border: 1px solid rgba(155, 109, 255, 0.45);
    border-radius: 999px;
    padding: 0.02rem 0.45rem;
    white-space: nowrap;
}
.workshop-tv .veh__rework-kind--warranty {
    background: rgba(2, 179, 254, 0.16);
    border-color: rgba(2, 179, 254, 0.4);
}
.workshop-tv .veh__rework-trade {
    display: block;
    max-width: 10rem;
    font-size: clamp(0.6rem, 0.85vw, 0.85rem);
    font-weight: 700;
    color: #cdb4ff;
    line-height: 1.2;
    /* Wraps rather than clipping — see the block comment above for why this one never hides an
       overflowing tail the way its neighbours are allowed to. */
    white-space: normal;
    overflow-wrap: break-word;
    text-align: right;
}
.workshop-tv .veh__parts {
    font-family: var(--bs-font-mono);
    font-size: clamp(0.58rem, 0.8vw, 0.8rem);
    font-weight: 700;
    color: var(--rag-amber);
    background: rgba(240, 180, 41, 0.14);
    border: 1px solid rgba(240, 180, 41, 0.35);
    border-radius: 999px;
    padding: 0.02rem 0.45rem;
    white-space: nowrap;
}
.workshop-tv .veh__parts--none {
    color: #fff;
    background: var(--rag-red);
    border-color: var(--rag-red);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    box-shadow: 0 0 10px 1px rgba(242, 104, 95, 0.5);
}
/* Every part in reads blue on the Workshop TV, matching the Live Job Status card and the running-clock
   dot's "this is live and in hand" blue — as opposed to amber (still waiting on parts). */
.workshop-tv .veh__parts--complete { background: #cfe8fa; color: #064e73; border: 1px solid #6fbde8; }
.workshop-tv .veh__tech {
    font-size: clamp(0.62rem, 0.9vw, 0.9rem);
    font-weight: 600;
    color: #e9ecf1;
    white-space: nowrap;
    /* Round 3 (NEW-5): this is the line that now yields under space pressure — several co-assignees
       (RSPH-3's "put on a job appends, doesn't displace") can run long, and .veh__id's floor means the
       registration no longer will. Abbreviates rather than bleeding into a neighbour. */
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* Running clock: a blue dot before the name of a technician actually clocked onto this car, as
   opposed to just assigned to the stage. Pulses continuously (unlike the tile's one-off update
   flash) because it reports a state, not an event — same idiom as the header's .tv-live__dot and the
   Technician Status tile's on-job light. */
.workshop-tv .veh__tech-dot {
    display: inline-block;
    width: 0.55em; height: 0.55em;
    margin-inline-end: 0.32em;
    border-radius: 50%;
    background: #02b3fe;
    animation: wtv-tech-clock-pulse 1.6s ease-in-out infinite;
}
@keyframes wtv-tech-clock-pulse {
    0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(2, 179, 254, 0.6); }
    50%      { opacity: 0.55; box-shadow: 0 0 0 0.35em rgba(2, 179, 254, 0); }
}
.workshop-tv .veh__loc {
    display: inline-flex; align-items: center; gap: 0.2rem;
    font-size: clamp(0.56rem, 0.8vw, 0.8rem);
    color: var(--bs-hi-vis);
    font-weight: 600;
    white-space: nowrap;
    /* Round 3 (NEW-5): same reasoning as .veh__tech above — a long location name yields, the plate
       does not. max-width: 100% bounds the flex item so overflow/text-overflow can actually apply
       (the pin icon is flex: 0 0 auto and keeps its own size regardless). */
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
}
.workshop-tv .veh__pin { width: 0.85em; height: 0.85em; fill: currentColor; flex: 0 0 auto; opacity: 0.85; }

/* Pulse on update --------------------------------------------------------- */
.workshop-tv .tile.is-updated {
    animation: wtv-tile-pulse 1.15s ease-out;
    border-color: var(--bs-hi-vis);
    z-index: 2;
}
@keyframes wtv-tile-pulse {
    0% { box-shadow: 0 0 0 0 rgba(0, 222, 145, 0); }
    18% { box-shadow: 0 0 0 3px rgba(0, 222, 145, 0.9), 0 0 34px 4px rgba(0, 222, 145, 0.45); }
    100% { box-shadow: 0 0 0 0 rgba(0, 222, 145, 0); }
}

@media (max-width: 1100px) { .workshop-tv .tv-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 720px) { .workshop-tv .tv-grid { grid-template-columns: repeat(2, 1fr); } }
@media (prefers-reduced-motion: reduce) {
    .workshop-tv .tv-live__dot, .workshop-tv .tile.is-updated,
    .workshop-tv .veh__tech-dot { animation: none; }
}

/* Light theme -------------------------------------------------------------
   The board follows the portal's theme selection (data-bs-theme on <html>, set
   by the shared theme-init script from the same 'rsph-theme' preference). In
   light mode we re-tokenise to a white-surface palette and override the handful
   of literal near-white text/surfaces the dark board hard-codes. The green is
   darkened so it stays legible as text/accent on white; RAG amber/red, the QC
   blue and the deadline flash already read on both. */
[data-bs-theme="light"] .workshop-tv {
    --bs-ink: #f2f5f8;        /* vehicle row fill */
    --bs-ink-2: #e6ebf0;
    --bs-steel: #ffffff;      /* tile / button surface */
    --bs-steel-2: #eef1f5;
    --bs-hi-vis: #0a9d68;     /* darker green: legible as text + fine as accent on white */
    --bs-line: #d7dde4;
    --bs-muted: #5b6675;
    /* Re-resolve against the light board's own --bs-hi-vis, same reasoning as the base .workshop-tv
       re-declaration above. */
    --hover-ring: var(--bs-hi-vis);
    background: radial-gradient(120% 120% at 50% 0%, #ffffff 0%, #e9edf2 70%);
    color: #1a1f26;
}
[data-bs-theme="light"] .workshop-tv .tv-btn { color: #2b3440; }
[data-bs-theme="light"] .workshop-tv .tv-clock,
[data-bs-theme="light"] .workshop-tv .veh__reg { color: #12161b; }
[data-bs-theme="light"] .workshop-tv .veh__tech { color: #2b3440; }
[data-bs-theme="light"] .workshop-tv .tile__name { color: #0a6fb0; }
[data-bs-theme="light"] .workshop-tv .tile__empty { color: #9aa4b0; }
[data-bs-theme="light"] .workshop-tv .veh__emblem { background: #eef1f5; }
[data-bs-theme="light"] .workshop-tv .veh__emblem--empty {
    background: linear-gradient(135deg, #eef1f4, #e2e7ec);
    color: #6b7683;
}
/* Swap the header logo to the dark mark on the light board (default board is dark → white mark). */
.workshop-tv .tv-brand__logo--for-light { display: none; }
[data-bs-theme="light"] .workshop-tv .tv-brand__logo--for-dark { display: none; }
[data-bs-theme="light"] .workshop-tv .tv-brand__logo--for-light { display: block; }

/* ── Cover-photo picker (Job View → Documents tab) ──────────────────────────────
   A row of selectable image thumbnails that mirror the Live Job Status card's photo
   slot (wide cover-fit on a near-black backdrop). The chosen tile carries the
   signature hi-vis ring + check; a "No cover" tile clears it. Selection is driven off
   the real checked radio via :has(), so it stays right even before the form re-renders. */
.cover-picker__hint { color: var(--bs-muted); font-size: 0.8rem; margin: 0 0 0.7rem; }
.cover-picker { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: stretch; }
.cover-option {
    position: relative;
    flex: 0 0 auto;
    width: 132px;
    aspect-ratio: 11 / 5;
    border: 1px solid var(--bs-line);
    border-radius: 10px;
    overflow: hidden;
    cursor: pointer;
    background: #0d0f12;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.cover-option input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.cover-option img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cover-option:has(input:checked) {
    border-color: var(--bs-hi-vis);
    box-shadow: inset 0 0 0 2px var(--bs-hi-vis);
}
.cover-option:has(input:focus-visible) { outline: 2px solid var(--hover-ring); outline-offset: 2px; }
/* Check badge, top-end corner, only on the selected tile. */
.cover-option__check {
    position: absolute;
    inset-block-start: 4px;
    inset-inline-end: 4px;
    width: 18px;
    height: 18px;
    display: none;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--bs-hi-vis);
    color: var(--bs-ink-2);
    font-size: 0.7rem;
    line-height: 1;
}
/* The tick is DRAWN, not typed. This badge used to render the character U+2713 from the template,
   and U+2713 is not in Archivo — nor in this server's sans-serif or monospace fallbacks — so the
   badge painted a .notdef tofu box instead of a tick. A glyph a font may or may not carry is not a
   safe way to draw a UI affordance; two borders on a rotated box always resolve to a tick.

   IMPORTANT if you add another cover picker: the badge must be EMPTY in the template. This is a
   `display: flex` box, so any text node left inside it becomes an anonymous flex item sitting
   BESIDE this pseudo-element — a drawn tick and a tofu box crammed into an 18px circle, which is
   worse than the bug this replaced. Four templates render this badge (fleet/_photos,
   workshop/equipment/show, jobs/job/show, parts/stock/_photos_panel) and all four are empty; each
   carries a comment saying why. */
.cover-option__check::before {
    content: "";
    width: 0.32rem;
    height: 0.6rem;
    margin-block-start: -0.14rem;
    border: solid currentColor;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}
.cover-option:has(input:checked) .cover-option__check { display: flex; }
/* The "No cover" tile is a quiet dashed placeholder rather than a photo. */
.cover-option--none {
    background: #fff;
    border-style: dashed;
    display: flex;
    align-items: center;
    justify-content: center;
}
.cover-option__none { color: var(--bs-muted); font-size: 0.74rem; font-weight: 600; }

/* ── Vehicle-photo picker carousel (Estimate → "Add from vehicle photos") ───────────────
   One photo at a time at full size, so a service advisor can actually tell whether a damage
   photo needs including — the 64px thumbnail strip below is for jumping around, not judging.
   Selection lives on the real checkbox in each thumb (estimate_vehicle_photo_picker_controller.js
   keeps the big "select this photo" toggle in sync with it); the thumb's check badge is driven
   off :has(), same drawn-tick idiom as .cover-option__check above — keep that badge span EMPTY
   in the template, see the comment on .cover-option__check for why. */
.photo-picker__stage { display: flex; align-items: center; gap: 0.6rem; }
.photo-picker__frame {
    flex: 1 1 auto;
    position: relative;
    height: min(52vh, 440px);
    border-radius: 10px;
    overflow: hidden;
    background: #0d0f12;
    display: flex;
    align-items: center;
    justify-content: center;
}
.photo-picker__image { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }
.photo-picker__nav {
    flex: 0 0 auto;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 1px solid var(--bs-line);
    background: var(--surface, #fff);
    color: var(--bs-text);
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.photo-picker__nav:hover { background: var(--bs-hi-vis); color: var(--bs-ink-2); border-color: var(--bs-hi-vis); }
.photo-picker__nav:focus-visible { outline: 2px solid var(--hover-ring); outline-offset: 2px; }
.photo-picker__toggle {
    position: absolute;
    inset-block-end: 0.6rem;
    inset-inline-start: 50%;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.4rem 0.9rem;
    border: none;
    border-radius: 999px;
    background: rgba(16, 24, 40, 0.65);
    color: #fff;
    font-size: 0.82rem;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.12s ease, color 0.12s ease;
}
.photo-picker__toggle:hover { background: rgba(16, 24, 40, 0.8); }
.photo-picker__toggle:focus-visible { outline: 2px solid var(--bs-hi-vis); outline-offset: 2px; }
.photo-picker__toggle.is-selected { background: var(--bs-hi-vis); color: var(--bs-ink-2); }
.photo-picker__toggle-check { width: 0.85rem; height: 0.85rem; display: none; }
.photo-picker__toggle.is-selected .photo-picker__toggle-check { display: block; }
.photo-picker__toggle-check::before {
    content: "";
    display: block;
    width: 0.3rem;
    height: 0.6rem;
    margin-block-start: -0.1rem;
    border: solid currentColor;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}
.photo-picker__counter { text-align: center; color: var(--bs-muted); font-size: 0.8rem; margin: 0.5rem 0 0.75rem; }
.photo-picker__thumbs { display: flex; gap: 0.5rem; overflow-x: auto; padding-block-end: 0.3rem; scroll-behavior: smooth; }
.photo-picker__thumb { position: relative; flex: 0 0 auto; }
.photo-picker__thumb-btn {
    display: block;
    width: 64px;
    height: 64px;
    padding: 0;
    border: 2px solid transparent;
    border-radius: 8px;
    overflow: hidden;
    cursor: pointer;
    background: none;
}
.photo-picker__thumb-btn img { width: 100%; height: 100%; object-fit: cover; display: block; }
.photo-picker__thumb.is-active .photo-picker__thumb-btn { border-color: var(--bs-hi-vis); }
.photo-picker__checkbox { position: absolute; inset-block-start: 2px; inset-inline-end: 2px; width: 18px; height: 18px; margin: 0; opacity: 0; cursor: pointer; z-index: 2; }
.photo-picker__thumb-check {
    position: absolute;
    inset-block-start: 2px;
    inset-inline-end: 2px;
    width: 18px;
    height: 18px;
    display: none;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--bs-hi-vis);
    color: var(--bs-ink-2);
    pointer-events: none;
}
.photo-picker__thumb-check::before {
    content: "";
    width: 0.32rem;
    height: 0.6rem;
    margin-block-start: -0.14rem;
    border: solid currentColor;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}
.photo-picker__thumb:has(.photo-picker__checkbox:checked) .photo-picker__thumb-check { display: flex; }

/* ── Photo gallery + lightbox (Documents tab) ─────────────────────────────
   The grid stays quiet and native; the immersive lightbox is the one signature moment. */
.photo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
    gap: 0.6rem;
    margin-top: 0.2rem;
}
.photo-tile {
    position: relative;
    display: block;
    aspect-ratio: 1 / 1;
    border: 1px solid var(--bs-line);
    border-radius: 10px;
    overflow: hidden;
    background: #0d0f12;
    transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.photo-tile img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.2s ease; }
.photo-tile:hover img { transform: scale(1.05); }
.photo-tile:focus-visible { outline: 2px solid var(--hover-ring); outline-offset: 2px; }
.photo-tile__zoom {
    position: absolute;
    inset-block-end: 5px;
    inset-inline-end: 5px;
    width: 22px;
    height: 22px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 6px;
    background: rgba(13, 15, 18, 0.66);
    color: #fff;
    font-size: 0.82rem;
    line-height: 1;
    opacity: 0;
    transition: opacity 0.12s ease;
}
.photo-tile:hover .photo-tile__zoom,
.photo-tile:focus-visible .photo-tile__zoom { opacity: 1; }

/* ── Courtesy car damage diagram (RSPH-178) ───────────────────────────────
   Five clickable elevations; a tap fills the hidden add-damage form (courtesy_car_damage_
   controller.js) and drops a pending marker at the tapped point.

   The click fraction is measured off the <svg>'s own getBoundingClientRect(), so the preview
   marker's positioning context must be a box that is PIXEL-IDENTICAL to the <svg>'s rendered box
   — not merely near it. `.car-diagram` is not that box: it also contains `.car-diagram__label`
   (real height, sits above the svg) and centres its content via `text-align`, so on a wide grid
   track the svg can render narrower than `.car-diagram` itself. Either difference — extra height
   from the label, horizontal centring slack — throws off a percentage computed against the wrong
   box: a click at the svg's top edge would resolve against `.car-diagram`'s full height (label +
   svg) and land visibly below where it was clicked. `.car-diagram__surface` exists solely to be
   that exact box: `display: inline-block` shrink-wraps it to the <svg>'s rendered size with no
   label and no centring slack, `position: relative` makes it the pending marker's positioning
   context, and the svg is forced `display: block` so no inline baseline gap sneaks a few px onto
   the wrapper's height. Append the marker into `.car-diagram__surface` (data-diagram-view lives
   there now), not `.car-diagram`. */
/* 300px, not the 200px this carried while the diagrams were line drawings. The photographs are much
   wider than they are tall — a side elevation is nearly 4:1 where the old drawing was under 2:1 — so
   at the same track width they render at half the height, and the whole car becomes a 60px-tall
   click target. Wider tracks buy that height back. */
.car-diagram-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 1rem;
    margin-bottom: 1rem;
}
.car-diagram { text-align: center; }
.car-diagram__label { margin: 0 0 0.35rem; font-size: 0.82rem; color: var(--bs-muted); }
.car-diagram__surface { position: relative; display: inline-block; }
.car-diagram__surface svg { display: block; max-width: 100%; height: auto; cursor: crosshair; }

/* Theme token for the damage marker. The five templates under templates/fleet/car_diagram/ are
   shared with the Receiving Sheet PDF and the handover sheet, where dompdf base64s them into an
   <img> and NO stylesheet reaches them — so they emit this var() reference only when their optional
   `view` parameter is passed, which only the on-screen include does. Paper keeps the literal.

   Light mode draws on white bodywork, so #c00000 reads clearly. Dark mode swaps to --due-red
   (already tuned elsewhere in this file for legibility on the dark surfaces), because the artwork
   itself is now genuinely transparent line art sitting directly on the dark panel — see
   .car-diagram__art--dark below — rather than a white-boxed photo the old #c00000 was tuned for. */
.car-diagram__surface {
    --cd-mark: #c00000;
}
[data-bs-theme="dark"] .car-diagram__surface {
    --cd-mark: var(--due-red);
}
/* Each view now renders BOTH a light and a dark <image>, and CSS picks the one for the active
   theme — plain visibility swap, no filter/recolour trickery. Both are transparent-background
   line art (see tools/car_diagram/slice.py), so unlike the photographs they replaced, dark mode
   needs no boxed-white backing to avoid a hard-edged rectangle on the panel. */
.car-diagram__art--dark { display: none; }
[data-bs-theme="dark"] .car-diagram__art--light { display: none; }
[data-bs-theme="dark"] .car-diagram__art--dark { display: block; }
/* The preview circle that follows the tap before the defect is saved. courtesy_car_damage_controller
   re-parents this element into the clicked [data-diagram-view] — i.e. into .car-diagram__surface —
   so --cd-mark is in scope by the time it is ever visible, and the preview is the SAME red as the
   marker that replaces it once saved. Hard-coding #c00000 here made the two reds differ in dark mode
   and left the preview at 2.55:1, the exact contrast the dark-mode pass removed everywhere else.
   The fallback covers the pre-first-click state, where this element is still hidden outside the
   surface and --cd-mark would otherwise be undefined. */
.pending-marker {
    position: absolute;
    width: 18px;
    height: 18px;
    margin: -9px 0 0 -9px;
    border-radius: 50%;
    border: 2px dashed var(--cd-mark, #c00000);
    pointer-events: none;
}
/* A repaired defect drops off the diagram (see _damage.html.twig) but keeps its row in the log,
   de-emphasised — the same "fade the row, keep the record" treatment as .is-total-loss / .is-reversed. */
.data-table tbody tr.is-repaired td { opacity: 0.55; }

/* ── Courtesy car handover signature pad (RSPH-178 Task 15) ───────────────
   A canvas the customer signs with a finger, a stylus or a mouse.

   `touch-action: none` is the load-bearing line: without it a finger dragged across the canvas
   scrolls the page and draws nothing at all, which is the single most likely way this screen fails
   on the tablet it exists for. The pad is deliberately WHITE in both themes — the signature is
   flattened to a PNG and printed onto a white sheet, so a dark-mode pad would produce ink the
   customer cannot see against the paper. The canvas has no intrinsic size, so the height here is
   what signature_pad_controller.js measures at connect(); changing it changes the drawing surface. */
.signature-pad {
    border: 1px solid var(--bs-line);
    border-radius: 10px;
    background: #ffffff;
    overflow: hidden;
}
.signature-pad__canvas {
    display: block;
    width: 100%;
    height: 220px;
    touch-action: none;
    cursor: crosshair;
    background: #ffffff;
}
.signature-pad__statement {
    font-size: 0.86rem;
    line-height: 1.5;
    color: var(--bs-muted);
    margin: 1.1rem 0;
}
.signature-pad__hint { font-size: 0.8rem; color: var(--bs-muted); margin: 0.4rem 0 0; }

/* Lightbox: a native <dialog> centred over a dimmed backdrop. Theme-agnostic (always dark). */
.lightbox {
    border: none;
    background: transparent;
    padding: 0;
    max-width: 96vw;
    max-height: 94vh;
    overflow: visible;
}
.lightbox::backdrop { background: rgba(8, 10, 13, 0.86); backdrop-filter: blur(2px); }
.lightbox__figure { margin: 0; display: flex; flex-direction: column; align-items: center; gap: 0.6rem; }
/* The viewport is the box the image used to be; the image now moves inside it under a transform.
   `overflow: hidden` is what keeps a zoomed photo from painting over the caption and the prev/next
   buttons, and `touch-action: none` is what makes a pinch zoom the photo rather than the page. */
.lightbox__viewport {
    overflow: hidden;
    touch-action: none;
    border-radius: 10px;
    box-shadow: 0 20px 60px -20px rgba(0, 0, 0, 0.7);
    background: #0d0f12;
    display: flex;
    align-items: center;
    justify-content: center;
}
.lightbox__img {
    max-width: 96vw;
    max-height: 82vh;
    object-fit: contain;
    /* Scaling about the centre is what zoomAbout()'s arithmetic assumes. */
    transform-origin: center center;
    /* Eases the discrete steps (buttons, double-click). A drag re-applies the transform every
       pointermove, so at this duration the image tracks the finger without visible lag. */
    transition: transform 0.08s ease-out;
    will-change: transform;
    display: block;
}
.lightbox__img.is-zoomed { cursor: grab; }
.lightbox__img.is-zoomed:active { cursor: grabbing; }
.lightbox__zoom {
    display: flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.2rem 0.3rem;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.12);
}
.lightbox__zoom-btn {
    width: 26px;
    height: 26px;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    background: transparent;
    color: #fff;
    font-size: 1rem;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
}
/* Tabular figures so the readout does not jog the buttons sideways as it steps 100% → 240% →
   1000%; the min-width holds the widest of those. */
.lightbox__zoom-level {
    min-width: 3.4em;
    text-align: center;
    color: #e9edf2;
    font-size: 0.76rem;
    font-variant-numeric: tabular-nums;
}
.lightbox__zoom-reset {
    border: none;
    border-radius: 6px;
    cursor: pointer;
    background: transparent;
    color: #fff;
    font-size: 0.76rem;
    font-weight: 600;
    padding: 0.25rem 0.45rem;
}
.lightbox__zoom-btn:hover,
.lightbox__zoom-reset:hover { background: var(--bs-hi-vis); color: var(--bs-ink-2); }
.lightbox__zoom-btn:focus-visible,
.lightbox__zoom-reset:focus-visible { outline: 2px solid var(--bs-hi-vis); outline-offset: 2px; }
.lightbox__caption { color: #e9edf2; font-size: 0.82rem; text-align: center; margin: 0; }
.lightbox__btn {
    position: fixed;
    top: 50%;
    transform: translateY(-50%);
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: none;
    cursor: pointer;
    background: rgba(255, 255, 255, 0.12);
    color: #fff;
    font-size: 1.5rem;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.12s ease;
}
.lightbox__btn:hover { background: var(--bs-hi-vis); color: var(--bs-ink-2); }
.lightbox__btn:focus-visible { outline: 2px solid var(--bs-hi-vis); outline-offset: 2px; }
.lightbox__btn--prev { inset-inline-start: 3vw; }
.lightbox__btn--next { inset-inline-end: 3vw; }
.lightbox__bar {
    position: fixed;
    inset-block-start: 3vh;
    inset-inline-end: 3vw;
    display: flex;
    align-items: center;
    gap: 0.75rem;
}
.lightbox__download {
    color: #fff;
    font-size: 0.8rem;
    font-weight: 600;
    text-decoration: none;
    padding: 0.35rem 0.7rem;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.12);
}
.lightbox__download:hover { background: var(--bs-hi-vis); color: var(--bs-ink-2); }
.lightbox__close {
    width: 38px;
    height: 38px;
    border-radius: 50%;
    border: none;
    cursor: pointer;
    background: rgba(255, 255, 255, 0.12);
    color: #fff;
    font-size: 1.1rem;
    line-height: 1;
}
.lightbox__close:hover { background: var(--bs-hi-vis); color: var(--bs-ink-2); }
.lightbox__close:focus-visible,
.lightbox__download:focus-visible { outline: 2px solid var(--bs-hi-vis); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
    .photo-tile, .photo-tile img { transition: none; }
    .lightbox::backdrop { backdrop-filter: none; }
    /* Zoom still works; it just arrives rather than travels. */
    .lightbox__img { transition: none; }
    .photo-picker__thumbs { scroll-behavior: auto; }
}

/* ===================================================================   AskRS — floating staff assistant (RSPH-96)
   A draggable launcher + chat popover on every screen. Uses the app's own
   tokens so it inherits light/dark theming; the hi-vis accent is the signature.
   ========================================================================== */
.askrs {
    position: fixed;
    inset-block-end: 1rem;
    inset-inline-end: 1rem;
    z-index: 1075; /* below toasts (1080) so transient notices still surface */
    touch-action: none; /* let the drag handler own pointer gestures */
}

.askrs__launcher {
    position: relative;
    width: 56px;
    height: 52px;
    /* Rounded-rectangle bubble: its flat bottom edge lets the tail attach flush with no gap. */
    border-radius: 18px;
    border: none;
    background: var(--bs-hi-vis);
    color: var(--bs-ink);
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28);
    cursor: grab;
    display: grid;
    place-items: center;
    padding: 0;
}
/* The bubble's tail: a downward triangle whose flat top edge sits on the bubble's flat bottom edge,
   so they join seamlessly (same colour). Placed left of centre to read as a chat bubble. */
.askrs__launcher::after {
    content: "";
    position: absolute;
    inset-inline-start: 19px;
    inset-block-end: -7px;
    width: 0;
    height: 0;
    border-inline-start: 8px solid transparent;
    border-inline-end: 8px solid transparent;
    border-block-start: 10px solid var(--bs-hi-vis);
}
.askrs__launcher:active { cursor: grabbing; }
.askrs__wordmark {
    font-family: var(--bs-font-display);
    font-size: 1.6rem;
    line-height: 1;
    letter-spacing: 0.02em;
}
/* Solid white R with a slim dark border on the glyph itself. paint-order keeps the
   white fill on top so the stroke reads as a clean outside edge, not a thinned letter. */
.askrs__wordmark-initial {
    color: #fff;
    -webkit-text-stroke: 1px var(--bs-ink);
    text-stroke: 1px var(--bs-ink);
    paint-order: stroke fill;
}
/* The S is the inverse of the R: dark fill with a slim white border. */
.askrs__wordmark-final {
    color: var(--bs-ink);
    -webkit-text-stroke: 1px #fff;
    text-stroke: 1px #fff;
    paint-order: stroke fill;
}

.askrs__panel {
    position: absolute;
    inset-block-end: calc(100% + 12px);
    inset-inline-end: 0;
    width: min(360px, calc(100vw - 2rem));
    max-height: 70vh;
    display: flex;
    flex-direction: column;
    background: var(--bs-paper);
    border: 1px solid var(--bs-line);
    border-radius: 14px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.30);
    overflow: hidden;
}

.askrs__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.65rem 0.9rem;
    border-block-end: 2px solid var(--bs-hi-vis);
    cursor: grab;
}
.askrs__header:active { cursor: grabbing; }
.askrs__title {
    /* UI font, not the display face: Bebas Neue is uppercase-only and would render
       the "AskRS" brand as "ASKRS". Archivo has lowercase glyphs, keeping the case. */
    font-family: var(--bs-font-ui);
    font-size: 1.2rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--bs-text);
}
.askrs__close {
    border: none;
    background: none;
    font-size: 1.4rem;
    line-height: 1;
    color: var(--bs-muted);
    cursor: pointer;
    padding: 0 0.25rem;
}
.askrs__close:hover { color: var(--bs-text); }

.askrs__log {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: 0.9rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.askrs__msg {
    margin: 0;
    padding: 0.5rem 0.7rem;
    border-radius: 12px;
    max-width: 85%;
    font-size: 0.9rem;
    line-height: 1.4;
    white-space: pre-wrap;
    word-wrap: break-word;
}
.askrs__msg--user {
    align-self: flex-end;
    background: var(--bs-hi-vis);
    color: var(--bs-ink);
    border-end-end-radius: 4px;
}
.askrs__msg--bot {
    align-self: flex-start;
    background: color-mix(in srgb, var(--bs-text) 8%, transparent);
    color: var(--bs-text);
    border-end-start-radius: 4px;
}
.askrs__intro { color: var(--bs-muted); background: none; padding-inline: 0; }
.askrs__msg--pending { color: var(--bs-muted); font-style: italic; }
/* The direct link to a matched help topic (RSPH — askRS "how do I" links). A quiet rounded button
   rather than another bubble, so it reads as an action on the answer above it, not a third
   participant in the chat. Wraps rather than truncates: a topic title plus the "Open help article"
   prefix can run past this narrow (360px) panel's width, and single-line overflow: hidden/ellipsis
   collapsed the whole label to nothing in Firefox — the flex item's shrink-to-fit width came out far
   narrower than the content there, unlike Chromium where the same rule happened to render fine. */
.askrs__link {
    display: inline-flex;
    align-self: flex-start;
    max-width: 85%;
    padding: 0.35rem 0.65rem;
    border-radius: 10px;
    border: 1px solid var(--bs-hi-vis);
    color: var(--bs-hi-vis);
    background: none;
    font-size: 0.82rem;
    font-weight: 600;
    line-height: 1.35;
    text-decoration: none;
    white-space: normal;
    word-wrap: break-word;
}
.askrs__link:hover { background: var(--bs-hi-vis); color: var(--bs-ink); }
.askrs__link:focus-visible { outline: 2px solid var(--hover-ring); outline-offset: 2px; }

.askrs__form {
    display: flex;
    gap: 0.5rem;
    padding: 0.7rem;
    border-block-start: 1px solid var(--bs-line);
}
.askrs__input {
    flex: 1 1 auto;
    min-width: 0;
    padding: 0.5rem 0.7rem;
    border: 1px solid var(--bs-line);
    border-radius: 9px;
    background: var(--bs-paper);
    color: var(--bs-text);
    font: inherit;
}
.askrs__input:focus-visible {
    outline: 2px solid var(--bs-hi-vis);
    outline-offset: 1px;
}
.askrs__input:disabled { opacity: 0.6; }
.askrs__send {
    flex: 0 0 auto;
    border: none;
    border-radius: 9px;
    padding: 0.5rem 0.9rem;
    background: var(--bs-hi-vis);
    color: var(--bs-ink);
    font-family: var(--bs-font-display);
    font-size: 1rem;
    letter-spacing: 0.03em;
    cursor: pointer;
}
.askrs__send:hover { background: var(--bs-hi-vis-press); }
.askrs__send:disabled { opacity: 0.6; cursor: default; }

.askrs__launcher:focus-visible,
.askrs__close:focus-visible,
.askrs__send:focus-visible {
    outline: 2px solid var(--bs-hi-vis);
    outline-offset: 2px;
}

.askrs__sronly {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0);
    white-space: nowrap; border: 0;
}

@media (prefers-reduced-motion: no-preference) {
    .askrs__launcher { transition: transform 0.12s ease, box-shadow 0.12s ease; }
    .askrs__launcher:hover { transform: translateY(-2px); box-shadow: 0 9px 22px rgba(0, 0, 0, 0.34); }
}

/* ===================================================================   Hover affordance
   Every "this is clickable" highlight in the app, in one place.

   This block is deliberately LAST in the file. The dark surface block above resets border-color on
   .board-tile / .vehicle-card / .queue-card / .clock-card / .radio-list__item at equal specificity,
   so any hover rule declared before it is silently dead in dark mode. Source order settles it here,
   which is why none of these rules need a [data-bs-theme="dark"] twin.

   Colour comes from --hover-ring, which resolves to the hi-vis green in light and the brand cyan in
   dark, so each rule is written once. Keep new hover highlights in this block.
   ========================================================================== */

/* Cards and tiles that are themselves the link or button. */
.board-tile:hover,
.board-tile:focus-visible,
.vehicle-card:hover,
.vehicle-card:focus-visible,
.kpi-mix__cell:hover,
.kpi-mix__cell:focus-visible,
a.kpi-stat--link:hover,
.photo-tile:hover {
    border-color: var(--hover-ring);
    box-shadow: var(--hover-lift);
}

/* Shared entity-link hover/focus reveal: the text takes the accent and its arrow slides in,
   signalling the link opens something (a leader's job list, a notification's target) (base styling
   sits in .entity-link above). */
.entity-link:hover { color: var(--hover-ring); }
.entity-link:hover::after,
.entity-link:focus-visible::after { opacity: 1; transform: translateX(0); }

/* Deadline urgency (see the .vehicle-card--due-amber / --due-red rules) documents that its border
   colour "holds over the hover border colour" — that invariant predates this block. Being last in
   the file now lets the plain .vehicle-card:hover above win the equal-specificity tie against the
   due rules, so restore the due treatment explicitly here for :hover and :focus-visible.
   Only border-color needs restoring: the ring now lives on ::after, which no hover rule touches, so
   a hovered due card keeps its ring AND gains the standard --hover-lift shadow (the old inset-ring
   override used to cancel that lift, leaving due cards the only ones that didn't rise on hover). */
.vehicle-card.vehicle-card--due-amber:hover,
.vehicle-card.vehicle-card--due-amber:focus-visible {
    border-color: var(--due-amber);
}
.vehicle-card.vehicle-card--due-red:hover,
.vehicle-card.vehicle-card--due-red:focus-visible {
    border-color: var(--due-red);
}

/* Option pickers. .cover-option:has(input:checked) is (0,2,1) — :has() takes the specificity of
   its argument — which beats .cover-option:hover at (0,2,0), so hovering a selected cover option
   produces no visible change: the selected state's higher specificity wins outright, no
   source-order tie-break needed.
   Same shape as the pressed-state carve-outs further down (button.pill / .tech-start__job): an
   explicit selection outranks the generic hover ring rather than being repainted by it. */
.radio-list__item:hover,
.cover-option:hover,
a.bk-chip:hover,
a.bk-chip:focus-visible {
    border-color: var(--hover-ring);
}

.radio-list__item:hover {
    background: var(--hover-tint);
}

/* Container cards: the card is not the click target, but it holds one. :has() keeps the ring off
   cards whose controls were permission-gated away, so the highlight never promises a control that
   is not there. */
.queue-card:has(a, button):hover,
.clock-card:has(a, button):hover,
.tech-card:has(a, button):hover {
    border-color: var(--hover-ring);
}

/* The accent rail carries state (on the clock / idle / neutral), so hover must not repaint it.
   border-color above sets all four sides; this puts the neutral left edge back. Scoped to
   .queue-card / .clock-card only — .tech-card has a uniform 1px border and would lose its left
   edge. Declared before the --active / --idle carve-outs below so those still win the
   equal-specificity tie. */
.queue-card:has(a, button):hover,
.clock-card:has(a, button):hover {
    border-left-color: var(--bs-line);
}

/* The accent rail on these variants carries state (on the clock / idle), so hover must not
   repaint it. border-color above sets all four sides; these put the left edge back. */
.queue-card--active:has(a, button):hover,
.clock-card--active:has(a, button):hover {
    border-left-color: var(--bs-hi-vis);
}

.clock-card--idle:has(a, button):hover {
    border-left-color: var(--bs-danger);
}

/* Bordered controls. */
.tech-start__job:hover,
.btn--ghost:hover,
.mq-rail__arrow:hover:not(:disabled),
.wf-calendar-nav a:hover,
.pager__btn:hover:not(:disabled),
.data-table__thumb a:hover,
.vehicle-card__move:hover {
    border-color: var(--hover-ring);
}

/* Board move keyboard access (RSPH): the reveal (opacity/pointer-events) stays next to the base
   rule near line 589 — only the ring itself lives here, per this block's placement rule. */
.vehicle-card__move:focus-visible {
    outline: 2px solid var(--hover-ring);
    outline-offset: 2px;
    border-color: var(--hover-ring);
}

/* An explicit selection must not be repainted by hover: .tech-start__job:hover ties
   .tech-start__job[aria-pressed="true"] (0,2,0 each, settled by source order) and would otherwise
   take its pressed look away with the generic hover ring. The carve-out reasserts the pressed
   state's own declarations exactly. If you add another pressed control here and it has a dark-theme
   twin, its carve-out needs one too — a theme-less carve-out alone ties that twin's specificity and,
   being last in the file, wrongly wins in dark mode. */
.tech-start__job[aria-pressed="true"]:hover {
    border-color: var(--bs-hi-vis);
    box-shadow: inset 0 0 0 1px var(--bs-hi-vis);
}

.workshop-tv .tv-btn:hover {
    border-color: var(--hover-ring);
    background: var(--bs-steel-2);
}

/* Drag-drop targets. Their border carries RAG status, so the highlight is an outer ring and the
   border is left alone. The attribute selectors self-gate: the drag bindings are only rendered
   when booking.manage / workshop.schedule_manage passes, so an unprivileged user gets no ring
   without any permission logic being duplicated here. */
.bk-day[data-booking-move-target]:hover:not(.is-drop),
td[data-allocation-move-target]:hover {
    box-shadow: 0 0 0 2px var(--hover-ring);
}

/* ------------------------------------------------------------------
   KPI section (RSPH-126). Tabs, scope strip and tile grid for the
   /kpi-reports page. Reuses the design tokens and the board-tile idiom;
   because every colour is a token that already flips under
   [data-bs-theme="dark"], dark mode needs almost no overrides here.
   ------------------------------------------------------------------ */
.kpi-tabs {
    display: flex;
    gap: 0.15rem;
    margin-top: 1.4rem;
    border-bottom: 1px solid var(--bs-line);
    overflow-x: auto;
    scrollbar-width: thin;
}
.kpi-tab {
    font-weight: 700;
    font-size: 0.9rem;
    white-space: nowrap;
    text-decoration: none;
    border-bottom: 3px solid transparent;
    padding: 0.7rem 1.05rem;
    color: var(--bs-muted);
    transition: color 0.12s ease, border-color 0.12s ease;
}
.kpi-tab:hover { color: var(--bs-text); }
.kpi-tab:focus-visible { outline: 2px solid var(--hover-ring); outline-offset: -2px; border-radius: 4px 4px 0 0; }
.kpi-tab[aria-selected="true"] {
    color: var(--bs-ink);
    border-bottom-color: var(--bs-hi-vis);
}
/* --bs-ink is near-black: without this the SELECTED tab's label disappears against the dark
   surface, leaving a strip with a gap and an underline under nothing. */
[data-bs-theme="dark"] .kpi-tab[aria-selected="true"] { color: var(--heading); }

.kpi-scope {
    margin: 0.75rem 0 0;
    font-family: var(--bs-font-mono);
    font-size: 0.68rem; letter-spacing: 0.04em;
    color: var(--bs-muted);
    display: flex; align-items: center; gap: 0.5rem;
}
.kpi-scope::before {
    content: ''; width: 6px; height: 6px; border-radius: 50%;
    background: #02b3fe; flex: none;
}

.kpi-empty {
    margin-top: 1.2rem;
    color: var(--bs-muted);
}

.kpi-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
    gap: 0.9rem;
    margin-top: 1.1rem;
}
.kpi-tile {
    /* 8.25rem, not 9.5: the taller box was sized around a sparkline that was never built, and
       left a dead band of empty paper between the figure and the footer on every tile. */
    display: flex; flex-direction: column; min-height: 8.25rem;
    text-decoration: none;
    color: inherit;
    background: var(--bs-paper);
    border: 1px solid var(--bs-line);
    border-radius: 12px;
    padding: 0.9rem 1rem 0.85rem;
    transition: border-color 0.12s ease, box-shadow 0.12s ease, transform 0.12s ease;
}
.kpi-tile:hover { border-color: var(--hover-ring); transform: translateY(-1px); box-shadow: 0 6px 16px -12px rgba(16,24,40,0.35); }
.kpi-tile:focus-visible { outline: 2px solid var(--hover-ring); outline-offset: 2px; }
.kpi-tile__label {
    font-size: 0.78rem; font-weight: 700; color: var(--bs-text);
    line-height: 1.25; min-height: 2.5em;
}
.kpi-tile__val { display: flex; align-items: baseline; gap: 0.35rem; margin-top: 0.35rem; }
.kpi-tile__n {
    font-family: var(--bs-font-display);
    font-size: 2.5rem; line-height: 0.9; letter-spacing: 0.02em;
    color: var(--bs-ink);
}
.kpi-tile__unit {
    font-family: var(--bs-font-mono); font-size: 0.66rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--bs-muted);
}
/* "Not measured" renders as an em dash. At the display face's 2.5rem that glyph is a thick slab
   that reads as a redaction bar rather than an absence, so unmeasured figures drop to the muted
   colour at a smaller size — present, but plainly not a number. */
.kpi-tile__n--none, .kpi-tile__part-n--none, .kpi-detail__n--none {
    color: var(--bs-muted);
    font-size: 1.5rem;
    letter-spacing: 0.1em;
}
.kpi-tile__part-n--none { font-size: 1.1rem; }
/* Split tile: three bands side by side instead of one headline figure. The figures are
   deliberately smaller than .kpi-tile__n — three of them at full size would out-shout every
   single-figure tile in the grid, and none of them is the headline. */
.kpi-tile__split {
    display: grid; grid-template-columns: repeat(3, 1fr);
    gap: 0.5rem; margin-top: 0.4rem;
}
.kpi-tile__part { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.kpi-tile__part-n {
    font-family: var(--bs-font-display);
    font-size: 1.5rem; line-height: 0.95; letter-spacing: 0.02em;
    color: var(--bs-ink);
}
.kpi-tile__part-label {
    font-family: var(--bs-font-mono); font-size: 0.58rem; letter-spacing: 0.07em;
    text-transform: uppercase; color: var(--bs-muted);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The split tile's unit sits under all three bands, so it needs its own line and a little air —
   inline it collided with the band labels and wrapped mid-phrase. */
.kpi-tile__unit--split {
    display: block; margin-top: 0.55rem;
    padding-top: 0.4rem; border-top: 1px solid var(--bs-line);
}
/* The one entered tile. Its input carries the display face at a readable size so the tile still
   reads as a figure rather than as a form dropped into the grid. */
.kpi-tile--edit { cursor: default; }
.kpi-tile--edit:hover { border-color: var(--bs-line); transform: none; box-shadow: none; }
.kpi-target { display: flex; align-items: center; gap: 0.4rem; margin-top: 0.4rem; }
.kpi-target__input {
    flex: 1 1 auto; min-width: 0;
    font-family: var(--bs-font-display); font-size: 1.4rem;
    padding: 0.2rem 0.4rem;
    color: var(--bs-ink);
    background: var(--bs-body-bg);
    border: 1px solid var(--bs-line); border-radius: 8px;
}
.kpi-target__save { flex: 0 0 auto; padding: 0.3rem 0.7rem; font-size: 0.78rem; }
.kpi-tile__note {
    margin: 0.4rem 0 0; font-size: 0.7rem; line-height: 1.35; color: var(--bs-muted);
}
.kpi-tile__link { font-size: 0.72rem; font-weight: 600; }
.kpi-tile__foot { margin-top: auto; padding-top: 0.65rem; }
.kpi-delta {
    font-family: var(--bs-font-mono); font-size: 0.7rem; font-weight: 700;
    display: inline-flex; align-items: baseline; gap: 0.25rem;
}
.kpi-delta small { font-weight: 400; color: var(--bs-muted); letter-spacing: 0.04em; text-transform: uppercase; font-size: 0.58rem; }
.kpi-delta--good { color: var(--rag-green); }
.kpi-delta--bad  { color: var(--rag-red); }
.kpi-delta--flat { color: var(--bs-muted); }

/* In dark mode the big figure uses the shared display-heading colour, matching
   the board tiles rather than staying near-black. The revenue target belongs in
   the same list: it is the one figure here that is typed rather than measured,
   and at --bs-ink on --surface it read as disabled placeholder text. */
[data-bs-theme="dark"] .kpi-tile__n,
[data-bs-theme="dark"] .kpi-tile__part-n,
[data-bs-theme="dark"] .kpi-target__input { color: var(--heading); }

/* KPI drill-down page (RSPH-126). Sized and coloured like the link it is, not like a caption: at
   0.68rem in --bs-muted it read as a label someone had left above the heading, and the way back off
   a drill-down page is the one control on it people actually need to find. Keeps the mono uppercase
   voice — it is still a workshop micro-label — but at link size, weight and colour. */
.crumb {
    /* Block so the inline .eyebrow that follows it on the index page starts a new line — at the old
       size the two read as one row of small caps, at this size they collide. `fit-content` keeps the
       clickable box around the words rather than stretching it across the column. */
    display: block;
    width: fit-content;
    margin-bottom: 0.6rem;
    font-family: var(--bs-font-mono);
    font-size: 0.95rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
    color: var(--bs-primary, #0d6efd); text-decoration: none;
}
.crumb:hover { color: var(--bs-ink); text-decoration: underline; }
/* The link blue is mixed for white paper and goes murky on the dark surface; --hero-accent is the
   blue this system already uses when one has to read against near-black. */
[data-bs-theme="dark"] .crumb { color: var(--hero-accent); }
/* --bs-ink is near-black, so the hover would vanish against the dark surface. */
[data-bs-theme="dark"] .crumb:hover { color: var(--heading); }
.kpi-detail__head {
    display: flex; align-items: flex-end; justify-content: space-between;
    gap: 1.2rem; flex-wrap: wrap;
    border-bottom: 3px solid var(--bs-ink);
    padding-bottom: 0.9rem;
}
.kpi-detail__figure { text-align: right; }
/* A split KPI's header carries three bands, not one number. Right-aligned and unconstrained they
   crushed into the corner and wrapped the unit mid-phrase, so give them room and their own row. */
.kpi-detail__figure .kpi-tile__split {
    grid-template-columns: repeat(3, minmax(4.5rem, auto));
    justify-content: end; text-align: left; gap: 1.1rem;
}
.kpi-detail__n {
    font-family: var(--bs-font-display); font-size: 3rem; line-height: 1;
    color: var(--bs-ink); display: block;
}
[data-bs-theme="dark"] .kpi-detail__n { color: var(--heading); }
.kpi-detail__unit {
    font-family: var(--bs-font-mono); font-size: 0.7rem; letter-spacing: 0.1em;
    text-transform: uppercase; color: var(--bs-muted);
}
/* The chart's own current value, sat under its heading — for the KPI whose chart plots a different
   unit from the headline (booth throughput). Half the headline's size on purpose: it answers the
   panel, not the page, and matching .kpi-detail__n would set up two figures competing to be read
   first. Same mono uppercase unit, so the two are visibly the same kind of statement. */
.kpi-chart-figure {
    display: flex; align-items: baseline; gap: 0.5rem;
    margin: -0.15rem 0 0.6rem;
}
.kpi-chart-figure__n {
    font-family: var(--bs-font-display); font-size: 1.6rem; line-height: 1; color: var(--bs-ink);
}
[data-bs-theme="dark"] .kpi-chart-figure__n { color: var(--heading); }
/* Unmeasured reads as the same muted em dash the headline uses, at this figure's size. */
.kpi-chart-figure__n--none { color: var(--bs-muted); font-size: 1.2rem; letter-spacing: 0.1em; }
.kpi-chart-figure__unit {
    font-family: var(--bs-font-mono); font-size: 0.66rem; letter-spacing: 0.1em;
    text-transform: uppercase; color: var(--bs-muted);
}
/* A technician's own figure (RSPH-187) — additional to the scope line above it and the team
   trend below, so it gets its own bordered strip rather than blending into either. Reuses
   .kpi-chart-figure__n/__unit for the number itself so it reads as the same kind of statement as
   every other figure on the page. */
.kpi-own-figure {
    display: flex; align-items: baseline; gap: 0.5rem;
    margin: 0.9rem 0 0;
    padding: 0.55rem 0.9rem;
    background: var(--bs-paper);
    border: 1px solid var(--bs-line);
    border-radius: 10px;
}
.kpi-own-figure__label {
    font-family: var(--bs-font-mono); font-size: 0.66rem; letter-spacing: 0.08em;
    text-transform: uppercase; color: var(--bs-muted);
}
.kpi-detail__panels {
    display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    gap: 1rem; margin-top: 1.1rem;
}
@media (max-width: 860px) { .kpi-detail__panels { grid-template-columns: 1fr; } }
/* .panel carries no padding of its own — it is built so a full-bleed table can reach its rounded
   edges (see the note by .panel__heading). These panels hold a heading, a chart and prose rather
   than a table, so they have to supply the padding themselves; without it every one of them sat
   flush against its own border. */
.kpi-detail__panels > .panel { padding: 1rem 1.15rem 1.2rem; }
/* The by-technician panel (RSPH-173), full width beneath the two-column grid above. It is NOT a
   third cell in that grid: the grid's second column is a third as wide, and a ranking squeezed into
   it loses the names the panel exists to show. Padding comes from .panel__heading / .panel__body
   because .panel carries none of its own — the table inside reaches the rounded edges. */
.kpi-breakdown { margin-top: 1rem; }
/* The note carries what the rows do and do not mean, so it is set to read rather than to be skipped:
   full measure, muted but not faint, above the chart instead of buried under the table. */
.kpi-breakdown__note { max-width: 68ch; margin: 0 0 0.9rem; color: var(--bs-muted); font-size: 0.85rem; }
/* The residual sits below the table, where a reader arrives after totting the rows up and wants to
   know what is missing from them. Smaller than the note: it is a footnote to the numbers, not a
   statement about what they mean. */
.kpi-breakdown__residual { margin: 0; color: var(--bs-muted); font-size: 0.8rem; }
/* The panel's own ranking box: min-height would fight the inline height the template computes from
   the row count, which is the whole mechanism keeping an Hbar from growing every frame. */
.kpi-breakdown .kpi-chart { min-height: 0; }
.kpi-chart { position: relative; min-height: 20rem; }
/* Ranking charts drop maintainAspectRatio and take their height from the inline style the
   template computes, so the canvas must fill the box rather than size itself. */
.kpi-chart--ranking > canvas { width: 100% !important; height: 100% !important; }
/* The same contract for the Reports module's charts (templates/reports/show.html.twig), which
   renders every report's charts through one wrapper. `--pinned` is set exactly when the spec says
   ChartSpec::sizesFromContainer(), and carries the inline height that stops the box and the canvas
   sizing each other; unpinned charts keep their own aspect ratio and must NOT be stretched. */
.chart-box { position: relative; }
.chart-box--pinned > canvas { width: 100% !important; height: 100% !important; }
/* `pre-line` rather than the default: a formula with a second line (booth throughput states its
   drill-down separately) keeps that break, while long single-line formulas still wrap to the box.
   `pre` would give them a horizontal scrollbar instead. */
.kpi-formula, .report-formula {
    display: block; margin-top: 0.7rem;
    font-family: var(--bs-font-mono); font-size: 0.74rem;
    background: var(--bs-paper); border: 1px solid var(--bs-line); border-radius: 8px;
    padding: 0.5rem 0.65rem; color: var(--bs-text);
    white-space: pre-line;
}

/* Site benchmark (RSPH-143): the KPI section read one branch against another. A wide table — one
   column per site — inside the standard .table-scroll, so a group with a dozen branches scrolls the
   table rather than the page. */
.kpi-scope__action {
    margin-inline-start: auto;
    font-family: var(--bs-font-body);
    font-size: 0.78rem; letter-spacing: 0;
    font-weight: 600;
    text-decoration: none;
    color: var(--bs-link, #0b6bcb);
}
.kpi-scope__action:hover { text-decoration: underline; }

.benchmark-focus {
    display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
    margin: 1.1rem 0 0.9rem;
}
.benchmark-focus label {
    font-family: var(--bs-font-mono);
    font-size: 0.68rem; letter-spacing: 0.04em; text-transform: uppercase;
    color: var(--bs-muted);
}
.benchmark-focus select { max-width: 18rem; }

/* The KPI name column stays put while the sites scroll past it — without it you lose track of which
   row you are reading the moment there are more branches than fit. */
.benchmark-table__kpi {
    position: sticky; left: 0; z-index: 1;
    background: var(--bs-paper);
    text-align: start; font-weight: 700;
    min-width: 13rem;
}
.benchmark-table__kpi a { color: inherit; text-decoration: none; }
.benchmark-table__kpi a:hover { text-decoration: underline; }
.benchmark-table__unit {
    display: block; margin-top: 0.15rem;
    font-family: var(--bs-font-mono);
    font-size: 0.62rem; letter-spacing: 0.04em;
    color: var(--bs-muted); font-weight: 400;
}
/* The cells carry a visually-hidden "Best" label, and Bootstrap's .visually-hidden is
   position:absolute. With no positioned ancestor its containing block is the page, not the scroll
   container — so it is NOT clipped by the container's overflow, and sits at its static position out
   at the far edge of a wide table, dragging the whole page into horizontal scroll on a phone.
   Making the table a containing block puts it back inside the box that is there to absorb it.
   (Found in the browser at 420px: the page scrolled to 667px with a 420px viewport.) */
.benchmark-table { position: relative; }
.benchmark-table__col, .benchmark-table__cell { text-align: end; white-space: nowrap; }
.benchmark-table__cell { min-width: 7.5rem; }
.benchmark-table__n {
    display: block;
    font-family: var(--bs-font-display);
    font-size: 1.15rem; line-height: 1.1; color: var(--bs-ink);
}
[data-bs-theme="dark"] .benchmark-table__n { color: var(--heading); }
.benchmark-table__n--none { color: var(--bs-muted); font-size: 1rem; }
.benchmark-table__cell .kpi-delta { justify-content: flex-end; margin-top: 0.15rem; font-size: 0.68rem; }

/* The winning branch for a row. A tint plus the figure in green — NOT box-shadow, which Chrome does
   not paint on a cell of a border-collapse table (the first attempt was simply invisible in the
   browser, on the one signal this whole page exists to give). Declared after the column tints so a
   winner inside the focused column still reads as the winner. Colour is never the only signal: the
   cell also carries a visually-hidden "best". */
.benchmark-table__cell--best { background: rgba(46, 199, 126, 0.12); }
.benchmark-table__cell--best .benchmark-table__n { color: var(--rag-green); font-weight: 700; }

/* The focused branch and the aggregate it is read against, tinted so the eye pairs them. */
.benchmark-table__col--focus { background: rgba(2, 179, 254, 0.06); }
.benchmark-table__col--rest { background: rgba(16, 24, 40, 0.035); }
[data-bs-theme="dark"] .benchmark-table__col--rest { background: rgba(255, 255, 255, 0.045); }

/* The table's key. A <dl> because that is what it is — three terms and what each one means — and
   the swatches carry aria-hidden so a screen reader is read the words alone; the winning cell
   already announces itself through its visually-hidden "Best" label, so the colour is never the
   only carrier. Wraps to as many lines as it needs on a narrow screen. */
.benchmark-key {
    display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 1.1rem;
    margin: 0.8rem 0 0;
    font-size: 0.75rem; color: var(--bs-muted);
}
.benchmark-key__term {
    font-family: var(--bs-font-mono); font-size: 0.62rem; letter-spacing: 0.1em;
    text-transform: uppercase; color: var(--bs-muted);
}
.benchmark-key__item { display: inline-flex; align-items: center; gap: 0.4rem; margin: 0; }
.benchmark-key__swatch {
    width: 0.85rem; height: 0.85rem; border-radius: 4px; flex: 0 0 auto;
    border: 1px solid var(--bs-line);
}
/* The same tint the winning cell wears, so the key is a sample of the table rather than an
   approximation of it. */
.benchmark-key__swatch--best { background: rgba(46, 199, 126, 0.12); border-color: var(--rag-green); }
/* Both arrows together: the colour is the message here, not the direction. */
.benchmark-key .kpi-delta { font-size: 0.62rem; letter-spacing: 0.05em; }

.benchmark-note {
    margin: 0.75rem 0 0;
    font-size: 0.78rem; color: var(--bs-muted);
}

.benchmark-charts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
    gap: 0.9rem;
    margin-top: 1.4rem;
}
.benchmark-charts > .panel { padding: 1rem 1.15rem 1.2rem; }
/* These plot one bar per site, not twenty workflow stages, so they take the height the template
   computes instead of the drill-down page's 20rem floor. */
.benchmark-charts .kpi-chart { min-height: 0; }

/* Notification settings matrix (NotificationSettingsController): a data-table whose two right-hand
   columns are channel checkboxes, grouped into domain sections by <tbody>. The StatusChanged row is
   followed by a full-width <details> row holding the per-status opt-out list. Brand accent on the
   boxes ties it to the rest of the app; the bulk "All" buttons are revealed only when JS is present. */
.notif-matrix__col { width: 6.5rem; text-align: center; }
.notif-matrix thead th.notif-matrix__col { text-align: center; }
.notif-matrix tbody .notif-matrix__group th,
.notif-matrix tbody .notif-matrix__group td {
    background: var(--bs-paper);
    border-bottom: 1px solid var(--bs-line);
    padding-top: 0.6rem; padding-bottom: 0.6rem;
}
.notif-matrix__group th {
    font-family: var(--bs-font-ui); font-size: 0.72rem; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.06em; color: var(--bs-muted, #6b7280);
}
.notif-matrix__group td { text-align: center; }
.notif-matrix tbody tr:hover .notif-matrix__group { background: var(--bs-paper); }
.notif-matrix__name { font-family: var(--bs-font-ui); text-transform: none; letter-spacing: 0; font-size: 0.92rem; color: var(--bs-text); }
.notif-matrix__box { width: 1.15rem; height: 1.15rem; accent-color: var(--bs-hi-vis); cursor: pointer; }
.notif-matrix__na { color: var(--bs-muted, #9aa1ab); }
.notif-matrix__bulk {
    border: 1px solid var(--bs-line); background: transparent; border-radius: 6px;
    padding: 0.1rem 0.5rem; font-size: 0.72rem; font-family: var(--bs-font-ui);
    color: var(--bs-text); cursor: pointer;
}
.notif-matrix__bulk:hover { border-color: var(--bs-hi-vis); color: var(--bs-hi-vis); }
.notif-matrix__detail-row td { background: var(--bs-paper); }
.notif-matrix tbody tr.notif-matrix__detail-row:hover td { background: var(--bs-paper); }
.notif-matrix__detail summary { cursor: pointer; font-family: var(--bs-font-ui); font-size: 0.9rem; color: var(--bs-text); }
.notif-matrix__detail summary:focus-visible { outline: 2px solid var(--bs-hi-vis); outline-offset: 2px; }
.notif-matrix__statuses {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
    gap: 0.35rem 1rem; margin-top: 0.6rem;
}
.notif-matrix__statuses input[type="checkbox"] { accent-color: var(--bs-hi-vis); }

/* Duplicate warning in dark mode (RSPH-156). A flat red panel would glare against a dark form
   card, so this leans on translucent red overlays instead of the light rules' fixed inks — same
   vivid red hue as the light-mode rules above, just carried as rgba() so it sits on --bs-panel
   rather than fighting it. --possible stays a lighter tint of the same red for the same reason as
   light mode: a same-name-only match should read as less urgent than a stronger match. */
[data-bs-theme="dark"] .duplicate-warning {
    background: rgba(239, 68, 68, 0.16);
    border-color: rgba(239, 68, 68, 0.65);
    color: #fca5a5;
}
[data-bs-theme="dark"] .duplicate-warning--possible {
    background: rgba(239, 68, 68, 0.08);
    border-color: rgba(239, 68, 68, 0.35);
    color: #f0a8a8;
}
[data-bs-theme="dark"] .duplicate-warning__item { background: rgba(0, 0, 0, 0.22); }
[data-bs-theme="dark"] .duplicate-warning--possible .duplicate-warning__item { background: rgba(0, 0, 0, 0.22); }
[data-bs-theme="dark"] .duplicate-warning__name { color: var(--bs-text); }
[data-bs-theme="dark"] .duplicate-warning__detail { color: var(--bs-muted); }
[data-bs-theme="dark"] .duplicate-warning__reason { background: #ef4444; color: #fff; }
[data-bs-theme="dark"] .duplicate-warning--possible .duplicate-warning__reason { background: rgba(239, 68, 68, 0.35); color: #fca5a5; }
/* =====================================================================
   Completion workbench: stage rail (RSPH-166)

   The six close-down stages as a real ordered process (clean → check → bill → settle → excess →
   release), so the numbered marker is earned rather than decorative. Four states share the same
   marker/body shape but are never visually interchangeable: NotApplicable gets a dashed, hollow
   ring (this step will never happen here) which must not be mistaken for Blocked's solid grey lock
   (this step WILL happen, just not yet) or for Done's filled green check.
   ===================================================================== */
.stage-rail { list-style: none; margin: 0; padding: 0; }
.stage-rail__item { display: flex; gap: 0.9rem; padding-bottom: 1.5rem; position: relative; }
.stage-rail__item:last-child { padding-bottom: 0; }
.stage-rail__marker {
    flex: none;
    width: 2.1rem;
    height: 2.1rem;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1.5px solid var(--bs-line);
    background: var(--bs-paper);
    color: var(--bs-muted);
    font-family: var(--bs-font-mono);
    font-size: 0.82rem;
    font-weight: 700;
    position: relative;
    z-index: 1;
}
.stage-rail__item:not(:last-child) .stage-rail__marker::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    width: 2px;
    height: 1.5rem;
    background: var(--bs-line);
}
.stage-rail__item--done .stage-rail__marker { background: #d7efdf; border-color: #8ecaa1; color: #146c37; }
.stage-rail__item--current .stage-rail__marker {
    background: var(--bs-ink);
    border-color: var(--bs-ink);
    color: #fff;
    box-shadow: 0 0 0 4px rgba(0, 222, 145, 0.18);
}
.stage-rail__item--blocked .stage-rail__marker { background: var(--hover-tint); border-color: var(--bs-line); color: var(--bs-muted); }
.stage-rail__item--not-applicable .stage-rail__marker { background: transparent; border-style: dashed; color: var(--bs-line); }

.stage-rail__body { flex: 1 1 auto; min-width: 0; padding-top: 0.2rem; }
.stage-rail__label { font-weight: 700; color: var(--bs-text); }
.stage-rail__item--blocked .stage-rail__label,
.stage-rail__item--not-applicable .stage-rail__label { color: var(--bs-muted); font-weight: 600; }
.stage-rail__receipt { color: var(--bs-muted); font-size: 0.88rem; margin-top: 0.25rem; }
.stage-rail__blocked-note,
.stage-rail__na-note {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    color: var(--bs-muted);
    font-size: 0.85rem;
    margin-top: 0.25rem;
}
.stage-rail__panel {
    margin-top: 0.65rem;
    padding: 0.9rem 1.1rem;
    background: var(--hover-tint);
    border: 1px solid var(--bs-line);
    border-radius: 10px;
}
.stage-rail__seam { color: var(--bs-muted); font-size: 0.78rem; font-style: italic; margin: 0.65rem 0 0; }
.stage-rail__issues,
.stage-rail__pool,
.stage-rail__reasons { margin: 0.4rem 0 0; padding-inline-start: 1.1rem; }
.stage-rail__issues li { color: var(--due-amber); }
.stage-rail__pool-heading { font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.68rem; color: var(--bs-muted); margin: 0.5rem 0 0; }
.stage-rail__override-note { color: var(--due-amber); font-size: 0.85rem; margin-top: 0.35rem; }
/* The write forms (RSPH-166 Task 14): assign a cleaner, mark cleaned, sign off, bill, take a
   payment. Each is a plain .form-grid/.form-actions form, just nested a little tighter than a
   full-page one since it lives inside a .stage-rail__panel already carrying its own padding. */
.stage-rail__form { margin-top: 0.7rem; }
.stage-rail__form .form-actions { margin-top: 0.8rem; }
/* The undo-payment control sits inline with its receipt line rather than stacking under it. */
.stage-rail__inline-form { display: inline-block; margin-left: 0.6rem; }

/* The release button is the only disabled button on this screen, and it must read as unmistakably
   dead — it is the control someone will keep clicking while a stage is still outstanding.
   Deliberately scoped to the rail: this shipped unqualified as `.btn:disabled`, which ties
   Bootstrap's own `.btn:disabled` on specificity and — app.css being imported after
   bootstrap.min.css — quietly moved EVERY disabled button in the product from Bootstrap's 0.65
   opacity to 0.5. Changing the product-wide disabled treatment is a design-system decision that
   belongs with the button palette, not a side effect of a feature block. `cursor: not-allowed` is
   dropped with it: pointer-events:none suppresses cursor resolution, so it never applied.
   pointer-events:none also stops :hover from firing, so a disabled .btn--primary never has to fight
   its own hover rule for the last word. */
.stage-rail .btn:disabled { opacity: 0.5; pointer-events: none; }

[data-bs-theme="dark"] .stage-rail__item--done .stage-rail__marker {
    background: rgba(0, 222, 145, 0.14);
    color: #3ff5b8;
    border-color: rgba(0, 222, 145, 0.55);
}
[data-bs-theme="dark"] .stage-rail__item--current .stage-rail__marker {
    background: var(--bs-hi-vis);
    border-color: var(--bs-hi-vis);
    color: var(--bs-ink-2);
    box-shadow: 0 0 12px rgba(0, 222, 145, 0.4);
}

/* Attended Hours (RSPH-169). The variance and pending-overtime tones live on SPANS rather than on
   the <td>, because `.data-table td` outranks a bare cell class and would win the colour back —
   the same trap noted above .data-table__actions. */
.attended-hours__figure { white-space: nowrap; }
.attended-hours__figure > .clock-dot { display: inline-block; vertical-align: middle; margin-inline-end: 0.35rem; }
.attended-hours__variance { font-variant-numeric: tabular-nums; }
.attended-hours__variance--short { color: #b3261e; }
.attended-hours__variance--over { color: #146c37; }
.attended-hours__pending { color: var(--bs-muted); font-size: 0.85em; margin-inline-start: 0.35rem; }

/* The variance greens/reds are the literal badge-flag tones, which go muddy on the dark canvas —
   lift both the way the badge-flag dark rules do. */
[data-bs-theme="dark"] .attended-hours__variance--short { color: #f2b8b5; }
[data-bs-theme="dark"] .attended-hours__variance--over { color: #8ecaa1; }
.attended-hours__sessions { list-style: none; margin: 0; padding: 0; }
.attended-hours__sessions > li + li { margin-block-start: 0.25rem; }

/* Clocking History. A tighter dialect of .stage-rail rather than that component: this is up to ten
   dense rows inside a modal, and the rail's 2.1rem numbered markers say "named stages of a process",
   which clock actions are not.

   The marker states DIRECTION — filled for clocked on, hollow for clocked off. That is the one thing
   the list is scanned for (which way the clock moved, and where the gaps between the pairs fall), so
   it earns the only piece of colour here. The filled tone is the literal green already used by
   .clock-dot and .badge-flag--green, so "clock running" reads the same as everywhere else.

   No pulse, deliberately: .clock-dot pulses because something is happening NOW, and a history that
   pulsed would be claiming the same thing about the past. */
.clocking-history { list-style: none; margin: 0; padding: 0; }
.clocking-history__item {
    display: grid;
    grid-template-columns: auto 0.9rem 1fr;
    align-items: baseline;
    column-gap: 0.7rem;
    padding-block: 0.4rem;
    position: relative;
}
.clocking-history__time {
    font-family: var(--bs-font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 0.78rem;
    letter-spacing: 0.02em;
    color: var(--bs-muted);
    white-space: nowrap;
    /* Each row is its own grid, so an `auto` first column is sized by that row's own text: "7 Jul
       15:05" would set a narrower column than "18 Jul 14:53" and drag its marker and label out of
       line with the rows above, bending the rail. In ch units on a monospace face this floor is the
       longest stamp we can emit ("28 Jul 14:53" — 12 characters) plus slack, so it is derived from
       the content rather than guessed at. */
    min-width: 13ch;
    text-align: end;
}
.clocking-history__marker {
    /* Sits on the text baseline of its row, then the rail below joins it to the next one. */
    align-self: center;
    justify-self: center;
    width: 0.55rem;
    height: 0.55rem;
    border-radius: 50%;
    border: 1.5px solid var(--bs-line);
    background: var(--bs-paper);
    position: relative;
    z-index: 1;
}
.clocking-history__item--on .clocking-history__marker { background: #1a7f3c; border-color: #1a7f3c; }
.clocking-history__item:not(:last-child) .clocking-history__marker::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    width: 1.5px;
    /* Spans the row's own bottom padding plus the next row's top padding, so the rail is continuous
       however tall a wrapped row gets. No negative z-index: the marker's own `z-index: 1` makes it a
       stacking context, so a child behind it is painted behind its background and disappears — and
       the segment sits below the marker (top: 100%) anyway, so there is nothing to hide from. */
    height: calc(0.8rem + 100%);
    background: var(--bs-line);
}
.clocking-history__what { min-width: 0; }
/* A badge on a wrapped row should not sit hard against the text above it. */
.clocking-history__what > .badge-flag { margin-inline-start: 0.35rem; }

[data-bs-theme="dark"] .clocking-history__marker { background: var(--surface-3); }
/* The badge-flag green goes muddy on the dark canvas — lift it the same way the badge-flag and
   attended-hours dark rules do. */
[data-bs-theme="dark"] .clocking-history__item--on .clocking-history__marker {
    background: #8ecaa1;
    border-color: #8ecaa1;
}

/* ---- RSPH-172 Estimate Status board ---- */
.estimate-bucket { margin-bottom: 1.25rem; }
/* Only the flex that puts the count beside the title. The inset comes from the `.panel__heading`
   wrapper around it and must NOT be written here: `[data-bs-theme="dark"] .section-heading` pads the
   heading itself for its accent bar, outranks a single-class shorthand, and so took the inline-start
   back — dropping the magenta bar onto the panel's own border, where the 14px corner radius clips it.
   Light mode looked right throughout, which is how it shipped. See PanelHeadingPaddingContractTest. */
.estimate-bucket__heading { display: flex; align-items: baseline; gap: 0.6rem; }
/* Days since the estimate was raised. Mono and tabular so a column of ages reads as a column of
   numbers rather than as words of differing width. */
.estimate-age { font-family: var(--bs-font-mono); font-size: 0.8rem; color: var(--bs-muted); font-variant-numeric: tabular-nums; }
/* A pager the table controllers inject as a direct child of a .panel. The panel carries no padding
   of its own (its table is meant to run edge to edge), so the pager has to bring its own or it sits
   flush against the border. `.pager:empty` already hides it on a single-page list. */
.panel > .pager { margin-top: 0.9rem; padding: 0 1rem 1rem; }

/* ---- RSPH-172 Wizard step indicator ---- */
/* Where you are in a two-step flow. Numbered because the order is real information — a vehicle list
   cannot be built before a customer is chosen — and both steps are shown so a one-field screen reads
   as a short first step rather than an unfinished page. */
.wizard-steps {
    display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1.25rem;
    list-style: none; margin: 0 0 1.5rem; padding: 0 0 1.1rem;
    border-bottom: 1px solid var(--bs-line);
}
.wizard-steps__step { display: flex; align-items: center; gap: 0.55rem; color: var(--bs-muted); }
.wizard-steps__index {
    display: inline-flex; align-items: center; justify-content: center;
    width: 1.55rem; height: 1.55rem; border-radius: 50%;
    border: 1.5px solid var(--bs-line); background: transparent;
    font-family: var(--bs-font-mono); font-size: 0.72rem; font-weight: 600;
}
.wizard-steps__label {
    font-family: var(--bs-font-mono); text-transform: uppercase; letter-spacing: 0.1em;
    font-size: 0.66rem;
}
.wizard-steps__step.is-current { color: var(--bs-text); }
.wizard-steps__step.is-current .wizard-steps__index {
    background: var(--bs-hi-vis); border-color: var(--bs-hi-vis); color: var(--bs-ink-2);
}
/* A finished step keeps the outline rather than the fill: only one step is where you are. */
.wizard-steps__step.is-done { color: var(--bs-text); }
.wizard-steps__step.is-done .wizard-steps__index { border-color: var(--bs-hi-vis); color: var(--bs-text); }

/* A wizard step holds one or two questions. At .form-card's 720px they floated in a box built for a
   settings form; this is sized for what the step actually asks. */
.form-card--wizard { max-width: 34rem; }
/* .form-actions does not wrap, so in the narrower wizard card three buttons squashed into two-line
   labels rather than moving to a second row. Scoped here rather than changed globally: every other
   screen's action row is sized for its card and works as it is. */
.form-card--wizard .form-actions { flex-wrap: wrap; }
.form-card--wizard .form-actions .btn { white-space: nowrap; }

/* RSPH-174: the suggested-bookings pane. It shares .bk-rail's card language deliberately — a
   planner reads both panes the same way — and adds only what a decision needs: the date, why it is
   that date, and the two buttons. .bk-rails stacks the two panes in the grid slot the unbooked rail
   used to occupy alone. */
.bk-rails { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
.bk-proposal__date { margin: 0.4rem 0 0; font-size: 0.9rem; display: flex; align-items: baseline; gap: 0.4rem; flex-wrap: wrap; }
.bk-proposal__duration { font-size: 0.72rem; color: var(--bs-muted); }
.bk-proposal__basis { margin: 0.15rem 0 0; font-size: 0.72rem; color: var(--bs-muted); }
.bk-proposal__actions { display: flex; gap: 0.4rem; margin-top: 0.6rem; flex-wrap: wrap; }
.bk-proposal__actions form { margin: 0; }
/* Outstanding parts on a suggested car. Folded by default: the count is what changes the decision,
   and a rail of dozens of cards cannot afford every line of every one. The summary carries the
   pointer cursor because a <details> marker alone reads as decoration at this size. */
.bk-proposal__parts { margin: 0.35rem 0 0; font-size: 0.72rem; }
.bk-proposal__parts > summary { cursor: pointer; color: var(--bs-muted); }
.bk-proposal__parts > summary:hover { color: inherit; }
.bk-proposal__parts-list { list-style: none; margin: 0.35rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.25rem; }
.bk-proposal__parts-list li { display: flex; align-items: baseline; gap: 0.35rem; flex-wrap: wrap; }
.bk-proposal__part-name { flex: 1 1 8rem; min-width: 0; }
.bk-proposal__part-number { color: var(--bs-muted); }
.bk-proposal__part-eta { color: var(--bs-muted); white-space: nowrap; }
/* Bounded so the unbooked rail beneath it stays reachable — see the template note. */
.bk-proposals__list { max-height: 32rem; overflow-y: auto; padding-right: 0.2rem; }
.bk-proposals__more { margin: 0.5rem 0 0; font-size: 0.72rem; color: var(--bs-muted); }

/* RSPH-174 Phase 4: the suggested-technicians pane. It reuses the booking rail's .bk-proposal
   language wholesale (same card, same date line, same basis note, same two buttons) — the two panes
   answer the same shape of question one level apart, and a manager should not have to learn a second
   layout. All this adds is the who/what line, which the booking pane has no equivalent of. */
.ts-proposal__who { margin: 0.4rem 0 0; font-size: 0.9rem; display: flex; align-items: baseline; gap: 0.4rem; flex-wrap: wrap; }
.ts-proposal__trade { font-size: 0.72rem; color: var(--bs-muted); }

/* Task 7: the Refinity screens' shared tab strip — LINKS across two routes, not Bootstrap panes (see
   templates/integration/_refinity_tabs.html.twig). Named .refinity-tabs rather than .tabs/.nav-tabs:
   Bootstrap owns .nav-tabs globally and .tabs is generic enough to collide with a future block. */
.refinity-tabs {
    display: flex; gap: 1.5rem; margin: 0 0 1.5rem;
    border-bottom: 1px solid var(--bs-line);
}
.refinity-tabs__tab {
    padding: 0.6rem 0.1rem; font-size: 0.85rem; font-weight: 600;
    color: var(--bs-muted); text-decoration: none;
    border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.refinity-tabs__tab:hover { color: var(--bs-text); }
.refinity-tabs__tab.is-current { color: var(--bs-text); border-bottom-color: var(--bs-hi-vis); }

/* First-run setup banner: sits above page content on every authenticated page for a tenant admin
   with outstanding items. Amber rather than red — this is an unfinished setup, not a failure. */
.setup-banner {
    display: flex; align-items: center; gap: 0.75rem;
    padding: 0.6rem 1rem; margin: 0 0 1rem;
    background: #fff4e0; border: 1px solid #f3d9a8; border-radius: 10px;
    font-size: 0.85rem; color: var(--bs-text);
}
.setup-banner[hidden] { display: none; }
.setup-banner__text { flex: 1; min-width: 0; }
.setup-banner__close {
    background: none; border: 0; cursor: pointer; font-size: 1.1rem; line-height: 1;
    color: var(--bs-muted); padding: 0 0.25rem;
}
[data-bs-theme="dark"] .setup-banner { background: rgba(255, 184, 28, 0.12); border-color: rgba(255, 184, 28, 0.45); }
