/**
 * The design system.
 *
 * Everything the dashboard's look is made of that is NOT a Tailwind utility lives here; the tokens
 * the utilities resolve to live in views/partials/design-system.php, which every page shell requires.
 * The two change together.
 *
 * WHY THIS FILE EXISTS
 *
 * The 107 content pages are written almost entirely in Tailwind utilities — `bg-gray-900`,
 * `text-brand-600`, `border-gray-100`. Remapping what `gray` and `brand` MEAN in the Tailwind config
 * restyles all of them at once without editing one. What cannot be expressed that way — the status
 * badges, the SLA meter, the way a table restacks on a phone — is written here as real classes.
 *
 * Tokens are CSS custom properties so this file and the Tailwind palette cannot drift: both are
 * derived from the same values, declared once in design-system.php's config and mirrored here.
 */

:root {
    color-scheme: light;

    /* Slate, with a deliberate cool bias — a flat neutral grey reads as unconsidered. These mirror
       the `gray` ramp in the Tailwind config; utilities and these classes must resolve alike. */
    --vs-50:  #F7F9FB;
    --vs-100: #EEF2F6;
    --vs-200: #E1E8EF;
    --vs-300: #C7D2DD;
    --vs-400: #94A3B4;
    --vs-500: #5B6B7C;
    --vs-600: #475669;
    --vs-700: #33404F;
    --vs-800: #262F3C;
    --vs-900: #161D26;
    --vs-950: #0F141B;

    /* The brand. Burgundy is reserved for things you can act on — buttons, active navigation, focus,
       links — and never carries status, so it is never mistaken for the past-target red beside it. */
    --vs-brand:      #800020;
    --vs-brand-lift: #A30E33;  /* fills and focus rings on a dark ground, where #800020 sinks */
    --vs-brand-text: #E8899D;  /* small text on a dark ground; any hue-faithful lift drifts rose */
    --vs-brand-soft: #FAE8EC;

    /* Elevation. Two layers each: a tight contact shadow that seats the edge, plus a wider, softer
       ambient one that lifts the whole surface. A single blurred shadow reads as a smudge at this
       size; the pair reads as a card sitting on a page.

       Ported from NizPhone-v2, which had already worked this out — same scale, same intent.

       sm is the resting state of every card. md is for something genuinely raised: a dropdown, a
       hovered tile. Cards do NOT use md on hover by default; a table lifting when the pointer
       crosses it is motion with nothing behind it. */
    --vs-shadow-sm: 0 1px 2px rgb(16 24 40 / 0.06), 0 1px 3px rgb(16 24 40 / 0.10);
    --vs-shadow-md: 0 2px 4px rgb(16 24 40 / 0.06), 0 8px 16px -4px rgb(16 24 40 / 0.10);

    /* Status. Four meanings, each with a tinted ground and a readable ink. */
    --vs-ok:        #197A3D;
    --vs-ok-bg:     #E9F7EE;
    --vs-ok-line:   #B7E3C6;
    --vs-warn:      #8A6100;
    --vs-warn-bg:   #FBF2DC;
    --vs-warn-line: #EBD08A;
    --vs-bad:       #C42B1E;
    --vs-bad-bg:    #FCEAE8;
    --vs-bad-line:  #F2B8B1;
    --vs-info:      #1D5FA8;
    --vs-info-bg:   #E8F0F9;
    --vs-info-line: #AFCCEA;

    --vs-track: #E1E8EF;   /* an unfilled SLA segment */

    --vs-radius:      10px;
    --vs-radius-chip: 4px;
}

html.dark {
    --vs-track: #262F3C;

    /* Tells the browser to paint its OWN widgets dark too — checkboxes, radios, scrollbars,
       date pickers, the autofill overlay. Without it a checkbox renders as a white square on a
       dark form, which is what every checkbox in this app was doing. */
    color-scheme: dark;

    --vs-brand-soft: #2E121A;

    --vs-ok:        #56D07C;
    --vs-ok-bg:     #122C1B;
    --vs-ok-line:   #1F5233;
    --vs-warn:      #E0B341;
    --vs-warn-bg:   #2E2412;
    --vs-warn-line: #5A4718;
    --vs-bad:       #FF7A6E;
    --vs-bad-bg:    #3A1A17;
    --vs-bad-line:  #6B2C27;
    --vs-info:      #6BA6E8;
    --vs-info-bg:   #14243A;
    --vs-info-line: #2A4A73;
}

/* ============================================================================================
   Typography
   ============================================================================================ */

body {
    font-feature-settings: "cv05" 1, "ss01" 1;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

/* Figures line up in columns everywhere they are compared: tables, hour totals, SLA days. Without
   this a 1 is narrower than a 7 and a column of times reads as ragged. */
table td, table th,
.tabular, .status-badge, input[type="number"] {
    font-variant-numeric: tabular-nums;
}

/* ============================================================================================
   Status badges
   ============================================================================================
   Moved here from datatables-theme.css, which had not described its own contents for some time.
   A square-ish chip rather than a full pill: pills read as tags you can remove, and these are
   states you cannot. */

.status-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.2rem 0.45rem;
    border-radius: var(--vs-radius-chip);
    border: 1px solid transparent;
    font-size: 0.75rem;
    font-weight: 500;
    line-height: 1.25;
    white-space: nowrap;
}

.status-badge.success { background: var(--vs-ok-bg);    color: var(--vs-ok);    border-color: var(--vs-ok-line); }
.status-badge.warning { background: var(--vs-warn-bg);  color: var(--vs-warn);  border-color: var(--vs-warn-line); }
.status-badge.danger  { background: var(--vs-bad-bg);   color: var(--vs-bad);   border-color: var(--vs-bad-line); }
.status-badge.info    { background: var(--vs-info-bg);  color: var(--vs-info);  border-color: var(--vs-info-line); }
.status-badge.neutral { background: var(--vs-100);      color: var(--vs-600);   border-color: var(--vs-200); }

html.dark .status-badge.neutral { background: var(--vs-800); color: var(--vs-300); border-color: var(--vs-700); }

/* ============================================================================================
   The SLA meter
   ============================================================================================
   Five working days from Date Received is the unit this business runs on. Rendered as an
   instrument rather than a number in a pill: the segments say where a job STANDS, which a figure
   alone does not. The overrun stub appears only once a job is past target.

   Markup: <span class="sla"><span class="sla-track"><i class="on"></i>…</span><span class="sla-num">4.20<span>/ 5</span></span></span> */

.sla { display: inline-flex; align-items: center; gap: 0.5rem; }

.sla-track {
    display: grid;
    grid-template-columns: repeat(5, 11px);
    gap: 2px;
    align-items: end;
    height: 14px;
}

.sla-track i {
    display: block;
    height: 6px;
    border-radius: 1.5px;
    /* A token rather than a second `html.dark .sla-track i` rule: that selector outranked
       `.sla-track i.on` and painted every filled segment the empty colour. */
    background: var(--vs-track);
}

.sla-track i.on   { background: var(--vs-ok);  height: 10px; }
.sla-track i.part { background: var(--vs-ok);  height: 10px; opacity: 0.45; }
.sla-track i.over { background: var(--vs-bad); height: 10px; }

.sla-over {
    width: 4px;
    height: 14px;
    border-radius: 1.5px;
    background: var(--vs-bad);
    margin-left: 1px;
}

.sla-num { font-size: 0.8125rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.sla-num span { color: var(--vs-400); font-weight: 400; }
.sla.is-bad .sla-num { color: var(--vs-bad); }
.sla-none { color: var(--vs-400); font-size: 0.8125rem; }

/* ============================================================================================
   Native form controls
   ============================================================================================
   Checkboxes and radios are left as the platform's own controls rather than restyled from
   scratch — they already behave correctly with the keyboard and with assistive technology.
   Only the checked colour is ours; the rest the browser draws to match color-scheme above. */

input[type="checkbox"], input[type="radio"], progress, input[type="range"] {
    accent-color: var(--vs-brand);
}

html.dark input[type="checkbox"],
html.dark input[type="radio"],
html.dark progress,
html.dark input[type="range"] {
    accent-color: var(--vs-brand-lift);   /* #800020 disappears into a dark ground. */
}

/* ============================================================================================
   Column alignment
   ============================================================================================
   Applied per column by assets/js/tables.js, which reads what the column actually holds.
   Everything used to be centred, so client names had no common left edge to scan down and
   figures did not line up by place value — which is most of the reason to put them in a column.

     ta-l  text          ta-r  figures          ta-c  a status chip or a button */

/* !important because datatables-theme.css centres every cell with a three-element selector
   that outranks a plain class, and because DataTables' own dt-type-numeric would otherwise
   decide the numeric columns on its own terms. */
th.ta-l, td.ta-l { text-align: left !important; }
th.ta-r, td.ta-r { text-align: right !important; }
th.ta-c, td.ta-c { text-align: center !important; }

/* Figures line up by place value, so they get the tabular figures as well as the edge. */
td.ta-r { font-variant-numeric: tabular-nums; }

/* A right-aligned column sits hard against the next one without this; the padding the
   DataTables theme gives every cell is symmetric and was tuned for centred content. */
th.ta-r, td.ta-r { padding-right: 18px !important; }
th.ta-l, td.ta-l { padding-left: 18px !important; }

/* ============================================================================================
   Focus
   ============================================================================================
   A fallback, not an override. The form controls already pair every `focus:outline-none` with a
   `focus:ring` — all 383 of them — and because `:where()` carries no specificity, those keep
   winning. This catches what they miss: links, table rows given a tabindex, and anything new
   that forgets, so nothing keyboard-reachable is ever focused invisibly. */

:where(a, button, input, select, textarea, [tabindex]):focus-visible {
    outline: 2px solid var(--vs-brand);
    outline-offset: 2px;
    border-radius: 4px;
}

html.dark :where(a, button, input, select, textarea, [tabindex]):focus-visible {
    outline-color: var(--vs-brand-text);
}

/* ============================================================================================
   Tables that become cards on a phone
   ============================================================================================
   Add `table-cards` to a <table> and give each <td> a data-label. Below 768px each row restacks
   into a card with the label beside the value; above it, nothing changes. One CSS block and one
   attribute per cell — there is no second set of markup to keep in step.

   Applied to the job lists people work from on the move, not to the wide reporting tables, where
   a hundred stacked label/value pairs would be worse than scrolling. */

@media (max-width: 767px) {
    table.table-cards,
    table.table-cards tbody,
    table.table-cards tr,
    table.table-cards td {
        display: block;
        width: auto;
    }

    table.table-cards thead { display: none; }

    table.table-cards tr {
        border: 1px solid var(--vs-200);
        border-radius: 8px;
        padding: 0.625rem 0.75rem;
        margin-bottom: 0.5rem;
        background: #FFFFFF;
    }

    html.dark table.table-cards tr {
        border-color: var(--vs-800);
        background: var(--vs-900);
    }

    table.table-cards td {
        border: 0 !important;
        padding: 0.1875rem 0 !important;
        /* !important because DataTables Responsive sets display:none inline on the columns it
           decided to hide, and in card mode there is room for all of them. */
        display: flex !important;
        align-items: center;
        justify-content: space-between;
        gap: 0.75rem;
        text-align: right !important;
    }

    /* The label the <th> used to carry, now on the row itself. */
    table.table-cards td::before {
        content: attr(data-label);
        flex: none;
        font-size: 0.6875rem;
        font-weight: 500;
        color: var(--vs-400);
        text-align: left;
        text-transform: none;
    }

    /* The first real column is the card's heading rather than another labelled row. */
    table.table-cards td[data-card="lead"]::before { content: none; }
    table.table-cards td[data-card="lead"] {
        justify-content: flex-start;
        text-align: left !important;
        font-weight: 600;
        padding-bottom: 0.375rem !important;
    }

    /* A row number is a position in a table. A card has no table to be in. */
    table.table-cards td[data-card="index"] { display: none !important; }

    /* DataTables Responsive has nothing left to collapse or reveal once the row is a card. */
    table.table-cards td.dtr-control { display: none !important; }
    table.table-cards tr.child { display: none !important; }
}

/* ============================================================================================
   The sidebar's surface
   ============================================================================================
   The navigation is part of the app's frame, not part of the page's content, and it should read
   that way: a white column under the burgundy header, with the work sitting on the grey ground
   beside it.

   It had no background at all, so it shared the body's --vs-50 and was separated from the content
   only by a hairline. The drawer below has always painted itself white — this is that same
   decision, applied at every width rather than only under lg.

   Deliberately NOT a brand tint. The active nav item is bg-brand-50, and the palette's own rule is
   that burgundy marks what you can act on; a pink column would take the cue away from the one item
   that needs it and spend the brand colour on a surface. */

#sidebar {
    background: #FFFFFF;
}

/* ============================================================================================
   Card elevation
   ============================================================================================
   A real class rather than Tailwind's shadow-[var(--vs-shadow-sm)]. That arbitrary value silently
   produces NOTHING through the Play CDN — the class is accepted, no rule is generated, and the
   computed style comes back `none`. It looks correct in the markup and does nothing on screen,
   which is the worst kind of wrong. */

.vs-card {
    box-shadow: var(--vs-shadow-sm);
}

/* ============================================================================================
   Tabular data
   ============================================================================================
   One size for every table in both applications.

   There were two, and they never agreed: hand-written tables carried Tailwind's text-sm (14px)
   while anything rendered by DataTables took .8125rem (13px) from datatables-theme.css. A staffer
   moving from Open Jobs to a Team Leader report saw the same kind of data at two sizes.

   13px is the settled size, matching what the DataTables pages already used — the denser of the
   two, which is what a table full of columns wants.

   Header cells are NOT set here. Every thead row already declares text-[11px] uppercase (101 of
   them), and DataTables' own theme sets the same 11px, so headers were already consistent; the
   gap was only ever in the body. */

table.vs-table {
    font-size: 0.8125rem;
}

/* FORM CONTROLS MATCH THE TABLES THEY FILTER — 13px, not 14px.

   A filter strip sits directly above its table. At text-sm the dropdowns and search boxes were a
   step larger than every row beneath them, which reads as the chrome shouting over the data.

   WHY THIS RETARGETS .text-sm RATHER THAN CHANGING $selectClass AND $inputClass.

   Those two variables cover about 250 of the 490 <select> elements here. The rest are pasted copies
   of their value that have since drifted — 97 of them differ only in border-gray-300 vs -200 — and
   editing the variables would leave those at 14px, which is the split this is meant to close. Every
   one of them, pasted or not, carries text-sm; matching on it reaches all of them at once.

   Specificity: select.text-sm is (0,0,1,1) and beats Tailwind's .text-sm at (0,0,1,0), so no
   !important is needed.

   Deliberately NOT included:
     button  — 142 are text-sm and 150 text-xs, sized by role, and a button is not a field
     .text-xs / .text-[11px] controls — already smaller on purpose (compact filter rows, the
       "Insert group…" select that sits inside a label). This rule only ever brings 14px down. */
select.text-sm,
input.text-sm,
textarea.text-sm {
    font-size: 0.8125rem;
}

html.dark .vs-card {
    /* A light shadow on a dark ground reads as fog. Depth there is carried by surface lightness. */
    box-shadow: none;
}

html.dark #sidebar {
    /* A step UP from the page ground, not down. Shadows are close to invisible on a dark ground, so
       elevation there is carried by surface lightness instead. */
    background: var(--vs-900);
}

/* ============================================================================================
   The mobile navigation drawer
   ============================================================================================
   Under lg the sidebar slides in over the page rather than stacking above it. Before this, every
   page on a phone opened with a screenful of navigation before any work. */

@media (max-width: 1023px) {
    /* Below the header rather than over it, so the button that opened the drawer is still
       there to close it — and notifications and the account menu stay reachable. 4rem is the
       header height (h-16), which is sticky, so this holds however far the page is scrolled. */
    #sidebar {
        position: fixed;
        inset: 4rem auto 0 0;
        z-index: 30;
        width: 17rem;
        max-width: 85vw;
        overflow-y: auto;
        background: #FFFFFF;
        border-right: 1px solid var(--vs-200);
        transform: translateX(-100%);
        transition: transform 0.22s ease;
    }

    html.dark #sidebar {
        background: var(--vs-900);
        border-right-color: var(--vs-800);
    }

    #sidebar.is-open { transform: translateX(0); }

    #sidebarScrim {
        position: fixed;
        inset: 4rem 0 0 0;
        z-index: 25;
        background: rgba(8, 12, 17, 0.55);
        opacity: 0;
        pointer-events: none;
        transition: opacity 0.22s ease;
    }

    #sidebarScrim.is-open { opacity: 1; pointer-events: auto; }
}

@media (min-width: 1024px) {
    #sidebarScrim { display: none; }

    /* The sidebar keeps its place while the page scrolls (the team's request, 2026-11-12).
       Below lg this was already true — the drawer above is position: fixed with its own
       overflow-y — but on desktop the sidebar was an ordinary flex item and scrolled away with
       everything else, so on the long pages the whole nav was off screen by the time you reached
       the table you came for.

       EACH LINE IS LOAD-BEARING, and three of them look redundant until they are removed:

       top: 4rem matches the header, which is sticky and h-16. Sticking at 0 would slide the nav
       up behind it.

       align-self and height together are what make sticky do anything at all. The sidebar is a
       flex item in a lg:flex-row container, so align-items: stretch sizes it to the full height
       of the row — an element already as tall as its container has no room to stick within, and
       position: sticky silently does nothing. Opting out of the stretch and giving it the
       viewport's height instead is the whole trick. (The aside also carries Tailwind's
       lg:min-h-[calc(100vh-4rem)]; same value, so it does not fight this.)

       overflow-y: auto gives a nav longer than the viewport its own scrollbar rather than a
       clipped tail. It also makes the sidebar a clipping context, which is safe because the nav
       expands in place — there is no absolutely-positioned flyout to cut off.

       overscroll-behavior stops a flick at the end of the nav from carrying on into the page,
       which is the behaviour this change exists to remove. */
    #sidebar {
        position: sticky;
        top: 4rem;
        align-self: flex-start;
        height: calc(100vh - 4rem);
        overflow-y: auto;
        overscroll-behavior: contain;
    }
}

@media (prefers-reduced-motion: reduce) {
    #sidebar, #sidebarScrim { transition: none; }
}

/* ============================================================================================
   Avatars — staff photos and client logos in table rows
   ============================================================================================

   WHY THIS IS CSS AND NOT TAILWIND UTILITIES

   These images are injected by JavaScript into DataTables rows, and they sit inside a flex
   container beside the name. That combination hides a trap:

   A FLEX ITEM'S DEFAULT `min-width` IS `auto`, WHICH FOR A REPLACED ELEMENT RESOLVES TO ITS
   INTRINSIC WIDTH — and min-width beats width. So `class="w-6 h-6"` on a client logo that is
   really 400px wide renders at 400px, not 24px. Square logos looked correct, wide banner logos
   (ABR Partners, ACORN) blew the row apart, which is why it read as "some images are broken"
   rather than as a sizing rule that never applied.

   Firefox enforces that automatic minimum on images more strictly than Chrome, so the same page
   looked right in one browser and wrong in the other.

   `min-width: 0` is the fix. It is expressed here rather than as another utility class because
   the sizing has now been corrected twice by editing class strings inside JavaScript, where it is
   invisible to anyone reading the markup and silently depends on the Tailwind CDN generating
   utilities for nodes that did not exist when it scanned the page. */

.vs-avatar,
.vs-avatar-lg {
    min-width: 0;          /* the whole point — see above */
    flex: 0 0 auto;        /* never stretched or squashed by the row beside it */
    border-radius: 9999px;
    object-fit: cover;     /* crop a non-square logo to the circle rather than distort it */
    object-position: center;
}

/* In a table row, beside the name. */
.vs-avatar {
    width: 1.5rem;
    height: 1.5rem;
}

/* In the edit dialog, where there is room for a recognisable image. */
.vs-avatar-lg {
    width: 3rem;
    height: 3rem;
}

/* The fallback when there is no image: the first letter of the name, same circle. */
.vs-avatar-initial {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 10px;
    font-weight: 600;
    background: var(--vs-100);
    color: var(--vs-400);
}

html.dark .vs-avatar-initial {
    background: var(--vs-800);
    color: var(--vs-500);
}

/* A client logo rather than a person's photo.
 *
 * `cover` is right for a face — crop to the middle and you still get the face. It is wrong for a
 * wordmark: ACORN's logo is a 400x80 banner, and cropping it to a 24px circle shows a sliver of the
 * middle two letters, which identifies nothing. `contain` fits the whole mark inside the circle
 * instead, small but recognisable, on a neutral ground so a transparent PNG still reads. */
.vs-avatar-logo {
    object-fit: contain;
    padding: 2px;
    background: #FFFFFF;
    border: 1px solid var(--vs-200);
}

html.dark .vs-avatar-logo {
    /* Kept white in dark mode on purpose: most of these logos are dark ink on transparent, and on
       a dark ground they vanish. A white disc is the lesser evil and is what the login card does. */
    border-color: var(--vs-700);
}

/* Hardening only — no size, no colour, nothing visual.
 *
 * Add this to any fixed-size image that sits inside a flex container. It removes the automatic
 * minimum size a flex item gets (`min-width: auto`, which for a replaced element resolves to its
 * INTRINSIC width and overrides any width you set) and stops the row stretching or squashing it.
 *
 * The .vs-avatar classes above already include both. This exists for images whose sizing is
 * already correct and should not be touched — the profile photo in the top bar, for one — so the
 * trap can be closed without restyling a file that every page loads. */
.vs-img-fixed {
    min-width: 0;
    flex: 0 0 auto;
}
