﻿/* ============================================================================
   Chilleweni IMS — design system
   ----------------------------------------------------------------------------
   An operational tool, not a marketing site: it is scanned and operated, so
   density, state legibility and tabular alignment matter more than whitespace.

   Type   IBM Plex Sans for UI, IBM Plex Mono for references, codes and figures.
          Plex is drawn for technical products and holds up at the 13px table
          sizes this app lives at. The mono face carries document refs
          (CH-IMS-SOP-MAINT-001) and job numbers, which are genuinely codes.
   Colour Navy and gold sampled from the Chilleweni logo. Neutrals are biased
          cold-blue rather than pure grey — the subject is a cold store, and a
          blue-shifted neutral sits under the navy without going muddy.
          Severity and status colour is semantic and kept separate from the
          gold accent, so "attention" never competes with "brand".
   ========================================================================= */

/* ---------------------------------------------------------------- tokens -- */

:root {
    /* Brand */
    --navy-900: #0a1530;
    --navy-800: #0e1c40;
    --navy-700: #132550;
    --navy-600: #16295c;
    --navy-500: #21386f;
    --navy-400: #33508f;
    --navy-300: #5b76b5;

    --gold-600: #a67c12;
    --gold-500: #c9a227;
    --gold-400: #ddb945;
    --gold-100: #fbf3dd;

    /* Cold-biased neutrals */
    --ink:        #0f1b2e;
    --ink-2:      #2c3d57;
    --steel:      #5b6f8b;
    --steel-2:    #8698b0;
    --line:       #dde5ee;
    --line-2:     #eaf0f6;
    --surface:    #ffffff;
    --surface-2:  #f7fafc;
    --canvas:     #eef3f8;

    /* Semantic — severity */
    /* The two series on a trend: what came in, and what went out. */
    --series-in:  #c8362c;
    --series-out: #2f5fd0;

    --sev-critical:    #b3261e;
    --sev-critical-bg: #fdeceb;
    --sev-high:        #b45309;
    --sev-high-bg:     #fef2e4;
    --sev-medium:      #8a6d0b;
    --sev-medium-bg:   #fdf6dc;
    --sev-low:         #0e6d84;
    --sev-low-bg:      #e3f4f8;

    /* Semantic — state */
    --ok:       #0f6b52;
    --ok-bg:    #e1f4ee;
    --warn:     #b45309;
    --warn-bg:  #fef2e4;
    --bad:      #b3261e;
    --bad-bg:   #fdeceb;
    --info:     #0e6d84;
    --info-bg:  #e3f4f8;
    --busy:     #5b3fa8;
    --busy-bg:  #efeafc;

    /* Type scale — dense by design */
    --fs-2xs:  0.6875rem;
    --fs-xs:   0.75rem;
    --fs-sm:   0.8125rem;
    --fs-base: 0.875rem;
    --fs-md:   0.9375rem;
    --fs-lg:   1.0625rem;
    --fs-xl:   1.3125rem;
    --fs-2xl:  1.625rem;

    --sans: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;

    /* ONE typeface, everywhere.
       Twelve selectors used to set chrome — table headers, the breadcrumb, sidebar section
       labels, tile labels, timestamps, the colour box, matrix group rows — in IBM Plex Mono,
       while the content sitting beside them was IBM Plex Sans. Almost every screen therefore
       carried two typefaces and read as two designs stitched together.

       The token is kept and pointed at the same stack rather than deleted, because .mono is
       applied throughout the markup to identifiers: usernames, URLs, system names, branch codes.
       Those keep their tabular numerals so they still line up down a column — they simply stop
       being a different face. */
    --mono: var(--sans);

    /* Shape & depth */
    --r-xs: 4px;
    --r-sm: 6px;
    --r-md: 8px;
    --r-lg: 12px;

    --sh-xs: 0 1px 2px rgba(15, 27, 46, .06);
    --sh-sm: 0 1px 3px rgba(15, 27, 46, .08), 0 1px 2px rgba(15, 27, 46, .04);
    --sh-md: 0 4px 12px rgba(15, 27, 46, .08), 0 2px 4px rgba(15, 27, 46, .04);
    --sh-lg: 0 12px 32px rgba(15, 27, 46, .12), 0 4px 8px rgba(15, 27, 46, .06);

    /* Both drive layout well beyond the sidebar itself — the content margin, the top bar and the
       left edge of every modal are measured from them, so they are tokens rather than literals. */
    --sidebar-w: 232px;
    --topbar-h: 54px;
    --ease: cubic-bezier(.4, 0, .2, 1);
}

*, *::before, *::after { box-sizing: border-box; }

html { font-size: 16px; }

body {
    margin: 0;
    font-family: var(--sans);
    font-size: var(--fs-base);
    line-height: 1.55;
    color: var(--ink);
    background: var(--canvas);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

/* Form controls do NOT inherit the page font — the browser gives them its own default, which is
   why the user button in the top bar was rendering in Arial while everything around it was Plex
   Sans. Every control class set font-family: inherit individually, so the one that forgot was
   invisible until you compared it against the text beside it. Stated once, for all of them. */
button, input, select, textarea, optgroup { font-family: inherit; font-size: inherit; }

h1, h2, h3, h4, h5 { margin: 0; line-height: 1.25; font-weight: 600; text-wrap: balance; }
p { margin: 0; }
a { color: var(--navy-600); text-decoration: none; }
a:hover { color: var(--navy-400); text-decoration: underline; }

:focus-visible {
    outline: 2px solid var(--gold-500);
    outline-offset: 2px;
    border-radius: var(--r-xs);
}

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #c3d0de; border-radius: 20px; border: 2px solid transparent; background-clip: content-box; }
::-webkit-scrollbar-thumb:hover { background: var(--steel-2); background-clip: content-box; }

/* The HTML `hidden` attribute must win.
   It is only `display: none` in the browser's own stylesheet, and ANY author rule that sets
   display beats a UA rule no matter how specific — so `.btn { display: inline-flex }` quietly
   un-hid every hidden button in the application. The Back button on the drill-down modal sat
   there at the top level with nothing to go back to. */
[hidden] { display: none !important; }

/* An identifier — a username, a URL, a code. Same face as everything else; the tabular figures
   are what actually make a column of them line up. */
.mono { font-variant-numeric: tabular-nums; letter-spacing: .01em; }
.num  { font-variant-numeric: tabular-nums; }
.muted { color: var(--steel); }
.tiny { font-size: var(--fs-xs); }

/* --------------------------------------------------------------- sidebar -- */
/* Dark navy. A tool that runs all day reads better with the chrome receding
   and the data bright, and it puts the brand somewhere other than a logo. */

.app-sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    width: var(--sidebar-w);
    background: var(--navy-800);
    background-image: linear-gradient(180deg, var(--navy-700) 0%, var(--navy-900) 100%);
    display: flex;
    flex-direction: column;
    z-index: 1040;
    transition: transform .22s var(--ease);
}

.sb-brand {
    display: flex;
    align-items: center;
    gap: 8px;
    height: var(--topbar-h);
    padding: 0 12px;
    flex-shrink: 0;
    border-bottom: 1px solid rgba(255, 255, 255, .08);
    cursor: default;
    user-select: none;
}
/* flex-shrink:0 stops the mark being squeezed when the brand text is long. Capped to the bar
   rather than a fixed height, so it cannot push the brand row taller than the top bar beside it. */
.sb-brand img { max-height: 32px; width: auto; flex-shrink: 0; }

.sb-brand-text {
    font-size: var(--fs-md);
    font-weight: 650;
    color: #fff;
    letter-spacing: -.015em;
    line-height: 1.15;
}
.sb-brand-text span { color: var(--gold-400); }
.sb-brand-text small {
    display: block;
    font-size: var(--fs-xs);
    font-weight: 400;
    color: var(--navy-300);
}

.sb-nav { flex: 1; overflow-y: auto; padding: 9px 8px 14px; }
.sb-nav::-webkit-scrollbar-thumb { background: rgba(255,255,255,.14); background-clip: content-box; }

/* RAISE A JOB CARD, ABOVE THE MENU AND NOT IN IT.
   ==============================================
   Every other thing in this sidebar is a PLACE. This is an ACT, and it is drawn as one: the gold
   the system already uses for the thing to press, against the navy, so the eye finds it before it
   starts reading the groups.

   NOT A .sb-link WITH A COLOUR ON IT. A link that is styled as a button but sits in a list of
   links reads as the first item of the list, which is the mistake this replaces — the button was
   in the corner of two screens and nobody found it from the other seven. */
.sb-raise {
    display: flex;
    align-items: center;
    gap: 9px;
    margin: 2px 1px 12px;
    padding: 10px 12px;
    border-radius: var(--r-sm);

    background: var(--gold-500);
    color: var(--navy-900);
    font-size: var(--fs-md);
    font-weight: 600;
    text-decoration: none;

    transition: background .14s var(--ease), transform .14s var(--ease);
}

.sb-raise:hover {
    background: var(--gold-400);
    color: var(--navy-900);
    text-decoration: none;
}

/* Pressed, and while you are ON the screen it opens — the same promise .sb-link.active makes,
   said in this control's own colours rather than borrowed from the links below it. */
.sb-raise:active { transform: translateY(1px); }
.sb-raise.active { background: var(--gold-400); box-shadow: inset 0 0 0 2px rgba(255, 255, 255, .35); }
.sb-raise:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

.sb-raise i { font-size: .95rem; }

/* A DIVIDER UNDER IT, because it is not part of the first group. Without one it reads as the
   heading of whatever section happens to be drawn first. */
/* THE LINE SITS ON THE FIRST GROUP, NOT FLOATING BETWEEN THE TWO. The gold reaches almost to the
   rule above it; leaving twelve pixels of empty navy under the rule made the group heading look
   dropped away from the line that introduces it. The label's own padding is the whole gap now. */
.sb-raise + .sb-section { border-top: 1px solid rgba(255, 255, 255, .09); padding-top: 0; }

.sb-section + .sb-section { margin-top: 10px; }

/* A divider between groups of links. Still set apart from the links themselves — smaller, lighter
   and in a quieter colour — but no longer uppercase monospace with a tenth of an em of tracking,
   which was the loudest text in the sidebar despite naming nothing you can click. */
.sb-label {
    font-size: var(--fs-base);
    font-weight: 600;
    color: var(--navy-300);
    padding: 5px 8px;
}

.sb-link {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 7px 9px;
    border-radius: var(--r-sm);
    color: #c3d1e8;
    font-size: var(--fs-md);
    font-weight: 450;
    text-decoration: none;
    position: relative;
    transition: background .14s var(--ease), color .14s var(--ease);
}
.sb-link:hover { background: rgba(255, 255, 255, .07); color: #fff; text-decoration: none; }

.sb-link.active {
    background: rgba(255, 255, 255, .1);
    color: #fff;
    font-weight: 550;
}
/* A rail marks position without relying on colour alone.
   Light blue rather than the --navy-600 used everywhere else for selection: this sits ON the
   navy sidebar, and the darker brand blue against a navy-800 background is invisible. Same
   idea, one of the few places where it has to be a different value to still be seen. */
.sb-link.active::before {
    content: "";
    position: absolute;
    /* Sits in the nav's own left padding, so it must move with it — at -10px against 8px of
       padding the rail landed outside the sidebar and was clipped away entirely. */
    left: -8px;
    top: 5px;
    bottom: 5px;
    width: 3px;
    border-radius: 0 3px 3px 0;
    background: var(--navy-300);
}

.sb-link i { font-size: 1.05rem; width: 18px; text-align: center; flex-shrink: 0; opacity: .9; }

/* How many things are waiting for this person on a My Queue item, and only on a My Queue item —
   see MenuController.Countable.

   SMALL AND QUIET, BECAUSE IT IS BESIDE FOUR OTHERS. It is white on the sidebar's own navy at a
   low opacity rather than the amber or red a chip would use: this says "there is something here",
   not "something is wrong", and four red pills down the left edge would be an alarm every morning.
   Severity and breach already own the loud colours in this system and they say something else.

   margin-left:auto pushes it to the right edge of the link, so the numbers line up down the column
   whatever the label beside them is; tabular figures stop 11 sitting narrower than 88. There is no
   nought state to style — an item with nothing waiting renders no badge at all. */
.sb-count {
    margin-left: auto;
    flex-shrink: 0;
    min-width: 20px;
    padding: 0 6px;
    border-radius: 999px;
    background: rgba(255, 255, 255, .16);
    color: #fff;
    font-size: var(--fs-2xs);
    font-weight: 600;
    line-height: 17px;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

/* On the item you are standing on, lifted just enough to stay visible against the lighter row. */
.sb-link.active .sb-count { background: rgba(255, 255, 255, .24); }

.sb-foot {
    flex-shrink: 0;
    padding: 9px 12px;
    border-top: 1px solid rgba(255, 255, 255, .08);
    font-size: var(--fs-2xs);
    line-height: 1.5;
    color: var(--navy-300);
}
.sb-foot strong { color: #b8c8e0; font-weight: 550; display: block; }

.sb-backdrop { display: none; position: fixed; inset: 0; background: rgba(10, 21, 48, .5); z-index: 1035; }

/* ---------------------------------------------------------------- shell --- */

.app-main {
    margin-left: var(--sidebar-w);
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

.app-topbar {
    height: var(--topbar-h);
    background: var(--surface);
    border-bottom: 1px solid var(--line);
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 0 18px;
    position: sticky;
    top: 0;
    z-index: 1030;
}

.topbar-toggle {
    border: none;
    background: transparent;
    color: var(--steel);
    font-size: 1.05rem;
    padding: 4px 6px;
    border-radius: var(--r-sm);
    cursor: pointer;
    line-height: 1;
}
.topbar-toggle:hover { background: var(--surface-2); color: var(--navy-600); }

/* A trail you can walk back up. Each segment that names a screen is a link; the last one is the
   page you are on and never is. It used to be one flat string of text that looked like a trail
   and behaved like a label. */
.crumb {
    display: flex;
    align-items: center;
    gap: 7px;
    flex-wrap: wrap;
    font-family: var(--mono);
    font-size: var(--fs-xs);
    color: var(--steel-2);
    letter-spacing: .02em;
    min-width: 0;
}

.crumb a {
    color: var(--steel);
    text-decoration: none;
    border-bottom: 1px solid transparent;
}
.crumb a:hover {
    color: var(--navy-600);
    border-bottom-color: var(--gold-400);
    text-decoration: none;
}

.crumb-sep { color: var(--line); }

/* Where you are, in the colour of text rather than of a link. */
.crumb-here { color: var(--ink-2); font-weight: 500; }

.user-btn {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 5px 9px 5px 5px;
    border-radius: 40px;
    border: 1px solid var(--line);
    background: var(--surface);
    cursor: pointer;
    transition: background .14s var(--ease);
}
.user-btn:hover { background: var(--surface-2); text-decoration: none; }

.avatar {
    width: 28px; height: 28px;
    border-radius: 50%;
    background: linear-gradient(140deg, var(--navy-500), var(--navy-700));
    color: #fff;
    display: flex; align-items: center; justify-content: center;
    font-size: var(--fs-2xs);
    font-weight: 600;
    letter-spacing: .02em;
    flex-shrink: 0;
}
.avatar.lg { width: 38px; height: 38px; font-size: var(--fs-sm); }
.avatar.sm { width: 24px; height: 24px; font-size: 10px; }

.user-btn .who { font-size: var(--fs-sm); font-weight: 500; color: var(--ink); line-height: 1.2; }
.user-btn .who small { display: block; font-size: var(--fs-2xs); color: var(--steel-2); font-weight: 400; }

/* --- THE NAME FITS THE PHONE IT IS ON ---------------------------------------------------------

   NOTHING CAPPED THIS, so on a handset the button took whatever width the name wanted. "System
   Administrator" over the role summary — the same words twice — came to nearly half a 375px screen,
   wrapped onto two lines each, and pushed the topbar to three times its own height with the
   breadcrumb squeezed into a column beside it. The owner: "on mobile the top right names thing is
   to big, please auto resize this."

   THE ROLE LINE GOES FIRST, because it is the half already said twice. The menu this button opens
   prints the name and the role at full width the moment it is tapped — see _Layout — so nothing is
   lost by keeping the button itself to the one fact a person glances at it for: who am I signed in
   as.

   AND THE NAME IS TRUNCATED RATHER THAN WRAPPED. A name that wraps changes the HEIGHT of a bar
   whose height is fixed, which is what broke the layout rather than the width did. One line and an
   ellipsis keeps the bar the size it says it is, whatever somebody is called. */
@media (max-width: 720px) {
    .user-btn {
        gap: 6px;
        padding: 4px 7px 4px 4px;

        /* Never more than the screen can spare. The breadcrumb has the rest. */
        max-width: 44vw;
    }

    .user-btn .who {
        display: block;
        min-width: 0;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }

    .user-btn .who small { display: none; }
}

.app-content { flex: 1; padding: 18px 22px 40px; }
.app-foot {
    padding: 14px 26px;
    border-top: 1px solid var(--line);
    font-size: var(--fs-xs);
    color: var(--steel-2);
    background: var(--surface);
}

/* ----------------------------------------------------------- page header -- */

.page-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 18px;
    flex-wrap: wrap;
    margin-bottom: 20px;
}
.page-head h1 { font-size: var(--fs-xl); font-weight: 650; letter-spacing: -.015em; }
/* text-wrap: pretty stops a subtitle breaking with three or four words stranded on a line of
   their own — the browser rebalances the last lines instead of filling greedily and orphaning
   the tail. No measure cap: it was 78ch, then 104ch, and both still broke the line well short
   of the page, which reads as text that has been cut off rather than text that has been set. */
.page-head .sub {
    font-size: var(--fs-base);
    color: var(--steel);
    margin-top: 4px;
    text-wrap: pretty;
}

/* A subtitle that is a list of rules rather than a sentence. Each line is one rule, so it
   gets the line spacing of something meant to be read a line at a time. */
.page-head .sub-rules { line-height: 1.75; }
.page-head .sub-rules strong { color: var(--ink); font-weight: 600; }
/* ACTIONS SIT ON THE RIGHT, ALWAYS. The page head is space-between, so this group already sat
   there on one line — but it wraps, and a wrapped second line fell back to the left, which put
   the buttons on two different sides of the same head. flex-end holds them together.

   AND THEY LINE UP WITH THE HEADING, NOT WITH THE TOP OF THE BLOCK. The head is align-items:
   flex-start, so the button's TOP edge met the h1's top edge and everything below it hung lower
   than the heading — a 30px button against a 26px line, reading as slightly dropped. The owner:
   "can be bigger and in line with the header".

   IT LINES UP WITH THE SUBTITLE, NOT THE HEADING. Centred on the h1 it sat above the top of the
   head entirely — the owner: "its to high now", and "needs to be inline with the under text of
   the header". So the offset below walks down the head the way the text does: the heading's line
   box, the subtitle's 4px gap, half the subtitle's line — and then back up by half the button, to
   put its middle on the subtitle's middle.

   EVERY NUMBER IN IT IS THE ONE THE ELEMENT IS ACTUALLY DRAWN FROM. 1.25 is the heading
   line-height set on h1–h5, 1.55 is the body line-height, --fs-xl and --fs-base are the two font
   sizes involved, and --action-h is the button's own height. Change any of them in its own place
   and the alignment follows instead of drifting. */
.page-head .actions {
    /* The action's own height: its text line, its padding twice, its border twice. */
    --action-h: calc(var(--fs-base) * 1.4 + 18px + 2px);

    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;

    /* No subtitle to line up with, so the heading's own line is the only thing to centre on. */
    margin-top: calc((var(--fs-xl) * 1.25 - var(--action-h)) / 2);
}

.page-head:has(.sub) .actions {
    margin-top: calc(
        var(--fs-xl) * 1.25          /* down past the heading            */
        + 4px                        /* the subtitle's gap               */
        + var(--fs-base) * 1.55 / 2  /* to the middle of its line        */
        - var(--action-h) / 2);      /* back up by half the button       */
}

/* THE SCREEN'S OWN ACTION, SIZED LIKE ONE. A page head carries the single thing this screen is
   for — Raise a job card, Print — and at the shared .btn size it read as a minor control beside a
   21px heading. Bigger here and nowhere else: a toolbar button acts on a row, this acts on the
   screen. Keep these two in step with --action-h above; the alignment is worked out from them. */
.page-head .actions .btn { font-size: var(--fs-base); padding: 9px 16px; }

/* --------------------------------------------------------------- panels --- */

.panel {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--sh-xs);
    overflow: hidden;
}
/* --- VERTICAL RHYTHM BETWEEN THE BLOCKS A PAGE IS BUILT FROM ---------------------------------

   THIS WAS `.panel + .panel`, AND THAT IS WHY THE DASHBOARD HAD NO GAPS.

   The adjacent-sibling selector only fires when two panels are directly next to each other.
   The dashboard puts a `.split` grid between two of them, and a `.risk-strip` between two more —
   so the rule matched nothing and the panels sat flush, edge to edge, twice on the same screen.
   The owner, twice: "no spacing between the tables again?? on dashboard fix!!"

   A BOTTOM MARGIN ON THE BLOCK ITSELF cannot be broken by whatever is put between them. It is on
   the four things a page is actually assembled from, so a screen built next year gets the spacing
   without anybody remembering this.

   EXCEPT INSIDE A GRID, where the gap is the grid's job — a bottom margin there would push the
   last panel past the bottom of its cell and break the equal heights the grid was chosen for. */
.panel,
.split,
.risk-strip,
.tbl-wrap + .tbl-wrap { margin-bottom: 16px; }

.split > * > .panel:last-child,
.split > .panel { margin-bottom: 0; }


.panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 9px 14px;
    /* Solid navy, edge to edge. A card used to announce itself with dark text on white, which at
       this size was indistinguishable from the first row of content under it — the header and the
       body were one continuous field of white. The bar is now the thing that says "new card". */
    background: var(--navy-600);
    color: #fff;
}
.panel-head h2, .panel-head h3 { font-size: var(--fs-sm); font-weight: 650; color: #fff; }

/* Secondary text on the bar. Not --steel-2, which is a light grey chosen for white backgrounds
   and all but disappears on navy. */
.panel-head .hint { font-size: var(--fs-xs); color: rgba(255, 255, 255, .72); }

/* Anything interactive that sits on the bar has to be legible against navy rather than against
   the white it was designed for. */
.panel-head .btn-ghost {
    background: rgba(255, 255, 255, .12);
    border-color: rgba(255, 255, 255, .28);
    color: #fff;
}
.panel-head .btn-ghost:hover {
    background: rgba(255, 255, 255, .22);
    border-color: rgba(255, 255, 255, .45);
    color: #fff;
}
.panel-head a { color: #fff; }
.panel-body { padding: 14px; }
.panel-foot {
    padding: 9px 14px;
    border-top: 1px solid var(--line);
    background: var(--surface-2);
    font-size: var(--fs-xs);
    color: var(--steel);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
}

/* ---------------------------------------------------------------- grid ---- */

.grid { display: grid; gap: 14px; }
.g-2 { grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); }
.g-4 { grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
/* Two columns whose ROWS line up.
   These used to be two independent stacks side by side, so each column ran to its own
   rhythm: the panels never shared a top or a bottom edge, and the shorter column trailed
   off leaving the page ragged. Panels are now grid items, paired into rows, and stretch
   to match their neighbour — so every horizontal rule on the page agrees with the one
   beside it. */
.split {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(280px, 1fr);
    gap: 16px;
    align-items: stretch;
}

/* Equal halves. The 2fr/1fr default suits a form beside a reference note, but where the two
   panels are peers — severity beside branch, both entry points into the same work — the narrow
   side squeezed a five-column table down to three. */
.split.is-even { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }

/* Which column a panel belongs to, stated rather than inferred, so a conditional panel
   disappearing cannot shunt the one after it into the wrong column. */
.split > .col-main { grid-column: 1; }
.split > .col-side { grid-column: 2; }
.split > .col-wide { grid-column: 1 / -1; }

/* The wrapper is the grid item; the panel inside must fill it, or the panel keeps its own
   height and the alignment the grid just established is thrown away. */
.split > .col-main,
.split > .col-side,
.split > .col-wide { display: flex; flex-direction: column; min-width: 0; }

.split > .col-main > .panel,
.split > .col-side > .panel,
.split > .col-wide > .panel { flex: 1 1 auto; }

/* Stretching a panel to match a taller neighbour keeps the row aligned, but leaves short
   content pinned to the top with a block of white under it. Centring it only moved the gap;
   letting the content FILL the space removes it — the cards grow instead of floating. */
.panel.fill-body { display: flex; flex-direction: column; }
.panel.fill-body > .panel-body {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
}
.panel.fill-body > .panel-body > * { flex: 1 1 auto; min-height: 0; }

@media (max-width: 1100px) {
    /* .split.is-even AS WELL AS .split, and the second selector is not redundant. The even variant
       declares its columns with one more class than this rule had, so it outranked the collapse and
       quietly kept its two columns all the way down — a phone was rendering the dashboard's branch
       breakdown in a 166px column, which is narrower than the word "Unassigned" in its own heading.
       Every table inside it scrolled sideways, and it read as the tables being at fault rather than
       the grid holding them. */
    .split,
    .split.is-even { grid-template-columns: 1fr; }
    .split > .col-main,
    .split > .col-side,
    .split > .col-wide { grid-column: 1; }
}

.stack { display: flex; flex-direction: column; gap: 14px; }
.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.row.tight { gap: 6px; }

/* A ROW OF ACTIONS GOES RIGHT. Filter and Clear are hung under a filter field on two screens
   rather than in a .filter-actions, and sat left while the same pair sat right everywhere else.
   Dillan: "buttons for actions are always right aligned". */
.row.is-actions { justify-content: flex-end; margin-left: auto; }
.push { margin-left: auto; }

/* ----------------------------------------------------------- stat tiles --- */

/* The dashboard's tile row. Four across on a wide screen, so six or seven tiles fall as 4+2
   or 4+3 rather than packing six into one row and stranding the last on its own. */
.tile-grid {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 14px;
    margin-bottom: 18px;
}
@media (max-width: 1300px) { .tile-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 820px)  { .tile-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px)  { .tile-grid { grid-template-columns: minmax(0, 1fr); } }

/* Every tile is a button now — the dashboard is a set of doorways, and a number you cannot
   follow is just trivia. Reset the button chrome and keep the card look. */
.tile {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    padding: 11px 13px;
    box-shadow: var(--sh-xs);
    position: relative;
    overflow: hidden;

    display: flex;
    flex-direction: column;
    align-items: stretch;
    text-align: left;
    font: inherit;
    cursor: pointer;
    transition: border-color .12s var(--ease), box-shadow .12s var(--ease), transform .12s var(--ease);
}

.tile:hover {
    border-color: var(--navy-400);
    box-shadow: var(--shadow-sm);
    transform: translateY(-1px);
}
.tile:focus-visible { outline: 2px solid var(--gold-400); outline-offset: 2px; }

/* The chevron only appears on approach — always-on it competed with the figure. */
.tile-go {
    position: absolute;
    right: 13px;
    top: 15px;
    font-size: .8rem;
    color: var(--steel-2);
    opacity: 0;
    transition: opacity .12s var(--ease);
}
.tile:hover .tile-go, .tile:focus-visible .tile-go { opacity: 1; }

/* A zero is still worth showing — and still worth opening, to confirm it really is empty —
   but it should not draw the eye the way a number does. */
.tile.is-quiet .value { color: var(--steel-2); font-weight: 500; }
/* State is carried by a left stripe as well as the number's colour, so it
   still reads in greyscale and in print. */
.tile::before {
    content: "";
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 3px;
    background: var(--line);
}
.tile.is-ok::before   { background: var(--ok); }
.tile.is-warn::before { background: var(--warn); }
.tile.is-bad::before  { background: var(--bad); }
.tile.is-info::before { background: var(--info); }
.tile.is-busy::before { background: var(--busy); }

.tile .label {
    font-size: var(--fs-xs);
    font-weight: 500;
    color: var(--steel);
    display: flex;
    align-items: center;
    gap: 6px;
}
.tile .value {
    font-size: var(--fs-xl);
    font-weight: 650;
    letter-spacing: -.03em;
    font-variant-numeric: tabular-nums;
    line-height: 1.15;
    margin-top: 4px;
}
.tile.is-bad .value  { color: var(--bad); }
.tile.is-warn .value { color: var(--warn); }
.tile.is-ok .value   { color: var(--ok); }
.tile .foot { font-size: var(--fs-xs); color: var(--steel); margin-top: 3px; }

/* --------------------------------------------------------------- tables --- */
/* Everything left-aligned — headers, text and figures alike — so the eye has a
   single vertical rule to follow. Columns size themselves to their content and
   long text wraps rather than forcing a horizontal scroll. */

.tbl-wrap { overflow-x: auto; }

table.tbl {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    font-size: var(--fs-sm);
    /* auto (the default) lets each column take the width its content needs */
    table-layout: auto;
}

table.tbl thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--surface-2);
    /* A plain bold label in the same face and size as the row beneath it. It was uppercase
       monospace with .07em of tracking, which made a one-word heading like "Status" occupy the
       width of three and read as a different language from its own column. */
    font-size: var(--fs-xs);
    font-weight: 600;
    color: var(--navy-600);
    text-align: left;
    padding: 7px 12px;
    border-bottom: 1px solid var(--line);
    vertical-align: bottom;
    white-space: nowrap;
}

table.tbl tbody td {
    padding: 8px 12px;
    border-bottom: 1px solid var(--line-2);
    vertical-align: top;
    text-align: left;
    /* Long descriptions wrap instead of stretching the table off-screen. */
    overflow-wrap: anywhere;
    word-break: break-word;
}

table.tbl tbody tr:last-child td { border-bottom: none; }

/* A totals row. The columns above only mean something against it — "2 overdue at Merino
   Phase 2" reads differently when the estate has four than when it has forty. */
table.tbl tfoot td {
    padding: 8px 12px;
    border-top: 1px solid var(--line);
    background: var(--surface-2);
    font-size: var(--fs-sm);
    font-weight: 550;
    color: var(--steel);
    text-align: left;
}
table.tbl tfoot tr:hover td { background: var(--surface-2); }
table.tbl tbody tr { transition: background .1s var(--ease); }
table.tbl tbody tr:hover td { background: var(--surface-2); }

/* Figures stay left-aligned but keep tabular digits so they still line up. */
table.tbl td.n, table.tbl th.n {
    text-align: left;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* THE HEADING MAY WRAP ON A NARROW SCREEN; THE FIGURE UNDER IT STILL MAY NOT.
   The rules above are right about the data — "1 250" broken across two lines is two numbers — and
   were wrong to apply the same to the heading. Headings like "Cards, 12 months", "Last raised" and
   "Assigned to" are several words that will never be misread on two lines, and holding them on one
   is what kept the asset and schedule tables wider than a phone: those two size to their content
   rather than to declared widths, so the longest HEADING was the minimum width of the whole table.

   Deliberately every header and not just the numeric ones. A heading is a label, and there is no
   column in this system whose label means something different on a second line. */
@media (max-width: 1100px) {
    table.tbl thead th { white-space: normal; }
}

/* ---------- The asset and schedule tables ----------
   THESE TWO SIZE THEMSELVES TO THEIR CONTENT — no fixed layout, no declared widths — so there is
   no floor to lower and no percentages to re-cut. Dropping the column is the only lever, and it
   is enough: with no colgroup to keep in step, hiding the cells is the whole change.

   Same order-of-expendability idea as the job card table. Assets loses the twelve-month history
   first (a number you look at when reviewing a machine, not when finding one), then Where. The
   schedule loses the last card it raised, then who it is allocated to, then Where. Both fold what
   they drop onto the naming cell, so the row still carries it.

   EVERY SELECTOR NAMES ITS TABLE. Written against .tbl they would have applied to every table in
   the system, and "hide the column called c-person" would have taken the Person column off the
   Users screen — the one column that screen exists for. */

@media (max-width: 900px) {
    table.tbl-assets    .c-history { display: none; }   /* cards in the last 12 months */
    table.tbl-schedules .c-last    { display: none; }   /* the last card it raised */
}

@media (max-width: 760px) {
    table.tbl-schedules .c-person     { display: none; }
    table.tbl-schedules .fold-person  { display: block; }
}

/* --- Today, on a phone -----------------------------------------------------------------
   THE SCREEN A TECHNICIAN ACTUALLY OPENS, and the one that was worst on a handset. Six columns
   on a 375px screen gave the issue about sixty pixels, which broke words across lines in the
   middle of a letter — "Chill / er 1 / door / seal / liftin / g at / the / hing / e side" — and
   pushed the due date off the right edge where nobody would ever see it.

   THE HOUSE PATTERN, the same one the assets and schedules tables already use: the columns that
   cannot earn their width stop being columns and fold into the one cell that survives. Nothing is
   hidden — the severity, the wait and the deadline are all still on the row, underneath the issue
   where they read as a sentence instead of as four cramped columns.

   JOB, ISSUE AND WHAT IT NEEDS ARE WHAT IS LEFT. They are the three a person standing in a chill
   room is actually asking: which card, what is wrong, what do I do about it. */
@media (max-width: 700px) {
    table.tbl-cards .c-sev,
    table.tbl-cards .c-since,
    table.tbl-cards .c-due,
    table.tbl-cards .c-when,
    table.tbl-cards .c-who,
    table.tbl-cards .c-est   { display: none; }

    /* The Seen button stays at every width — it is the panel's one action, and the whole reason
       the row list was rebuilt once already was that this button fell off a 390px screen. */
    table.tbl-review .c-seen { width: 1%; white-space: nowrap; }

    /* TWO MECHANISMS FOR ONE PROBLEM, AND ONLY ONE OF THEM AT A TIME.
       ==============================================================
       The folds put a hidden column back as a line under the description. The stacker turns the
       whole row into labelled blocks, which puts EVERY column back with its own heading. Both were
       running at 390px, so the due date and the name each appeared twice in one stacked row —
       once as a fold and again as a labelled block.

       THE STACKER WINS WHERE IT RUNS, because it is the better answer: "Due: Monday 24 August"
       reads as a field, where the fold is a loose line that has to repeat the word Due itself. The
       folds stay for the case the stacker cannot cover — no JavaScript — which is the whole reason
       they were written. */
    table.tbl-review.is-stacked .fold { display: none; }

    table.tbl-cards .fold-sev,
    table.tbl-cards .fold-since,
    table.tbl-cards .fold-due,
    table.tbl-cards .fold-when,
    table.tbl-cards .fold-who,
    table.tbl-cards .fold-est { display: block; }

    /* The folded severity is a chip on a line of its own, so it needs the room a chip wants
       rather than the line-height of the sub-text it sits among. */
    table.tbl-cards .fold-sev { margin-top: 4px; }

    /* WORDS BREAK ON WORD BOUNDARIES OR NOT AT ALL. The default here is break-word, which is right
       for a long asset code in a narrow column and catastrophic for ordinary prose — it is what
       turned "Chiller" into "Chill / er". The issue cell is prose and is treated as prose. */
    table.tbl-cards .c-issue,
    table.tbl-cards .c-needs { overflow-wrap: normal; word-break: normal; hyphens: none; }

    /* The job number is twelve fixed characters that must not break, so it takes a floor and the
       issue gives up the difference — the same trade tbl-jobs makes at 480px. */
    table.tbl-cards .c-job { width: 1%; white-space: nowrap; }
}

@media (max-width: 620px) {
    table.tbl-assets    .c-where    { display: none; }
    table.tbl-assets    .fold-where { display: block; }
    table.tbl-assets    .c-open     { display: none; }  /* also counted on the tile above */

    table.tbl-schedules .c-where    { display: none; }
    table.tbl-schedules .fold-where { display: block; }
    table.tbl-schedules .c-rhythm   { display: none; }  /* implied by the next due date */
}

/* Columns that must never wrap — references, dates, chips, action buttons. */
table.tbl td.shrink, table.tbl th.shrink {
    width: 1%;
    white-space: nowrap;
    overflow-wrap: normal;
    word-break: normal;
}

/* --- One cell vocabulary, used by every table ---------------------------- */
/* The complaint that started this: severity, status and the assignee's name were
   each set in a different face at a different size, so a row read as three
   unrelated fragments. There are now exactly two text styles in a cell — a title
   and a subtitle — and everything else is a chip. */

/* Matched to .perm-text strong — the permission labels on the same screen. A role called
   "Technician" in the table above and the permissions it holds below were 12px/550 and 13px/500,
   which put the two halves of one decision in two different sizes. */
.tbl .cell-title {
    font-weight: 500;
    color: var(--ink);
    font-size: var(--fs-sm);
    line-height: 1.5;

    /* Wraps onto a second line rather than pushing the column wider. Long-word breaking is
       kept for the odd asset code with no spaces in it. */
    white-space: normal;
    overflow-wrap: anywhere;
}

/* Bumped from 11px. It carries a description, a code, a username — real content, not a footnote,
   and at 11px it was the smallest text in the system by two steps. */
.tbl .cell-sub {
    font-weight: 400;
    color: var(--steel);
    font-size: var(--fs-sm);
    margin-top: 3px;
    line-height: 1.45;
    white-space: normal;
    overflow-wrap: anywhere;
}

.tbl .cell-date {
    font-family: var(--mono);
    font-size: var(--fs-xs);
    color: var(--steel);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* Assignee: mark plus name, on one line, matching the chips beside it. */
.tbl .person {
    display: flex;
    align-items: center;
    gap: 7px;
    min-width: 0;
}
/* Matches the issue title. Left at the larger size it outweighed the thing the row is about. */
.tbl .person-name {
    font-size: var(--fs-xs);
    color: var(--ink);
    line-height: 1.4;
    overflow-wrap: anywhere;
}

/* Where a cell can hold more than one chip, they wrap rather than stretch the column — that was
   what squashed the SLA and Issue columns.
   THE ONLY DECLARATION. There were two, ~1500 lines apart, and the later one won silently — so
   editing this rule appeared to do nothing. */
.chip-stack {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    max-width: 260px;
    align-items: flex-start;
}

/* One per line, where the chips are a list rather than a set of tags — a branch and the count of
   the others belong under each other, not run together as one long name. */
.chip-stack.is-stacked { flex-direction: column; }

/* --- Job card table proportions ------------------------------------------ */
/* Fixed layout with declared widths. Left to itself the browser gives the space
   to whichever column happens to hold the longest string, which is how Issue and
   SLA ended up squeezed behind three chips. Percentages, so it still fits any
   container width. */

/* A floor, not just proportions. Percentages of a container that has become too small still
   resolve — they just resolve to widths the content cannot fit in, and a nowrap job number then
   prints straight over the issue beside it. Below these widths the table keeps its proportions
   and .tbl-wrap scrolls instead, which is the honest failure mode for a dense table. */
table.tbl-jobs { table-layout: fixed; }

/* The widest real strings in the table, measured rather than guessed, each
   plus 28px of cell padding:
       Job no.  "JC-2026-0014"             100 + 28 = 128
       Severity "Critical"                  90 + 28 = 118
       Status   "Verification" (wrapped)    94 + 28 = 122
       Assigned "RV Ryno van Wyk"          127 + 28 = 155
       SLA      "1 day 11 hours overdue"   166 + 28 = 194
       Raised   "Wednesday 5 August 2026"  172 + 28 = 200
   Around 1400px of table, everything lands on one line and every row is the same height.

   The floor is deliberately set BELOW that, at 1180. Between the two, Assigned and Raised
   wrap to a second line and the row grows — which is a far better narrow-window failure than
   a horizontal scrollbar across the whole table, and a scrollbar is what a floor of 1260
   produced on a 1500px window. Only under 1180 does .tbl-wrap start scrolling, because below
   that the columns really cannot hold their content. */
table.tbl-jobs { min-width: 1180px; }

/* Seven columns. Each set sums to exactly 100% — with a fixed layout, anything less
   leaves the last column short and a nowrap date then spills past the table edge.
   The job number and the date are constant-width strings that must never wrap, so
   they get enough room at the narrowest place either table appears: the 1080px
   severity modal. Too little, and the reference overlaps the issue beside it. */
/* Three columns carry written-out text rather than shorthand: SLA holds the longest string
   on the row ("8 days 23 hours overdue"), and Raised holds a full weekday, date and year
   over a time. Both take their space from Status, which is the only column here allowed to
   wrap — "Awaiting Verification" sets over two lines rather than holding the column open to
   the width of the longest status in the system. That released four points, three of which
   went to Issue and the rest to the columns that cannot wrap. */
table.tbl-jobs col.c-ref    { width: 11%; }
table.tbl-jobs col.c-issue  { width: 22%; }
table.tbl-jobs col.c-sev    { width: 10%; }
table.tbl-jobs col.c-status { width: 11%; }
table.tbl-jobs col.c-person { width: 12%; }
/* THE SAME WIDTH, because they are the same shape carrying the same thing: a full date over a
   second line. They were 17 and 16, which is close enough to look like a mistake rather than a
   choice and made the two dates sit slightly out of step. */
table.tbl-jobs col.c-sla    { width: 17%; }
table.tbl-jobs col.c-date   { width: 17%; }

/* THE CHEVRON COLUMN. A fixed 40px rather than a share of the table, because it holds one glyph
   at every width and a percentage of it would grow a gutter on a wide monitor. Fixed layout scales
   the percentages above down to make room, which costs each of them under a point.

   It says the row goes somewhere BEFORE the pointer is over it — the hover states below only ever
   answered somebody who had already guessed. Same column, same heading and same glyph as the
   branch breakdown and the team workload, which is where the reader learned to expect it. */
table.tbl-jobs col.c-go { width: 44px; }

table.tbl-jobs td.cell-go {
    text-align: right;
    color: var(--steel-2);
}
table.tbl-jobs tr:hover td.cell-go { color: var(--navy-500); }

/* Slightly more air than the generic table — these rows carry two lines and a chip stack. */
table.tbl-jobs tbody td { padding: 9px 12px; }

/* Every row the same height. Letting Status wrap is what bought the column its width back,
   but a wrapped chip is 18px taller than an unwrapped one, so the two "Awaiting Verification"
   rows stood proud of the nine around them — a stepped edge for no reason the reader can see.
   A minimum sized to the two-line case absorbs it: the wrap now costs nothing, and rows only
   differ when the ISSUE runs to a second line, which is content worth the space. */
table.tbl-jobs tbody tr { height: 56px; }

/* The Planned chip sits UNDER the job number rather than beside it — see the note in
   _JobCardTable. Beside it, the two together are wider than the column at every breakpoint and the
   number would wrap, which is the one thing on the row that must not.

   flex rather than the inherited inline-flex, with a fitted width: it is what puts the chip on its
   own line without a wrapper div, and the fitted width is what stops it stretching to the column. */
table.tbl-jobs td.c-ref .chip.is-planned {
    display: flex;
    width: fit-content;
    margin-top: 5px;
}

/* --- Estimated against actual, on the person's record ----------------------- */
/* FIVE COLUMNS AND TWO OF THEM ARE HOURS, so they are given the same width and the same alignment:
   a reader compares the pair down the page rather than across the row, and two columns of different
   widths make that comparison harder than it needs to be. */
table.tbl-estimate { table-layout: fixed; min-width: 640px; }

table.tbl-estimate col.c-ref   { width: 12%; }
table.tbl-estimate col.c-issue { width: 42%; }
table.tbl-estimate col.c-hours { width: 12%; }
table.tbl-estimate col.c-gap   { width: 22%; }

table.tbl-estimate tbody td { padding: 8px 12px; vertical-align: top; }

table.tbl-estimate td.c-hours,
table.tbl-estimate th.c-hours { text-align: right; font-variant-numeric: tabular-nums; }

table.tbl-estimate td.c-gap { font-size: var(--fs-sm); white-space: normal; }
table.tbl-estimate td.c-gap.is-warn { color: var(--warn); }
table.tbl-estimate td.c-gap.is-ok   { color: var(--ok); }

@media (max-width: 900px) {
    table.tbl-estimate col.c-ref   { width: 16%; }
    table.tbl-estimate col.c-issue { width: 34%; }
    table.tbl-estimate col.c-hours { width: 14%; }
    table.tbl-estimate col.c-gap   { width: 22%; }
}

/* --- The raiser's follow-up table ------------------------------------------- */
/* FOUR COLUMNS, and the three that are missing are the point of it: who the card was given to,
   the SLA clock and the evidence all belong to the people doing and checking the work. See
   Pages/Following/FollowedTable.cs.

   Declared widths and a fixed layout, like every other table here. Where it is and Expected both
   carry sentences, so they take the room; the job number never wraps. */
table.tbl-followed { table-layout: fixed; min-width: 720px; }

table.tbl-followed col.c-ref      { width: 12%; }
table.tbl-followed col.c-issue    { width: 36%; }
table.tbl-followed col.c-standing { width: 30%; }
table.tbl-followed col.c-expected { width: 22%; }

/* The chevron, exactly as the job card list and the dashboard's summaries carry it — this row
   opens the follow-up view of the card and nothing on it said so until the pointer arrived. */
table.tbl-followed col.c-go { width: 44px; }

table.tbl-followed td.cell-go {
    text-align: right;
    color: var(--steel-2);
}
table.tbl-followed tr:hover td.cell-go { color: var(--navy-500); }

table.tbl-followed tbody td { padding: 9px 12px; vertical-align: top; }

/* "Where it is" is a sentence rather than a chip, deliberately — a raiser reads words, not the
   workflow's own vocabulary — so it needs the line height a paragraph gets. */
table.tbl-followed td.c-standing {
    font-size: var(--fs-sm);
    color: var(--steel);
    line-height: 1.45;
    white-space: normal;
}

table.tbl-followed td.c-expected .cell-sub.is-warn { color: var(--warn); }

@media (max-width: 900px) {
    table.tbl-followed .c-standing { display: none; }
    table.tbl-followed col.c-standing { width: 0; }
    table.tbl-followed col.c-ref      { width: 16%; }
    table.tbl-followed col.c-issue    { width: 51%; }
    table.tbl-followed col.c-expected { width: 33%; }
}

/* --- The week table -------------------------------------------------------- */
/* One row per DAY: the day, the job cards on it in order, and what the day adds up to.

   Declared widths and a fixed layout, like the job card table and for the same reason: left to
   itself the browser gives the space to whichever column holds the longest string, and here that
   is always the job descriptions — which would squeeze the day and the load figure, the two
   columns the screen is read for.

   The two constant-width strings are measured rather than guessed, each plus 24px of cell padding:
       Day    "Wednesday 12 August 2026"   184 + 24 = 208
       Load   "6.5 hours, 2.5 spare"       150 + 24 = 174
   The floor is set below their sum plus a usable middle column, so .tbl-wrap only scrolls on a
   window narrower than anything in the building. */

table.tbl-week { table-layout: fixed; min-width: 720px; }

table.tbl-week col.c-day   { width: 22%; }
table.tbl-week col.c-cards { width: 58%; }
table.tbl-week col.c-load  { width: 20%; }

/* The day and the load are short phrases that must stay on one line at any sensible width; the
   cards column is the one allowed to grow downwards. */
table.tbl-week td.c-day,
table.tbl-week td.c-load { white-space: normal; overflow-wrap: normal; }

/* A card line inside a cell has the table's own padding above and below it already, so the first
   and last lines do not need their own. Without this a day with one card sits noticeably taller
   than the day above it. */
table.tbl-week .ev-list { margin-top: 0; }
table.tbl-week .ev-line:first-child { padding-top: 0; }
table.tbl-week .ev-line:last-child { padding-bottom: 0; }

/* The load chip carries a figure, so its digits line up down the column the way every other
   number in this system does. */
table.tbl-week td.c-load .chip { font-variant-numeric: tabular-nums; }

/* The hours note under a job number is the second line of a cell, not a footnote — same size as
   the description above it, one step quieter. Matches .cell-sub, which it sits beside elsewhere. */
table.tbl-week .ev-line-text .cell-sub { display: block; }

/* A day that has already happened recedes. It is a record rather than something to act on, and a
   week is read forwards from today. */
table.tbl-week .ev-line.is-done .ev-line-text { color: var(--ink); }   /* it also flexes — see the rule above */

/* --- The week as a calendar ------------------------------------------------
   THE SAME WEEK AS THE TABLE ABOVE, TURNED NINETY DEGREES. My Work shows ONE person, so their week
   is read the way a wall planner is read: days across, in columns. The board stacks several people
   and keeps its rows — see _ScheduleWeek, which draws both from one model.

   THE RULES BETWEEN THE DAYS ARE THE POINT AND NOT DECORATION. The owner could read the days
   perfectly well and still could not tell where one stopped: "just make more solid lines between
   days please, its hard to follow what is where." So the separators are 2px of --line
   against the panel, full height of the grid, rather than the hairline a table draws between rows.

   ONE ROW OF EQUAL COLUMNS, SIZED BY HOW MANY DAYS THERE ARE. grid-auto-flow: column means the week
   decides the count — five on an ordinary Monday-to-Friday roster, six when somebody was called out
   on the Saturday — without the markup having to count them and tell the stylesheet.

   IT STOPS BEING COLUMNS BEFORE THEY GET TOO THIN TO READ. Five columns need about 130px each to
   hold a job number and a description; below 900px they stack into days down the page, which is the
   same fallback the contractor strip makes and the only one that works on a 390-wide phone. The
   dividing rule moves from the left edge to the top so the days stay just as plainly separated. */
.day-week {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    border: 1px solid var(--line);
    border-radius: 10px;
    overflow: hidden;
    background: var(--surface);
}

/* A whole day is one control: clicking anywhere in the column opens that day's job cards in the
   drill panel. It is a <button> and not a link deliberately — on My Work a link to a job card
   STARTS the job, and this must never do that. */
.day-col {
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-width: 0;
    padding: 10px 10px 12px;
    border: 0;
    border-left: 2px solid var(--line);
    background: var(--surface);
    font: inherit;
    color: inherit;
    text-align: left;
    cursor: pointer;
}

.day-col:first-child { border-left: 0; }

.day-col:hover { background: var(--surface-2); }
.day-col:focus-visible { outline: 2px solid var(--gold-400); outline-offset: -2px; }

/* A COLUMN WITH NOWHERE TO GO — Workload and the allocation board, where the day is context rather
   than a way in. It keeps the same shape and the same rules between days and drops the two things
   that promise a click: the pointer and the hover. The job numbers inside it are the links, exactly
   as they were when this week was drawn as rows. */
.day-col.is-static { cursor: default; }
.day-col.is-static:hover { background: var(--surface); }
.day-col.is-static.is-gone:hover { background: var(--surface-2); }
.day-col.is-static.is-off:hover { background: var(--line-2); }

/* A day that has already happened recedes; today is marked the way every other screen marks it. */
.day-col.is-gone { background: var(--surface-2); }
.day-col.is-gone:hover { background: var(--line-2); }
.day-col.is-today { box-shadow: inset 0 3px 0 0 var(--gold-500); }
.day-col.is-off { background: var(--line-2); }

.day-col-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    row-gap: 1px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--line);
}

.day-col-name {
    font-size: var(--fs-sm);
    font-weight: 700;
    color: var(--ink);
    overflow-wrap: anywhere;
}

.day-col-date, .day-col-standing {
    grid-column: 1 / -1;
    font-size: var(--fs-xs);
    color: var(--steel);
}

.day-col-standing { color: var(--steel-2); }

/* The chevron says the column goes somewhere, the way the dashboard's tiles and rows do. */
.day-col-go { color: var(--steel-2); opacity: .55; }
.day-col:hover .day-col-go, .day-col:focus-visible .day-col-go { opacity: 1; color: var(--gold-600); }

/* The cards grow downwards; the load chip is pushed to the bottom so the figures line up across
   the week however many jobs each day carries. */
.day-col-body {
    display: flex;
    flex-direction: column;
    gap: 6px;
    flex: 1 1 auto;
}

.day-col-empty { font-size: var(--fs-xs); color: var(--steel-2); }

.day-col-foot {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    font-variant-numeric: tabular-nums;
}

/* One job card on one day. Worked lines are ticked and read in full colour; expected ones are
   hollow and recede — the same vocabulary the rows layout uses, so a line means the same thing
   whichever way the week is drawn. */
.day-card {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 6px;
    align-items: start;
    font-size: var(--fs-xs);
    color: var(--steel);
}

.day-card > .bi { color: var(--steel-2); line-height: 1.5; }
.day-card.is-done > .bi { color: var(--ok); }
.day-card.is-off-roster > .bi { color: var(--info); }

.day-card-text { display: block; min-width: 0; }

/* ONE FACT PER LINE, in the order somebody reads them: which job, what is wrong, how long, and
   whether it is the one in their hands. They ran together across the card, which on a column
   narrow enough to fit five of them across meant the number and the description wrapped into each
   other and neither was findable at a glance. */
.day-card-ref {
    display: block;
    font-weight: 700;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}

.day-card-what { display: block; color: var(--ink-2); overflow-wrap: anywhere; margin-top: 1px; }
.day-card-note { display: block; color: var(--steel); margin-top: 2px; }

/* A chip inside a card line starts its own row rather than squeezing the description. */
.day-card .chip { grid-column: 2; margin-top: 3px; }

@media (max-width: 900px) {
    .day-week { grid-auto-flow: row; grid-auto-columns: auto; }

    .day-col { border-left: 0; border-top: 2px solid var(--line); }
    .day-col:first-child { border-top: 0; }
    .day-col.is-today { box-shadow: inset 3px 0 0 0 var(--gold-500); }
}

/* My Work's day list — the one list Today is now read from.

   THE HOURS NOTE IS THE SECOND LINE OF A CARD LINE, NOT A FOOTNOTE. .cell-sub is only styled
   inside .tbl, so out here it inherited the line it sat in and "2h — carried over from the day
   before" ran on from the description as though it were part of the fault. This is the rule
   .tbl-week already carries for the same markup, with the sizing .tbl .cell-sub uses, so the
   sub-line reads the same on both screens. Scoped to .is-day rather than to .ev-line-text at
   large, because the allocation board and the review lists put a whole reason in that span and
   read as one sentence deliberately. */
.ev-list.is-day .ev-line-text .cell-sub {
    display: block;
    font-weight: 400;
    color: var(--steel);
    font-size: var(--fs-sm);
    margin-top: 3px;
    line-height: 1.45;
}

/* The folded facts. Hidden while the column they duplicate is still on the row, so nothing is
   ever stated twice, and shown by the breakpoint that removed it.

   DECLARED BEFORE THE LADDER BELOW, not after it. These rules and the ones in the media queries
   carry the same weight, so whichever is written last wins — putting the base rules underneath
   would quietly undo every override in the ladder, and the give-away would be a phone laying the
   table out as though it were a desktop. */
.fold { display: none; }

/* --- Job card table: shedding columns instead of scrolling ---------------- */
/* A floor of 1180 and a scrollbar under it was the old answer, and it was the wrong one. The
   content area is the window less the 232px sidebar and its padding — about 288px — so the table
   only ever fitted above roughly 1470. Every laptop in the building was reading this table with
   the Raised column off the edge, and a tablet was seeing half of it.

   So the columns leave in a fixed order of expendability, each step re-cutting the remaining
   widths to 100% and dropping the floor with them:

       Raised     the SLA badge beside it already says how urgent this is
       Assigned   folds onto the Issue line
       Severity   the coloured stripe down the first cell says it without a column
       Status     last to go, and only on a phone

   THE BREAKPOINTS ARE WINDOW WIDTHS, BUT THE TABLE CARES ABOUT CONTENT WIDTH, and the two differ
   by the sidebar — which is 288px until 992 and nothing below it. That is why the ladder pauses
   between 1117 and 992: the sidebar leaves at 992 and hands the table back 288px, so no further
   column needs to go until the window itself is smaller than the table.

   Each floor is the previous one times the share of the columns that remain, rounded down. */

@media (max-width: 1456px) {
    table.tbl-jobs { min-width: 980px; }
    table.tbl-jobs .c-date { display: none; }
    table.tbl-jobs col.c-date { width: 0; }
    table.tbl-jobs .fold-date { display: inline; }

    table.tbl-jobs col.c-ref    { width: 13%; }
    table.tbl-jobs col.c-issue  { width: 27%; }
    table.tbl-jobs col.c-sev    { width: 12%; }
    table.tbl-jobs col.c-status { width: 13%; }
    table.tbl-jobs col.c-person { width: 14%; }
    table.tbl-jobs col.c-sla    { width: 21%; }
}

@media (max-width: 1256px) {
    table.tbl-jobs { min-width: 840px; }
    table.tbl-jobs .c-person { display: none; }
    table.tbl-jobs col.c-person { width: 0; }
    table.tbl-jobs .fold-person { display: inline; }

    table.tbl-jobs col.c-ref    { width: 15%; }
    table.tbl-jobs col.c-issue  { width: 32%; }
    table.tbl-jobs col.c-sev    { width: 14%; }
    table.tbl-jobs col.c-status { width: 15%; }
    table.tbl-jobs col.c-sla    { width: 24%; }
}

@media (max-width: 1116px) {
    table.tbl-jobs { min-width: 700px; }
    table.tbl-jobs .c-sev { display: none; }
    table.tbl-jobs col.c-sev { width: 0; }

    table.tbl-jobs col.c-ref    { width: 17%; }
    table.tbl-jobs col.c-issue  { width: 38%; }
    table.tbl-jobs col.c-status { width: 17%; }
    table.tbl-jobs col.c-sla    { width: 28%; }
}

/* Phones. Status goes, and the job number is allowed to set a size smaller — it is a constant
   twelve characters that must not wrap, and at this width those characters are the difference
   between a table that fits and one that does not. */
@media (max-width: 760px) {
    table.tbl-jobs { min-width: 480px; }
    table.tbl-jobs .c-status { display: none; }
    table.tbl-jobs col.c-status { width: 0; }

    /* The chevron goes with it. On a phone the whole row is a tap target and there is no hover to
       discover anything with, so forty pixels of arrow are forty pixels not spent on the issue. */
    table.tbl-jobs .c-go, table.tbl-jobs td.cell-go { display: none; }
    table.tbl-jobs col.c-go { width: 0; }

    table.tbl-jobs col.c-ref   { width: 24%; }
    table.tbl-jobs col.c-issue { width: 44%; }
    table.tbl-jobs col.c-sla   { width: 32%; }

    table.tbl-jobs .ref { font-size: var(--fs-xs); }
    table.tbl-jobs tbody td { padding: 8px 8px; }
}

/* The narrowest phones. 340 is measured against the real thing rather than chosen: a 390px
   handset leaves 356px inside the panel, and a floor above that is a table that scrolls sideways
   on the device most likely to be holding it. The job number sets the lower bound — it is twelve
   characters that must not break — so it takes a bigger share here than anywhere else. */
@media (max-width: 620px) {
    /* THE LAST THING HOLDING THE TABLE OPEN, once the columns are down to three. The due badge
       refuses to wrap everywhere else, and rightly so — "1 day 11 hours overdue" broken across
       two lines in the middle of a wide table looks like two separate figures. In a 114px column
       on a phone it is the opposite: nowrap makes a 172px pill that drags the whole table
       sideways, so the badge wraps and the table fits. */
    table.tbl-jobs .due { white-space: normal; }
}

@media (max-width: 480px) {
    table.tbl-jobs { min-width: 340px; }

    /* The job number takes the biggest share it has anywhere, which looks wrong written down and
       is right on the screen: "JC-2026-0014" is twelve fixed characters that must not break, so it
       is the one column here with a hard floor of its own. Issue gives up the difference because
       it is the only cell on the row that can wrap onto a third line without hurting. */
    table.tbl-jobs col.c-ref   { width: 32%; }
    table.tbl-jobs col.c-issue { width: 36%; }
    table.tbl-jobs col.c-sla   { width: 32%; }
}

/* The empty row must not be striped or padded like a data row. */
table.tbl tbody tr.is-empty:hover td { background: transparent; }
table.tbl tbody tr.is-empty td { padding: 0; }

/* --- Summary tables (By branch, Team workload) ---------------------------- */
/* A name and three figures. Left to itself the browser gave the name every spare
   pixel and pushed the numbers hard against the right edge, so the row read as two
   unrelated halves separated by a field of white. Declared widths keep the figures
   near their headings and near each other, which is the comparison being made. */

/* THE FLOOR IS 380, NOT THE 560 IT WAS, and the reason is where this table actually lives. The
   other tables here fill the content area, so a floor sized against the window is a fair guess for
   them. This one spends its life in the dashboard's SIDE column, which is about 400px wide on a
   1100px window — so a floor of 560 meant the branch breakdown scrolled sideways on the very first
   screen anybody sees, at a width where every other table on the page was comfortable.

   A floor is a minimum, not a width: above 380 the percentages below still do the work, so the
   wide case is unchanged and only the cramped one improves. Four of the five columns hold a number
   under three digits, so 380 costs nothing worse than a long branch name taking two lines. */
table.tbl-summary { table-layout: fixed; min-width: 380px; }

table.tbl-summary col.c-name { width: auto; }
table.tbl-summary col.c-num  { width: 20%; }
table.tbl-summary col.c-go   { width: 44px; }

table.tbl-summary td.cell-go {
    text-align: right;
    color: var(--steel-2);
}
table.tbl-summary tr:hover td.cell-go { color: var(--navy-500); }

/* --- A stage heading inside a list, and a group folded down to it -------- */
table.tbl tr.is-group td {
    background: var(--surface-2);
    border-top: 1px solid var(--line);
    padding-top: 8px;
    padding-bottom: 8px;
}
table.tbl tr.is-group .btn { margin-left: 8px; }
table.tbl tbody.is-folded tr:not(.is-group) { display: none; }

/* --- Figures in a summary column ------------------------------------------ */
/* A count used to be a chip when it mattered and bare text when it was zero. The chip
   carries ~10px of side padding and the bare text carries none, so the digits landed on
   two different left edges and the column zig-zagged down the page.

   Every figure now gets the same box whatever its value; only the colour changes. A zero
   still recedes — it just recedes in place. */

.count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 30px;
    padding: 3px 9px;
    border-radius: 20px;
    font-size: var(--fs-xs);
    font-weight: 550;
    line-height: 1.5;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Nothing to report: no ground, no colour, but the same footprint. */
.count-zero  { background: transparent; color: var(--steel-2); font-weight: 400; }

.count-plain { background: var(--surface-2); color: var(--ink); }
.count-bad   { background: var(--bad-bg);  color: var(--bad); }
.count-warn  { background: var(--warn-bg); color: var(--warn); }

/* A totals figure. No ground and no colour — a total is not something you act on — but it has
   to keep the SAME BOX as the chips above it, or its digit starts at the cell edge while
   theirs sits inset by the chip's padding, and the column steps left on its last row. */
.count-bare {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 30px;
    padding: 3px 9px;
    line-height: 1.5;
    font-variant-numeric: tabular-nums;
    background: transparent;
    color: inherit;
}

/* --- Rows that drill one level deeper ------------------------------------- */
/* Used by branch rows on the dashboard and by every row inside the modal list. */

tr.is-drillable { cursor: pointer; }
tr.is-drillable:hover td { background: var(--surface-2); }
tr.is-drillable:focus-visible {
    outline: 2px solid var(--gold-400);
    outline-offset: -2px;
}

/* The reference reads as a label rather than a link when the whole row is the target. */
tr.is-drillable .ref { background: var(--surface-2); }
tr.is-drillable:hover .ref { background: var(--gold-100); border-color: var(--gold-400); color: var(--gold-600); }

/* --- A plain table whose rows are job cards -------------------------------- */
/* The asset's service history and the cards a person has completed. Both already opened their
   card from the row and neither said so; this is the same chevron the dashboard's summaries and
   the job card list carry, in a table that has no colgroup to declare it in.

   SCOPED, NOT GENERIC. cell-go is also the attachments list's action cell, which holds a button
   and a removal form — right-aligning that to forty pixels would wreck it. */

/* ---------- A dialog laid out in columns, not in one flex line ---------------------------------
   The owner, on the time-off dialog: "need to fix this modal view to be way bigger, and for the
   text not to run off, and the layout to be way nicer and easier to follow, keep columns etc."

   A `.row tight` is a flex line: every field on it, shrinking until they stop fitting and then
   overflowing. That is right for two or three controls and wrong for eight — which is how a
   paragraph explaining whole days ended up cut off mid-sentence at the right-hand edge.

   A GRID INSTEAD, so the columns are a promise. Each field takes a whole track, the tracks wrap
   onto a second line when the panel is narrow, and nothing is ever pushed off the side. */
/* ONE COLUMN. The owner: "put it under each other, and no bottom scrool".

   THE COLUMNS WERE THE PROBLEM, NOT THE CURE. `repeat(auto-fit, minmax(230px, 1fr))` with one
   field spanning two tracks resolved to `380px 380px 380px 0px` — a phantom fourth track, created
   only because something had to span into it. A track that exists to be spanned into is a width
   the container never agreed to, and it is where the sideways scrollbar came from.

   A form of eight controls read across three columns is also simply harder to follow than the same
   eight read down. There is no reading order in a grid; there is one in a stack. */
.dlg-grid {
    display: flex;
    flex-direction: column;
    gap: 16px;
    margin-bottom: 18px;

    /* A flex item will not shrink below its content by default, which is the other half of how a
       dialog grows a scrollbar. */
    min-width: 0;
}

.dlg-grid .field { margin: 0; min-width: 0; }

/* `.modal-form .field + .field` adds 15px to every field after the first — three classes, so it
   beats a two-class rule. The gap above does the spacing now, and evenly. */
.modal-form .dlg-grid .field + .field { margin-top: 0; }

/* The explanation under a control is prose and keeps a readable measure rather than running the
   width of a 980px dialog. */
.dlg-grid .field.is-explain { max-width: 62ch; }

/* A CONTROL IS AS WIDE AS WHAT GOES IN IT. Stacked, every box stretched the full width of the
   dialog, so a date read as a 930px field with eight characters in it and the eye had to travel
   the whole way to find the next one. A date is a date wide; a note is a sentence wide. */
.dlg-grid .inp[type="date"],
.dlg-grid .inp.n            { max-width: 220px; }
.dlg-grid .sel              { max-width: 300px; }
.dlg-grid .inp              { max-width: 520px; }

/* A tick box and its words on one baseline, at the height of the inputs beside it, so a row of
   three fields does not have one of them floating a few pixels high. */
.check-line {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 38px;
    cursor: pointer;
}

.check-line input[type="checkbox"] { width: 16px; height: 16px; flex: none; cursor: pointer; }

/* The note inside a dialog is guidance, not an alert: it explains the box beside it and should
   not shout over the form it belongs to.

   BLOCK, BECAUSE .note IS A FLEX ROW. It is built for an icon beside a line of text, so three
   paragraphs inside one became three columns of narrow text read left to right — which is not how
   anybody reads an explanation of two alternatives. */
.dlg-grid .note { display: block; margin: 0; }
.dlg-grid .note p { margin: 0 0 6px; }
.dlg-grid .note p:last-child { margin-bottom: 0; }

/* Colleagues wrap into as many columns as fit rather than running off in one line. */
.dlg-people { border: 0; padding: 0; margin: 0 0 4px; }

.pick-people {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 4px 14px;
}

/* --- Time on this job: shares on a desk, lines on a phone -------------------------------------
   Four figures at 110px and a badge at 170 came to 610px of fixed furniture, with the person's
   name left to absorb whatever the panel had left. In a 356px panel that is 250px more than there
   is, so the name broke a word to a line. Shares divide what there is; pixels demand what there
   might not be. */
table.tbl-time            { table-layout: fixed; width: 100%; }
table.tbl-time .c-person  { width: 34%; }
table.tbl-time .c-alloc,
table.tbl-time .c-worked,
table.tbl-time .c-rework,
table.tbl-time .c-over    { width: 13%; }
table.tbl-time .c-verdict { width: 14%; }

/* FOUR FIGURES AND A BADGE CANNOT BE READ ACROSS A PHONE, and there is no width at which they
   could be. They become lines under the name, where the sub-lines about somebody's day already
   are — the same fold the job card lists use. */
@media (max-width: 820px) {
    table.tbl-time .c-alloc,
    table.tbl-time .c-worked,
    table.tbl-time .c-rework,
    table.tbl-time .c-over,
    table.tbl-time .c-verdict { display: none; }

    table.tbl-time .fold-figures,
    table.tbl-time .fold-worked  { display: block; }
    table.tbl-time .fold-verdict { display: inline-block; }

    table.tbl-time .c-person { width: auto; }
}

/* --- Today's list: the stage column earns its width and no more ---------------------------
   The owner: "also give it less space, less column width! and bring the severity closer!"

   Nothing sized these on a desk, so table-layout: auto gave the stage column room in proportion
   to its longest sentence — "Waiting on Compliance to release it" — and pushed the severity chip
   halfway across the screen, away from the description it qualifies.

   FIXED WIDTHS, SO THE THREE SHORT COLUMNS STOP BORROWING FROM THE PROSE. Stage, severity and the
   two dates are all short and predictable; the issue is prose and takes whatever is left, which is
   the column that can actually use it. */
/* PERCENTAGES, NOT rem, AND THAT IS NOT A STYLE PREFERENCE. Fixed widths add up to a number: six
   columns at their comfortable size come to about 800px, which is more than the table has on a
   1024px laptop once the sidebar is taken off — so the prose column would be squeezed to nothing
   and the table would push past its own container. Shares divide whatever there is, at every
   width, which is the same reason the phone rules below are written in percentages. */
/* --- A person's open work: six columns, all of them named -------------------------------------
   THREE ARRANGEMENTS OF THE SAME TABLE, AND ONLY THE THIRD IS RIGHT.

   Fixed for every .tbl-cards — which I applied for the Today list and should not have — gave this
   table equal sixths, and a nowrap badge reading "13 days 20 hours overdue" ran straight over the
   estimate beside it. That is what the owner saw.

   Auto layout fixed the overlap and starved the only column anybody reads: the job number took
   its full width, the due badge took its full width, and the issue took what was left.

   Shares divide the room deliberately. */
@media (min-width: 821px) {
    table.tbl-openwork .c-job    { width: 12%; }
    table.tbl-openwork .c-issue  { width: 26%; }
    table.tbl-openwork .c-sev    { width: 10%; }

    /* The status wraps now, so it needs room for its longest WORD rather than its longest phrase;
       the due badge carries a whole sentence and needs the room that buys. */
    table.tbl-openwork .c-status { width: 16%; }
    table.tbl-openwork .c-due    { width: 27%; }
    table.tbl-openwork .c-est    { width: 9%; }
}

/* AND NEITHER BADGE CAN OVERFLOW ITS SHARE, WHATEVER THE SHARE TURNS OUT TO BE.
   A width is a guess about the longest word. "Awaiting Sign-Off" is nowrap and wider than fifteen
   per cent, so it ran over the due badge; widen it and some longer status does the same thing at
   some other size. A chip that MAY wrap cannot overflow at any width, which is a promise rather
   than an estimate. */
table.tbl-openwork .c-status .chip,
table.tbl-openwork .c-due .due { white-space: normal; text-align: left; }

/* A JOB NUMBER IN A CARD LIST STAYS ON ONE LINE. The same rule the outbox already states for the
   same reason: "a code split across two is one nobody can read back". Auto layout gives the column
   its width from this, so a twelve-character code takes twelve characters' worth and the prose
   beside it takes the rest — rather than the code wrapping to three lines to save forty pixels. */
table.tbl-cards td .mono { white-space: nowrap; }

/*  .is-sized SAYS THE COLUMNS HAVE BEEN NAMED, AND ONLY THOSE ARE FIXED.
    ======================================================================
    This block was written for the Today list and applied to every .tbl-cards, which was a
    regression I caused and the owner found: "on users, under workload, the due and est. are
    overlapping now".

    table-layout: fixed divides the width between the columns whether or not anybody has said
    what they should be. A table with named columns gets the shares below; a table WITHOUT them
    got equal sixths — and a nowrap badge reading "6 days 23 hours overdue" is wider than a
    sixth, so it ran straight over the cell beside it. Auto layout had been sizing those columns
    to their content perfectly well.

    So the class is the promise: put .is-sized on a table and its columns are named and shared.
    Leave it off and the table sizes itself the way it always did. */
@media (min-width: 821px) {
    table.tbl-cards.is-sized  { table-layout: fixed; width: 100%; }
    table.tbl-cards .c-job    { width: 12%; }

    /* The issue is prose and takes the largest share: it is the column somebody reads. */
    table.tbl-cards .c-issue  { width: 30%; }
    table.tbl-cards .c-needs  { width: 14%; }

    /* The chip is one short word and must not be broken across two lines to save a few pixels. */
    table.tbl-cards .c-sev    { width: 10%; white-space: nowrap; }
    table.tbl-cards .c-since  { width: 16%; }
    table.tbl-cards .c-due    { width: 15%; }
    table.tbl-cards .c-go     { width: 3%; }

    /* The dashboard's "closed too quickly" uses the same table with three columns of its own. */
    table.tbl-cards .c-when   { width: 16%; }
    table.tbl-cards .c-who    { width: 15%; }
    table.tbl-cards .c-est    { width: 19%; }

    /* THE MORNING REVIEW'S OWN COLUMN — the Seen button, which is a control and not a figure.
       It takes what the button needs and nothing more, so the description keeps the rest. */
    table.tbl-review .c-seen  { width: 9%; }
}

table.tbl-cards td.cell-go {
    width: 1%;
    text-align: right;
    color: var(--steel-2);
}
table.tbl-cards tr:hover td.cell-go { color: var(--navy-500); }

/* THE VERDICT SITS UNDER THE DAY IT IS ABOUT, always — the owner asked for it, and left to itself
   it only fell there when the column happened to be too narrow to hold both. A chip that is beside
   the date on one row and under it on the next is a column the eye has to re-read on every line. */
table.tbl-review td.c-when .chip { display: block; width: fit-content; margin-top: 4px; }

/* THE REVIEW'S ROWS. The description is prose and wraps inside its share; the sub-line under it is
   the sentence that used to run the width of the panel. */
table.tbl-review td { vertical-align: top; }
table.tbl-review td.c-seen { text-align: right; }
table.tbl-review td.c-seen form { display: inline; }
/* SPAN, NOT EVERY .cell-sub IN THE ROW. The folded due date and name are DIVS carrying
   .cell-sub as well, and "table.tbl-review .cell-sub" outranks the plain ".fold { display: none }"
   that hides them — so this rule, written for the sentence, printed both folds on the desktop
   beside the very columns they exist to replace. */
table.tbl-review span.cell-sub { display: block; margin-top: 3px; }

/* --- Rows that open the job card ------------------------------------------ */
/* On a full page the row is not a drill-in, it is a link to the card. The job number was the
   only clickable thing on it — a 100px target in a 1300px row — so the row read as inert and
   people clicked the description and got nothing. The row now carries the click (see _Layout),
   and this is what says so before they try it. */
tr.is-openable { cursor: pointer; }
tr.is-openable:hover td { background: var(--surface-2); }
tr.is-openable:hover .ref { background: var(--gold-100); border-color: var(--gold-400); color: var(--gold-600); }

.sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* Severity as a left stripe on the row — urgency reads before any text. */
tr.sev-row > td:first-child { position: relative; padding-left: 17px; }
tr.sev-row > td:first-child::before {
    content: "";
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 3px;
}
tr.sev-row.s-critical > td:first-child::before { background: var(--sev-critical); }
tr.sev-row.s-high     > td:first-child::before { background: var(--sev-high); }
tr.sev-row.s-medium   > td:first-child::before { background: var(--sev-medium); }
tr.sev-row.s-low      > td:first-child::before { background: var(--sev-low); }

/* ---------------------------------------------------------------- chips --- */

/* ---------- A moment, over two lines ----------
   Day on top, time beneath. Written out in full a moment is a long line, and in a facts
   list it pushed the label and the value so far apart they stopped reading as a pair.
   Same shape as the Raised column, so a date looks the same wherever it is a fact. */
.stamp {
    display: inline-flex;
    flex-direction: column;
    line-height: 1.4;
}
.stamp-day {
    font-weight: 550;
    color: var(--ink);
}
.stamp-time {
    font-size: var(--fs-xs);
    color: var(--steel-2);
    font-variant-numeric: tabular-nums;
}

/* A quiet qualifier on the fact it sits with — "Measured in working hours". */
.stamp-note {
    display: block;
    margin-top: 3px;
    font-size: var(--fs-xs);
    color: var(--steel-2);
}

/* The SLA badge and the clock it was measured on, side by side under "Where it stands".
   Baseline-aligned rather than centred: the note is a comment on the figure, so its text
   should sit on the same line as the words inside the badge, not float against its box. */
.cd-clock {
    display: flex;
    align-items: baseline;
    gap: 10px;
    flex-wrap: wrap;
}
.cd-clock .stamp-note { margin-top: 0; }

/* ONE chip. Severity and status used to be two different components — a mono
   uppercase pill against a sentence-case one — which is why a row never looked
   settled. They are now the same object in different colours; severity simply
   carries a dot as well. */

.chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 3px 10px;
    border-radius: 20px;
    font-size: var(--fs-xs);
    font-weight: 550;
    letter-spacing: .01em;
    white-space: nowrap;
    line-height: 1.5;
    max-width: 100%;
    border: 1px solid transparent;
}
.chip i { font-size: .85em; flex-shrink: 0; }

/* A chip that may set over two lines. Only the status column asks for this: holding a
   column open to the width of "Awaiting Verification" costs four points of table width
   for a string that reads perfectly well wrapped. The radius drops to a rounded rectangle
   because a two-line stadium looks like a mistake, and the text centres so the short
   second line sits under the middle of the first. */
/* Left aligned, like every single-line chip beside it. Centred, a two-line status read as a
   different kind of thing from the one-line ones in the rows above and below it — the column
   lost its left edge exactly where the eye follows it down. */
.chip-wrap {
    white-space: normal;
    text-align: left;
    justify-content: flex-start;
    border-radius: 12px;

    /* IT WRAPS BETWEEN WORDS AND NEVER INSIDE ONE. Letting the line break is the whole point of
       this variant; letting the WORD break is not, and in a narrow column it turned a status of
       "Rework" into "Rew / ork" on the asset failure history. A chip carries a term somebody has
       to read back — the same reason a job number stays whole. */
    overflow-wrap: normal;
    word-break: normal;
    hyphens: none;
}

/* The severity dot. An explicit element rather than ::before so a chip can carry
   an icon instead without the two colliding. */
.chip .dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
    flex-shrink: 0;
}

/* Neutral is the only chip with a visible edge — it has the least contrast against the row
   and would otherwise float. The border is declared transparent on .chip itself rather than
   added here, so every chip keeps the SAME BOX: without that, a Rework chip stood 2px shorter
   than the Approved chip directly beneath it in the same column. */
.chip-neutral  { background: var(--surface-2); color: var(--steel); border-color: var(--line); }
.chip-ok       { background: var(--ok-bg);   color: var(--ok); }
.chip-warn     { background: var(--warn-bg); color: var(--warn); }
.chip-bad      { background: var(--bad-bg);  color: var(--bad); }
.chip-info     { background: var(--info-bg); color: var(--info); }

/* SEVERITY IS CARRIED BY ITS DOT, NOT BY THE WHOLE CHIP.
   Every column on a job card row used to be tinted — severity, status, the assignee's mark and
   the due state all competing — and when everything is coloured nothing reads as urgent. The
   row already carries a severity stripe down its left edge, so the chip only needs to name the
   band; the dot keeps the hue at a size that informs without shouting. */
.chip[class*="sev-"] {
    background: var(--surface-2);
    color: var(--ink);
    border: 1px solid var(--line);
    font-weight: 500;
}

.chip.sev-critical .dot { background: var(--sev-critical); }
.chip.sev-high     .dot { background: var(--sev-high); }
.chip.sev-medium   .dot { background: var(--sev-medium); }
.chip.sev-low      .dot { background: var(--sev-low); }

/* The standalone .sev pill (maintenance setup, people screens) keeps its full colour — it is
   the subject of those screens rather than one column among six. */
.sev-critical { background: var(--sev-critical-bg); color: var(--sev-critical); }
.sev-high     { background: var(--sev-high-bg);     color: var(--sev-high); }
.sev-medium   { background: var(--sev-medium-bg);   color: var(--sev-medium); }
.sev-low      { background: var(--sev-low-bg);      color: var(--sev-low); }

/* Retained for the maintenance and people screens, which show a severity or a
   competency on its own rather than in a table row. */
.sev {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 3px 10px;
    border-radius: 20px;
    font-size: var(--fs-xs);
    font-weight: 550;
    white-space: nowrap;
}
.sev::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }

.ref {
    font-family: var(--mono);
    font-size: var(--fs-xs);
    color: var(--navy-600);
    background: var(--surface-2);
    border: 1px solid var(--line);
    padding: 1px 6px;
    border-radius: var(--r-xs);
    white-space: nowrap;
}
a.ref:hover { background: var(--gold-100); border-color: var(--gold-400); color: var(--gold-600); text-decoration: none; }

/* ------------------------------------------------------------ due badge --- */
/* How much time is left, as one phrase.

   This replaced a progress bar with a percentage. A percentage of an SLA window answers a
   question nobody asks: "98% elapsed" and "1,1h left" are the same fact, but only one of them
   tells a supervisor whether they can get to it before lunch — and 98% of a week and 98% of an
   hour looked identical while meaning completely different things.

   Three states worth distinguishing, which is what the industry settles on: breached, close
   enough to act on, and fine. Colour is never the only signal — an icon and the words carry it
   for a printed worklist, a colour-blind reader, or a screen seen across a plant room. */

.due {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 10px 3px 8px;
    border-radius: 20px;
    font-size: var(--fs-xs);
    font-weight: 550;
    line-height: 1.5;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
    border: 1px solid transparent;
}
.due i { font-size: .9em; }

/* The badge is a pill around a short phrase, and inline-flex says so — but dropped straight
   into .stack it becomes a flex item, and a flex item stretches to the cross axis by default.
   That turned "1 day 11 hours overdue" into a red bar the full width of the panel, which reads
   as a page-level alarm rather than a label on a figure.
   Scoped to .stack on purpose: a blanket align-self on .due would override the baseline
   alignment .cd-clock sets to sit the badge beside its "measured in working hours" note. */
.stack > .due { align-self: flex-start; }

/* A DUE BADGE CARRYING A SENTENCE RATHER THAN A PHRASE.
   =====================================================
   .due is a pill built for "1 day 11 hours overdue" and is nowrap so a figure never breaks across
   two lines. The allocation board puts the whole consequence in one — "It would not be finished
   until Monday 31 August 2026, which is past its target" — which is 467px that cannot break, in a
   356px panel that clips for its rounded corners.

   The same distinction .chip-wrap draws, for the same reason: a word does not break, a sentence
   must. */
.due-wrap { white-space: normal; text-align: left; }

/* Past its target. The only state that gets a border as well as a fill, because it is the
   only one that means somebody has to do something now. */
.due-breached {
    background: var(--bad-bg);
    color: var(--bad);
    border-color: var(--bad);
}

/* Inside the last quarter of the window — still fine, but worth planning. */
.due-close { background: var(--warn-bg); color: var(--warn); }

/* Comfortable. */
.due-ok { background: var(--ok-bg); color: var(--ok); }

/* Finished in time. Recedes: a closed card is not asking for attention. */
.due-done { background: var(--surface-2); color: var(--steel); }

/* NO DAY SET — nobody has placed this work, so there is no deadline to be measured against.

   IT IS NOT A FAULT AND MUST NOT LOOK LIKE ONE. There is exactly one deadline in this system, the
   computed end of the work a manager placed, so a card nobody has placed has none at all — that is
   the honest state of every card between being raised and being given a day, and it is now the
   state of the open cards that were carrying a date the retired severity bands left behind. Red
   would say somebody is late for a promise nobody made.

   NEITHER IS IT NOTHING. It had no rule at all until these cards became common, so the badge drew
   as an unstyled pill — the one state on the screen with no colour behind it, which reads as a
   rendering fault rather than as a card waiting for a day. Informational blue says what it is:
   something to do, not something gone wrong. The icon beside it is a calendar with a plus on it.

   AND THE CLASS IS BUILT AT RUNTIME — "due-@Model.State" in _DueBadge.cshtml — so no view names
   this selector and no search for it will find one. See ACardWithNoDeadlineTests, which is why the
   rule survived a sweep for CSS nothing uses. */
.due-unplaced { background: var(--info-bg); color: var(--info); }

/* -------------------------------------------------------------- buttons --- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 5px 12px;
    border-radius: var(--r-sm);
    border: 1px solid transparent;
    font-family: inherit;
    font-size: var(--fs-sm);
    font-weight: 550;
    line-height: 1.4;
    cursor: pointer;
    white-space: nowrap;
    transition: background .14s var(--ease), border-color .14s var(--ease), color .14s var(--ease);
    text-decoration: none;
}
.btn:hover { text-decoration: none; }
.btn:disabled, .btn.disabled { opacity: .5; cursor: not-allowed; pointer-events: none; }
.btn i { font-size: .95em; }

.btn-primary { background: var(--navy-600); color: #fff; }
.btn-primary:hover { background: var(--navy-500); color: #fff; }

.btn-gold { background: var(--gold-500); color: var(--navy-900); }
.btn-gold:hover { background: var(--gold-400); color: var(--navy-900); }

.btn-ghost { background: var(--surface); border-color: var(--line); color: var(--ink-2); }
.btn-ghost:hover { background: var(--surface-2); border-color: var(--steel-2); color: var(--ink); }

.btn-danger { background: var(--bad); color: #fff; }
.btn-danger:hover { background: #91201a; color: #fff; }

.btn-ok { background: var(--ok); color: #fff; }
.btn-ok:hover { background: #0b5340; color: #fff; }

.btn-sm { padding: 5px 12px; font-size: var(--fs-sm); }  /* ONE SIZE. The owner, twice: "make all buttons the same sizes". */
.btn-lg { padding: 5px 12px; font-size: var(--fs-sm); }
.btn-block { width: 100%; }

/* ---------------------------------------------------------------- forms --- */

.field { display: flex; flex-direction: column; gap: 5px; }

/* Same size and weight as .set-row's label on System Settings. They were 12px/550 here and
   13px/500 there, so the same question asked in a dialog and asked on a settings row looked
   like two different kinds of label. */
.field > label, .lbl {
    font-size: var(--fs-sm);
    font-weight: 500;
    color: var(--ink-2);
}
.field .help { font-size: var(--fs-xs); color: var(--steel-2); line-height: 1.45; }

/* "— optional" beside a label, in the label's own line but not in its voice. The demo writes it
   this way on the raise screen: the field name is the instruction, and whether it is compulsory is
   a quieter fact about it. */
.lbl-soft { font-weight: 400; color: var(--steel); }
.field .err  { font-size: var(--fs-xs); color: var(--bad); font-weight: 500; }

.inp, .sel, .txt {
    width: 100%;
    padding: 5px 10px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface);
    font-family: inherit;
    font-size: var(--fs-base);
    color: var(--ink);
    transition: border-color .14s var(--ease), box-shadow .14s var(--ease);
}
.inp:hover, .sel:hover, .txt:hover { border-color: var(--steel-2); }
.inp:focus, .sel:focus, .txt:focus {
    outline: none;
    border-color: var(--navy-400);
    box-shadow: 0 0 0 3px rgba(51, 80, 143, .13);
}
.inp[readonly], .inp:disabled, .sel:disabled { background: var(--surface-2); color: var(--steel); cursor: not-allowed; }
.txt { min-height: 84px; resize: vertical; line-height: 1.55; }
.inp.n { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* Hours and minutes — the one way a duration is typed. See _Hm. */
.hm { display: inline-flex; align-items: center; gap: 4px; }
.hm .inp { width: 4.6em; min-width: 0; text-align: right; -moz-appearance: textfield; appearance: textfield; }
/* No up-and-down arrows on the boxes — the owner: "just have the h and m". */
.hm .inp::-webkit-outer-spin-button, .hm .inp::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.hm .sel { width: auto; margin-right: 4px; }
.hm-unit { color: var(--steel); font-size: var(--fs-sm); margin-right: 4px; }
.assign-day-field .hm .inp { min-height: 31px; padding: 5px 8px; }
.sel { appearance: none; padding-right: 30px;
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%235b6f8b'%3E%3Cpath d='M4.5 6.5 8 10l3.5-3.5z'/%3E%3C/svg%3E");
    background-repeat: no-repeat; background-position: right 9px center; background-size: 15px;
}

.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 15px; }
.form-grid .span-2 { grid-column: 1 / -1; }

/* A fixed column count where the number of fields makes auto-fit wrap badly — five fields in a
   four-column grid strands the fifth on a row of its own. Falls back to auto below the floor. */
@media (min-width: 900px) {
    .form-grid.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .form-grid.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* ---------- Sections WITHIN one form ----------

   For a form that is filled in once, top to bottom, rather than a screen of settings that are
   maintained separately. Raising a job card is one task: a card per question meant four navy
   headers, four borders and four bodies for what is really one page of answers.

   A section heading is a rule and a line of text — enough to group the fields under it, not
   enough to read as a separate card. */

.form-sections { display: flex; flex-direction: column; }

/* Deliberately identical to .perm-group-head h3 on the Permissions screen, which had already
   solved this — grouped questions inside one card, under a ruled heading. Same size, weight and
   rule, so a section heading is one thing in this system rather than two that nearly match. */
.form-section {
    font-size: var(--fs-sm);
    font-weight: 650;
    color: var(--ink);
    letter-spacing: .01em;
    padding-bottom: 8px;
    margin: 26px 0 14px;
    border-bottom: 1px solid var(--line);
}

/* The first one opens the form rather than dividing it, so it does not need the space above. */
.form-sections > .form-section:first-child { margin-top: 0; }

/* A heading that carries something on the right — a status chip, a count. Same as the Permissions
   group head, which puts a "critical" chip beside its title the same way. */
.form-section.is-row { display: flex; align-items: center; gap: 10px; }
.form-section.is-row .push { flex: 1 1 auto; }

.check { display: flex; align-items: flex-start; gap: 8px; cursor: pointer; }
.check input[type="checkbox"], .check input[type="radio"] {
    width: 15px; height: 15px; flex-shrink: 0;
    accent-color: var(--navy-600); cursor: pointer;

    /* THE BROWSER'S OWN MARGIN IS THE FIRST THING TO GO, and it is why this looked wrong for so
       long. A checkbox ships with margin: 3px 3px 3px 4px from the user agent. Only the top and
       bottom were ever reset here, so every .check in the system was drawing the gap it asked for
       PLUS 3px on the right and starting 4px in from the label's edge — the owner: "its to far".
       The gap belongs to the flex container and nowhere else, so all four sides are cleared and
       the top is set deliberately below.

       ON THE SAME LINE AS THE LABEL. Half the difference between the line box and the control is
       where the maths puts it. A previous attempt added 2px on top of that, on the reasoning that
       the weight of a word sits below its own centre — but 2px is more than the whole discrepancy
       it was correcting, and it left the box sitting under the text instead: "the text is higher
       than the tick box".

       Worked through rather than nudged: at 13px on a 18.85px line box the cap-to-baseline centre
       lands about 8.1px down and the lowercase mass about 9.3px, so a 15px box wants its top
       between roughly 0.6px and 1.8px. The mathematical figure, 1.925px, sits at the top of that
       range and is the one number here that is not somebody's guess.

       align-items on the parent stays flex-start deliberately: a .check whose label carries a
       <small> underneath is two lines tall, and the box belongs beside the FIRST line rather than
       centred on the pair. */
    margin: calc((var(--fs-sm) * 1.45 - 15px) / 2) 0 0 0;
}
.check span { font-size: var(--fs-sm); line-height: 1.45; }
.check small { display: block; color: var(--steel-2); font-size: var(--fs-xs); }
.check input:disabled { cursor: not-allowed; }
.check input:disabled + span { color: var(--steel-2); }

/* ---------------------------------------------------------------- alerts -- */

.note {
    display: flex;
    gap: 11px;
    align-items: flex-start;
    padding: 12px 15px;
    border-radius: var(--r-md);
    font-size: var(--fs-sm);
    line-height: 1.5;
    border: 1px solid;
    margin-bottom: 16px;
}
.note i { font-size: 1rem; flex-shrink: 0; margin-top: 1px; }
.note strong { font-weight: 600; }
.note-info { background: var(--info-bg); border-color: rgba(14,109,132,.25); color: #0a4e5f; }
.note-warn { background: var(--warn-bg); border-color: rgba(180,83,9,.25);  color: #8a3f07; }
.note-bad  { background: var(--bad-bg);  border-color: rgba(179,38,30,.25); color: #8a1d17; }
.note-ok   { background: var(--ok-bg);   border-color: rgba(15,107,82,.25); color: #0b5340; }

/* ------------------------------------------------------------- timeline --- */

/* The connector is drawn PER ITEM, not once down the container.
   It used to be a single absolutely-positioned rule on .timeline sized with top/bottom.
   Inside a scrolling box that resolves against the container's visible height, not its
   scrollable content — so the line covered the first screenful and then stopped dead
   halfway down the list. A segment per item scrolls with the content and cannot break. */
.timeline { padding-left: 24px; }

/* Each entry is a band with a faint rule under it.
   The date sits hard right, a long way from the step it belongs to, and with nothing between
   the rows the eye lost the line on the way across. The rule is pulled left past the dot so it
   spans the whole panel — a divider that stopped short of the markers would just be another
   thing to track. */
.tl-item {
    position: relative;
    padding-bottom: 14px;
    margin-bottom: 14px;
    margin-left: -24px;      /* out to the panel edge... */
    padding-left: 24px;      /* ...while the content stays indented past the markers */
    border-bottom: 1px solid var(--line-2);
}
.tl-item:last-child {
    padding-bottom: 0;
    margin-bottom: 0;
    border-bottom: none;
}

.tl-item::after {
    content: "";
    position: absolute;
    left: 6px;            /* centred under the 10px dot */
    top: 15px;            /* starts just below the dot */
    bottom: -20px;        /* across the rule and into the next dot, so there is no seam */
    width: 2px;
    background: var(--line);
}
.tl-item:last-child::after { display: none; }
/* The marker. Positioned from the item's own left edge, which the negative margin above pulled
   out to the panel edge — so the dot, the connector and the rule all share one origin. */
.tl-item::before {
    content: ""; position: absolute; left: 2px; top: 5px;
    width: 10px; height: 10px; border-radius: 50%;
    background: var(--surface); border: 2px solid var(--steel-2);
}
.tl-item.is-ok::before   { background: var(--ok);   border-color: var(--ok); }
.tl-item.is-bad::before  { background: var(--bad);  border-color: var(--bad); }
.tl-item.is-warn::before { background: var(--warn); border-color: var(--warn); }
.tl-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.tl-head strong { font-size: var(--fs-sm); font-weight: 600; }

/* The action that caused the step, subordinate to the state it produced. The old markup put
   "Pending Approval → Approved" here in mono and the arrow shouted louder than either state. */
.tl-action {
    font-size: var(--fs-2xs);
    color: var(--steel-2);
    background: var(--surface-2);
    border: 1px solid var(--line);
    border-radius: 20px;
    padding: 1px 8px;
}

/* An entry that changed no state is a note against the current one, not a step of its own. */
.tl-item.is-note > .tl-head strong { font-weight: 500; color: var(--steel); }
.tl-item.is-note::before { width: 7px; height: 7px; left: 3.5px; top: 7px; }

/* Where it goes next. Open marker, so it reads as not-yet-happened. */
.tl-item.tl-next::before {
    background: var(--surface);
    border-style: dashed;
    border-color: var(--navy-400);
}
.tl-item.tl-next .tl-head strong { color: var(--navy-600); }
.tl-item.tl-next .tl-body { color: var(--steel); }
.tl-when { font-family: var(--mono); font-size: var(--fs-2xs); color: var(--steel-2); margin-left: auto; }
.tl-body { font-size: var(--fs-sm); color: var(--steel); margin-top: 2px; }

/* --------------------------------------------------------------- detail --- */

.dl { display: grid; grid-template-columns: minmax(120px, auto) 1fr; gap: 9px 16px; font-size: var(--fs-sm); }
.dl dt { color: var(--steel); font-size: var(--fs-xs); font-weight: 500; padding-top: 1px; }
.dl dd { margin: 0; font-weight: 450; }

/* ---------------------------------------------------------------- misc ---- */

.empty { text-align: center; padding: 46px 24px; color: var(--steel-2); }
.empty i { font-size: 2.1rem; display: block; margin-bottom: 10px; opacity: .45; }
.empty p { font-size: var(--fs-sm); }

/* One line, for a small panel that happens to be empty. The full-height version is right in a
   grid, where the space belongs to rows that are not there yet; in a panel that holds a row of
   chips when it is full, it opens a hole three times the height of the panel beside it. */
.empty.is-line {
    display: flex;
    align-items: center;
    gap: 8px;
    text-align: left;
    padding: 0;
}
.empty.is-line i { font-size: 1rem; display: inline; margin: 0; }
.empty.is-line p { margin: 0; }

/* Tabs WRAP, they never scroll. A horizontal scrollbar on a tab strip hides the tabs past the
   edge — you cannot see what you cannot see, so a whole section of a screen goes missing. */
.tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 2px;
    border-bottom: 1px solid var(--line);
    margin-bottom: 18px;
}
.tab {
    padding: 7px 13px;
    border: none;
    background: transparent;
    font-family: inherit;
    font-size: var(--fs-sm);
    font-weight: 500;
    color: var(--steel);
    cursor: pointer;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    gap: 7px;
}
.tab:hover { color: var(--navy-600); }

/* The whole tab, filled. A 2px underline is a lot of nothing to carry the one piece of state the
   strip exists to show, and next to a filled navy panel header it read as the weaker signal of
   the two. Filled navy matches the header bar under it and the primary button beside it. */
.tab.active {
    background: var(--navy-600);
    color: #fff;
    border-bottom-color: var(--navy-600);
    font-weight: 600;
    border-radius: var(--r-sm) var(--r-sm) 0 0;
}
.tab.active:hover { background: var(--navy-500); border-bottom-color: var(--navy-500); color: #fff; }

/* A tab strip whose tabs are links to other pages rather than panes of this one. Same strip to
   look at; the anchor just has to be talked out of its underline and its link colour. */
.tabs-nav a.tab { text-decoration: none; }
.tabs-nav a.tab:hover:not(.active) { background: var(--surface-2); }
.tabs-nav a.tab.active:hover { text-decoration: none; }

/* How many things are on the tab you are not looking at. Without it you have to open a tab to
   find out whether it holds anything. */
.tab-count {
    min-width: 20px;
    padding: 1px 6px;
    border-radius: 20px;
    background: var(--surface-2);
    color: var(--steel);
    font-size: var(--fs-2xs);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
/* Inverted on the active tab: the count used to be navy on navy once the tab itself filled. */
.tab.active .tab-count { background: rgba(255, 255, 255, .9); color: var(--navy-600); }

/* A count that is also a warning — a competency lapsed or about to. The person record's band
   states nothing but figures now, so the tab a problem lives behind has to carry the mark. */
.tab-count.is-warn { background: var(--warn-bg); color: var(--warn); }
.tab.active .tab-count.is-warn { background: var(--warn); color: #fff; }

/* The one survivor of the Roles admin screen: the label beside each menu tick on the
   Permissions tab. Everything that used to sit around it here — the role list, the grant rows,
   the retire panel — went when role editing moved into SystemPermissions.cshtml. */
.menu-rule-name { display: flex; align-items: flex-start; gap: 10px; font-size: var(--fs-sm); }
.menu-rule-name i { color: var(--steel-2); margin-top: 2px; }
.menu-rule-name small { display: block; font-size: var(--fs-2xs); color: var(--steel-2); }
.filter-bar {
    display: flex;
    gap: 10px;
    align-items: flex-end;
    flex-wrap: wrap;
    padding: 13px 18px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    margin-bottom: 14px;
    box-shadow: var(--sh-xs);
}
/* Search takes the slack, but is capped. Left uncapped it grew to whatever was left over —
   on a wide screen a 700px box for a short name, with the selects that actually narrow the
   list crammed against the far edge. */
.filter-bar .field { min-width: 150px; flex: 1 1 220px; max-width: 460px; }
.filter-bar .field.narrow { flex: 0 0 170px; max-width: none; }

/* A search box on a crowded bar cannot have 460px as well — it took the whole of the first row,
   pushed the selects along and clipped its own placeholder. Capped tighter where there is more to
   fit beside it.

   THE DATABASE LOG IS THE ONLY USER OF THIS NOW. It was written for the outbox and did not fix it,
   because with seven controls the search box was never what was wrong; that bar is a grid below.
   Four filters and a tickbox is within what flex can place, so the log keeps this. */
.filter-bar .field.is-search { flex: 1 1 200px; max-width: 260px; }

/* ---------- A filter bar of SEVEN controls: the outbox ----------
   WHY THIS IS A GRID AND THE OTHERS ARE NOT.

   Capping the search box was twice attempted here and twice failed, because the search box was
   never the problem. Flex on this bar hands every field a 220px basis and lets it grow, and the
   outbox carries seven of them — search, message, state, branch, from, to, and the buttons. Seven
   times 220 plus the gaps is 1580px against the 1256px this bar actually gets at 1600, so it
   wrapped whatever the search box was capped to. What that produced was five fields stretched
   across the first row and To stranded on a second beside the Filter button: not a filter bar, a
   filter bar and an offcut.

   Making the search smaller could only ever move which field got stranded. The bar needs to be
   TOLD its shape instead of having one negotiated for it, and that is a grid.

   Measured, not guessed: 1256px of usable width at 1600, 1022px at 1366, 680px at 1024. The four
   flexible columns share what the two date fields and the buttons leave, so all seven sit on one
   row down to about 1200 and the row is the same shape at every width above it. */
.filter-bar.is-outbox {
    display: grid;
    align-items: end;
    gap: 10px 12px;

    grid-template-columns:
        minmax(140px, 1.5fr)    /* Search  — the widest share, not a fixed size */
        minmax(105px, 1.15fr)   /* Message */
        minmax(95px, 1fr)       /* State   */
        minmax(105px, 1.15fr)   /* Branch  */
        140px                   /* From    — a date input and its picker, and nothing else */
        140px                   /* To      */
        auto;                   /* Filter, and Clear when there is something to clear */
}

/* The flex sizing has to come off, or the 150px floor every .field carries would raise three of
   these tracks above the share the grid just worked out for them and put the row back over its
   width — which is the original fault with an extra step in it. */
.filter-bar.is-outbox .field { min-width: 0; max-width: none; }

/* Below about 1200 the four flexible columns would each be under 120px, which is a select showing
   half a branch name. Two rows of four instead — four filters, then the two dates with the buttons
   at the end of the row, which is a balanced pair of rows rather than a row and a remainder. */
@media (max-width: 1200px) {
    .filter-bar.is-outbox { grid-template-columns: repeat(4, minmax(0, 1fr)); }

    /* Spanning the last two columns rather than sitting in one: Filter and Clear together are
       wider than a quarter of the bar, and a button that overflows its own column is what the
       flex version was doing. */
    .filter-bar.is-outbox .field.is-actions { grid-column: 3 / -1; }
}

/* Phones. Two columns, and the buttons take a row of their own — at this width they are the
   widest thing on the bar and there is nothing to line them up beside. */
@media (max-width: 620px) {
    .filter-bar.is-outbox { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .filter-bar.is-outbox .field.is-actions { grid-column: 1 / -1; }
}

/* The buttons act on the whole bar, so they sit at the end of it, at the height of the
   controls they act on. .filter-bar aligns to flex-end, which lines their bottom edge up
   with the inputs; the min-height matches an .inp so the tops agree as well. */
.filter-bar .filter-actions {
    display: flex;
    gap: 8px;
    flex: 0 0 auto;
    margin-left: auto;

    /* NOWRAP, so a tickbox beside the button stays beside it. Wrapping put "Errors only" on a
       line of its own ABOVE the button it belongs with, which reads as a filter that has come
       adrift from its bar rather than as one of its controls. */
    flex-wrap: nowrap;
    align-items: center;
    white-space: nowrap;
}

/* The field that holds them cannot be squeezed under its own contents either — it carries a
   tickbox and one or two buttons, so it is sized to them rather than to the 220px every other
   field starts at. */
.filter-bar .field.is-actions { flex: 0 0 auto; min-width: 0; max-width: none; }
.filter-bar .field.is-actions .check { margin-bottom: 0; }
/* 31px is the height an .inp resolves to (5px padding, 1px border, base line-height).
   Matched exactly rather than approximately: .filter-bar aligns to flex-end, so a button
   2px taller lines its bottom up with the inputs and stands 2px proud at the top, which
   reads as a wobble along the row. Kept in step with the .inp padding above. */
.filter-bar .filter-actions .btn {
    min-height: 31px;
    padding: 5px 14px;
}

/* ---------- The people table ----------
   Same approach as the job card table: declared widths and a fixed layout, so the columns
   do not resize as the content of a page changes. Status carries up to three stacked chips
   (Active, Contractor, Locked) and Roles can carry two, which is why neither can be sized
   to "whatever this page happens to hold". */
table.tbl-users { table-layout: fixed; min-width: 900px; }

table.tbl-users col.c-person   { width: 30%; }
table.tbl-users col.c-roles    { width: 24%; }
table.tbl-users col.c-branch   { width: 22%; }
table.tbl-users col.c-capacity { width: 12%; }
table.tbl-users col.c-status   { width: 12%; }

/* Narrow windows, same ladder as the job card table and for the same reason: a 900px floor with
   a scrollbar under it meant Status was off the edge on any laptop. Capacity leaves first — it is
   a number that matters when you are planning work, not when you are looking somebody up — and
   folds onto the person's own cell so it is still on the row. Branch follows. */
@media (max-width: 1188px) {
    table.tbl-users { min-width: 760px; }
    table.tbl-users .c-capacity { display: none; }
    table.tbl-users col.c-capacity { width: 0; }
    table.tbl-users .fold-capacity { display: block; }

    table.tbl-users col.c-person { width: 34%; }
    table.tbl-users col.c-roles  { width: 27%; }
    table.tbl-users col.c-branch { width: 25%; }
    table.tbl-users col.c-status { width: 14%; }
}

@media (max-width: 1048px) {
    table.tbl-users { min-width: 560px; }
    table.tbl-users .c-branch { display: none; }
    table.tbl-users col.c-branch { width: 0; }

    table.tbl-users col.c-person { width: 45%; }
    table.tbl-users col.c-roles  { width: 36%; }
    table.tbl-users col.c-status { width: 19%; }
}

/* Phones. Roles go too — a chip stack is the widest thing on the row and the least readable at
   this size. Who somebody is and whether their account works is what is left. */
@media (max-width: 620px) {
    table.tbl-users { min-width: 320px; }
    table.tbl-users .c-roles { display: none; }
    table.tbl-users col.c-roles { width: 0; }

    table.tbl-users col.c-person { width: 68%; }
    table.tbl-users col.c-status { width: 32%; }
}

/* ---------- The audit table ----------
   Eight columns, the most of any table here, so the widths matter more rather than less. From
   and To carry the only variable-length content and get a third of the table between them; the
   rest hold constant-width strings — a name, an entity, an action — and are sized to those.
   Shares are declared so the columns do not shift as you page through, which on a table read
   by scanning one column downwards is the difference between legible and not. */
/* Half to the message, a quarter each to the date and the person. The message is the longest,
   but a date squeezed into a sixth of the width wraps onto three lines and stops being scannable. */
table.tbl-audit { table-layout: fixed; min-width: 760px; }

table.tbl-audit col.c-when { width: 25%; }
table.tbl-audit col.c-who  { width: 25%; }
table.tbl-audit col.c-what { width: 50%; }

.audit-message { font-size: var(--fs-sm); line-height: 1.5; }

/* A value can be a sentence, a GUID or a 60-character truncation, and none of them may push
   the column open — the width above is the whole point. */
table.tbl-audit tbody td { overflow-wrap: anywhere; }

/* NO COLUMN IS DROPPED HERE, unlike the tables above, and the floor simply comes down instead.
   Three columns is already the minimum this table means anything at: an audit row that did not
   say who, or did not say when, would not be an audit row. Every cell already wraps rather than
   pushing its column open, so a lower floor costs height instead of legibility — which is the
   right trade for a table nobody scans on a phone but everybody may need to open on one.
   The message keeps taking share as the space goes, because it is the part being read. */
@media (max-width: 1048px) {
    table.tbl-audit { min-width: 520px; }
    table.tbl-audit col.c-when { width: 26%; }
    table.tbl-audit col.c-who  { width: 24%; }
    table.tbl-audit col.c-what { width: 50%; }
}

@media (max-width: 620px) {
    table.tbl-audit { min-width: 300px; }
    table.tbl-audit col.c-when { width: 24%; }
    table.tbl-audit col.c-who  { width: 22%; }
    table.tbl-audit col.c-what { width: 54%; }
    table.tbl-audit tbody td { padding: 8px; }
}

/* ---------- System Settings: label left, control right, two columns of pairs ----------
   Two columns of independent ELEMENTS, not one grid flowing down shared rows. The grid version
   tied the height of a row on the left to whatever sat beside it on the right, so a tall textarea
   stretched the colour box opposite it and the left column stopped lining up with its own labels.
   Nothing in one column can now pull on the other. */
.settings-cols {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0 44px;
    align-items: start;
}

.settings-col { min-width: 0; }

/* The save for a form, at the end of the form rather than in a bar under it. No rule and no
   background: the button is the last thing in the card, not a separate region of it. */
.body-actions {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 18px;
}

/* The long fields, side by side underneath. Separated from the block above so the rule reads as
   a break between "what it is" and "what it says". */
.settings-cols-wide {
    margin-top: 18px;
    padding-top: 18px;
    border-top: 1px solid var(--line);
}

@media (max-width: 1100px) {
    .settings-cols { grid-template-columns: minmax(0, 1fr); }
}

/* Height and width are one decision, so they sit on one row rather than two. */
.size-field { display: flex; gap: 12px; }
.size-field > span { flex: 1 1 0; min-width: 0; }
.size-field small {
    display: block;
    font-size: var(--fs-xs);
    color: var(--steel);
    margin-bottom: 3px;
}

.set-row {
    display: grid;
    grid-template-columns: 180px minmax(0, 1fr);
    gap: 12px;
    align-items: start;
    padding: 7px 0;
    min-height: 50px;
}

.set-row > label {
    font-size: var(--fs-sm);
    font-weight: 500;
    color: var(--ink-2);
    padding-top: 6px;
    text-wrap: pretty;
}

.set-row .help { display: block; margin-top: 5px; }

/* A row built around a textarea rather than a single-line input. The label stops pretending to
   be vertically centred against something four lines tall and sits at the top with it. */
.set-row-tall { min-height: 0; align-items: start; }
.set-row-tall .txt { min-height: 132px; resize: vertical; line-height: 1.55; }

/* What the field is for, under its own label. Lighter and smaller than the label so the two read
   as a heading and its aside rather than as two labels stacked. */
.label-note {
    display: block;
    margin-top: 3px;
    font-size: var(--fs-xs);
    font-weight: 400;
    color: var(--steel);
    line-height: 1.45;
    text-wrap: pretty;
}

@media (max-width: 620px) {
    .set-row { grid-template-columns: minmax(0, 1fr); }
    .set-row > label { padding-top: 0; }
}

/* The colour, with the colour beside it — a hex value is not something anyone reads as a colour. */
.colour-field { display: flex; align-items: center; gap: 10px; }
.colour-field .inp { font-family: var(--mono); }
.colour-field .swatch {
    flex: 0 0 auto;
    width: 34px;
    height: 34px;
    border-radius: var(--r-sm);
    border: 1px solid var(--line);
}

/* ---------- A toolbar over a grid ----------
   Buttons that act on the SELECTED row, so most of them start disabled. A disabled tool that
   still reads as a label is the point: it tells you the action exists and that it wants a
   selection, which an absent button cannot. */
.grid-toolbar {
    display: flex;
    align-items: center;
    gap: 4px;
    flex-wrap: wrap;
    padding: 5px 9px;
    border-bottom: 1px solid var(--line);
    background: var(--surface-2);
}

/* The search, at the far end of the same bar as the actions. Pushed right rather than given a
   row of its own — a second grey strip under the first read as two headers on one card. */
.toolbar-search {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-left: auto;
    flex: 0 1 460px;
    min-width: 0;
}
.toolbar-search .inp { flex: 1 1 auto; min-width: 120px; }

/* Pushed right is right when the bar also carries Add and Edit — the search belongs at the far end
   from the actions. On a bar that holds nothing else there is nothing to be at the far end of, and
   a lone box against the right edge reads as though the actions went missing. */
.toolbar-search.is-left { margin-left: 0; }

@media (max-width: 720px) {
    .toolbar-search { flex-basis: 100%; margin-left: 0; margin-top: 6px; }
}

/* A toolbar action: a 16px icon, then its label. Flat until hovered, so a row of them reads as a
   line of choices rather than a wall of boxes. The icon carries the colour and the label carries
   the meaning — which is what lets Delete be recognisable at a glance without being red text. */
.tool {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 9px;
    border: 1px solid transparent;
    border-radius: var(--r-sm);
    background: transparent;
    font-family: inherit;
    font-size: var(--fs-xs);
    font-weight: 500;
    color: var(--ink-2);
    text-decoration: none;
    cursor: pointer;
    line-height: 1.5;
}

/* 16px, and a fixed box so labels line up whether or not their glyph is a wide one. */
.tool i {
    font-size: 1rem;
    line-height: 1;
    width: 16px;
    text-align: center;
    flex-shrink: 0;
    color: var(--navy-500);
}

.tool:hover:not(:disabled) { background: var(--surface); border-color: var(--line); color: var(--navy-600); }
.tool:disabled { color: var(--steel-2); cursor: not-allowed; opacity: .65; }
.tool:disabled i { color: var(--steel-2); }

/* Per action, as in the system this is modelled on: adding is green, removing is red, and the
   rest are the ordinary navy. Colour is the fast read; the word is the confirmation. */
.tool.is-add i { color: var(--ok); }
.tool.is-danger i { color: var(--bad); }
.tool.is-danger:hover:not(:disabled) {
    color: var(--bad);
    border-color: color-mix(in srgb, var(--bad) 35%, var(--line));
}

/* A filter bar directly under a toolbar carries the toolbar's rule, not its own margin. */
.filter-bar-tight {
    margin: 0;
    padding: 10px 12px;
    border-bottom: 1px solid var(--line);
    background: var(--surface-2);
}

/* A row that is a problem rather than a place — "No branch" in the cover breakdown. Tinted, not
   hidden: people holding a role and placed nowhere are invisible to every branch-scoped screen,
   which is exactly the thing this panel exists to surface. */
tr.row-warn td { background: var(--warn-bg); }
tr.row-warn:hover td { background: color-mix(in srgb, var(--warn) 14%, var(--surface)); }

/* A heading INSIDE a table, naming the thing the rows under it belong to — the contracting firm
   on the Users screen's Contractor tab.

   ONE TABLE, NOT ONE PER GROUP. A table per business would mean a colgroup per business, and four
   firms would then draw four grids whose columns did not line up with each other. A row spanning
   the width keeps every person on the page in the same columns and still says plainly where one
   firm ends and the next begins.

   It is a <th> in its own <tbody> rather than a <td>, because that is what it is: a heading for
   the rows beneath it, and a screen reader should say so. */
tr.row-group > th {
    padding: 10px 12px;
    background: var(--surface-2);
    border-top: 1px solid var(--line);
    border-bottom: 1px solid var(--line);
    font-size: 13px;
    text-align: left;
    color: var(--navy-600);
}

tr.row-group .row-group-name { font-weight: 700; }

/* The count and any note sit beside the name rather than under it — the row is a heading, and a
   heading that wraps to two lines starts to look like a record. */
tr.row-group .row-group-note {
    margin-left: 8px;
    font-weight: 400;
    color: var(--steel);
}

/* A CONTRACTOR'S WEEK: seven days across, one fact on each.

   NOT A TABLE, and the difference is the question being asked. An hours week is a table because
   each day carries a list of job cards and a total; a contractor's day carries only whether they
   are coming, and seven table rows to say "Not booked" seven times is most of a panel spent on
   nothing. This is read at a glance, which is what "a manager can easily see which days
   contractors are booked" asks for.

   IT WRAPS RATHER THAN SCROLLS. Seven cells at a 120px floor need 840px; below that they fall onto
   a second line, which reads perfectly well, whereas a sideways scrollbar hides the end of the
   week — and the end of the week is where the booking usually is. */
.book-week {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.book-day {
    flex: 1 1 120px;
    min-width: 120px;
    padding: 10px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--surface-2);
}

/* The days that have gone recede; today is marked the way every other screen marks it. */
.book-day.is-gone { opacity: .62; }
.book-day.is-today { border-color: var(--navy-300); box-shadow: inset 0 0 0 1px var(--navy-300); }

/* A booked day has to be findable by colour alone from across a desk. */
.book-day.is-booked { background: var(--ok-bg); border-color: var(--ok); }

.book-day-name {
    font-size: 13px;
    font-weight: 700;
    color: var(--ink);
}

.book-day-date {
    font-size: 12px;
    color: var(--steel);
    margin-bottom: 8px;
}

/* ---------- Cover matrix: roles down, branches across ---------- */

/* The header row has to survive the scroll. At thirty roles the branch names leave the top of the
   dialog long before the rows do, and a grid of bare numbers with no headings is unreadable. */
table.tbl-matrix thead th {
    position: sticky;
    top: 0;
    z-index: 1;
}

/* The role name is the only column with words in it, so it takes the room; the counts are equal
   and narrow so the eye can run down a column. */
table.tbl-matrix th:first-child, table.tbl-matrix td:first-child { min-width: 200px; }
table.tbl-matrix th.n, table.tbl-matrix td.n { width: 88px; }

/* The total is the row's own figure rather than another branch — ruled off so it does not read as
   one more site. */
table.tbl-matrix .is-total { border-left: 2px solid var(--line); }

/* A count is a link, but a whole grid of underlined blue numbers would be unreadable. It reads as
   a figure and behaves as a link. */
.cover-cell {
    display: inline-block;
    min-width: 26px;
    padding: 1px 5px;
    border-radius: var(--r-sm);
    color: inherit;
    font-variant-numeric: tabular-nums;
    text-decoration: none;
}
.cover-cell:hover {
    background: var(--navy-600);
    color: #fff;
    text-decoration: none;
}
.cover-cell.is-warn { color: var(--warn); font-weight: 600; }
.cover-cell.is-warn:hover { background: var(--warn); color: #fff; }

/* The role name opens everyone in that role. Underlined only on approach, so thirty rows read as
   a table rather than as a list of links. */
.cover-role { color: inherit; text-decoration: none; }
.cover-role:hover { color: var(--navy-400); text-decoration: underline; }

/* ---------- A row you pick before acting on it ---------- */
tr.is-selectable { cursor: pointer; }
tr.is-selectable:hover td { background: var(--surface-2); }
tr.is-selectable.selected td {
    background: color-mix(in srgb, var(--navy-400) 16%, var(--surface));
    box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--navy-400) 30%, transparent);
}

/* Nothing in code claims this key. Marked, not hidden. */
tr.row-unknown .cell-title { color: var(--warn); }

table.tbl-settings { table-layout: fixed; min-width: 640px; }
/* Even halves. The name and the value are peers — one is what a setting is called and the other
   is what it is set to — and 38/62 read as though the value were the more important of the two. */
table.tbl-settings col.c-name  { width: 50%; }
table.tbl-settings col.c-value { width: 50%; }
table.tbl-settings tbody td { overflow-wrap: anywhere; }

/* Five even columns. They were 8/30/40/10/12, which gave the URL four times the room of the
   status beside it for no reason the reader could see. */
table.tbl-menu { table-layout: fixed; min-width: 900px; }
table.tbl-menu col { width: 20%; }
table.tbl-menu tbody td { overflow-wrap: anywhere; }

/* Five equal columns is generous for three that hold almost nothing — an integer, a glyph and a
   tick. They go first, in that order of uselessness at a small size, and the URL folds under the
   title when its own column goes rather than being lost: on this screen the URL is half of what
   identifies the row. */
@media (max-width: 1188px) {
    table.tbl-menu { min-width: 700px; }
    table.tbl-menu .c-icon { display: none; }
    table.tbl-menu col.c-icon { width: 0; }

    table.tbl-menu col.c-order  { width: 12%; }
    table.tbl-menu col.c-title  { width: 36%; }
    table.tbl-menu col.c-url    { width: 36%; }
    table.tbl-menu col.c-active { width: 16%; }
}

@media (max-width: 900px) {
    table.tbl-menu { min-width: 460px; }
    table.tbl-menu .c-order { display: none; }
    table.tbl-menu col.c-order { width: 0; }

    table.tbl-menu col.c-title  { width: 42%; }
    table.tbl-menu col.c-url    { width: 40%; }
    table.tbl-menu col.c-active { width: 18%; }
}

@media (max-width: 620px) {
    table.tbl-menu { min-width: 300px; }
    table.tbl-menu .c-url { display: none; }
    table.tbl-menu col.c-url { width: 0; }
    table.tbl-menu .fold-url { display: block; }

    table.tbl-menu col.c-title  { width: 74%; }
    table.tbl-menu col.c-active { width: 26%; }
}

.menu-icon-cell { font-size: 1.15rem; color: var(--navy-500); }
.tick-ok  { color: var(--ok);      font-size: 1.05rem; }
.tick-off { color: var(--steel-2); font-size: 1.05rem; }

/* A key that may be set once and not afterwards. Readonly rather than disabled, so it still
   posts and can still be selected and copied. */
.inp.is-locked { background: var(--surface-2); color: var(--steel); }

table.tbl-roles { table-layout: fixed; min-width: 560px; }
table.tbl-roles col.c-name   { width: 88%; }
table.tbl-roles col.c-people { width: 12%; }
table.tbl-roles .cell-title { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }

/* Sized to the permission rows below them — .perm-row is 46px with 9px of padding. A role and the
   permissions it holds are two halves of one decision, and they were being read at two scales. */
table.tbl-roles tbody td { padding: 11px 12px; }
table.tbl-roles tbody tr { min-height: 46px; }

/* A toolbar link that needs a selection it does not have yet. Anchors cannot be disabled, so it
   is dimmed and its click is refused — the same result, without a button that lies about being
   pressable. */
.tool.is-off { color: var(--steel-2); opacity: .65; cursor: not-allowed; }
.tool.is-off:hover { background: transparent; border-color: transparent; }

/* ---------- Role permissions ----------
   One permission per row, straight down, grouped under the area it belongs to. A grid of cards
   sizes each card to the length of its description, so no two are the same height and the eye has
   no line to follow down. */
.perm-groups { display: flex; flex-direction: column; gap: 22px; }

.perm-group-head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding-bottom: 8px;
    margin-bottom: 4px;
    border-bottom: 1px solid var(--line);
}
.perm-group-head h3 {
    font-size: var(--fs-sm);
    font-weight: 650;
    color: var(--ink);
    letter-spacing: .01em;
}

.perm-row {
    display: grid;
    grid-template-columns: 20px minmax(0, 1fr) auto;
    gap: 12px;
    align-items: start;
    padding: 9px 4px;
    border-radius: var(--r-sm);
    cursor: pointer;
    min-height: 46px;
}
.perm-row:hover { background: var(--surface-2); }
.perm-row input[type="checkbox"] { width: 16px; height: 16px; margin-top: 2px; }
.perm-row input:disabled { cursor: not-allowed; }

.perm-text strong {
    display: block;
    font-size: var(--fs-sm);
    font-weight: 500;
    color: var(--ink);
}
.perm-text span {
    display: block;
    font-size: var(--fs-xs);
    color: var(--steel);
    margin-top: 2px;
}

/* ---------- Users in a role ----------
   A dialog with its own toolbar and list, so the table inside it loses the panel's rounding and
   sits flush to the dialog's edges. */
.modal-panel .grid-toolbar { border-radius: 0; }
.modal-panel .modal-body > .tbl { border-radius: 0; }

/* The tick column in the people picker — sized to the box, not to a heading it does not have. */
table.tbl th.c-tick, table.tbl td.c-tick { width: 42px; text-align: center; }
table.tbl td.c-tick input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; }

/* Whole row is the target, so a long list is not a column of 16px squares to aim at. */
#availableList tr { cursor: pointer; }
#availableList tr:hover td { background: var(--surface-2); }

/* The menu grid inside the role dialog. */
/* ---------- A list down the left, one thing open on the right ----------
   Used by Message Wording, where eleven messages stacked as eleven forms would be a page nobody
   reaches the bottom of. The list is narrow because the items are short names; the panel beside it
   takes whatever is left. */

.split-pane {
    display: grid;
    grid-template-columns: minmax(0, 260px) minmax(0, 1fr);
    gap: 22px;
    align-items: start;
}

/* Below this the two columns would each be too narrow to read, so the list goes above the panel
   and the page becomes one column of ordinary height. */
@media (max-width: 900px) {
    .split-pane { grid-template-columns: minmax(0, 1fr); gap: 14px; }
}

.pane-list {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--line);
    border-radius: var(--rad);
    overflow: hidden;
    background: var(--surface);
}

.pane-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 13px;
    border-top: 1px solid var(--line-2);
    color: var(--ink-2);
    font-size: var(--fs-sm);
    text-decoration: none;
}
.pane-item:first-child { border-top: 0; }
.pane-item:hover { background: var(--surface-2); }

/* The open one carries a bar down its edge rather than a fill, so a long list does not become a
   block of colour with one gap in it. */
.pane-item.is-open {
    background: var(--surface-2);
    color: var(--navy-600);
    font-weight: 600;
    box-shadow: inset 3px 0 0 var(--navy-500);
}

.pane-item-title { flex: 1 1 auto; min-width: 0; }

.pane-body { min-width: 0; }

/* ---------- The outbox ----------
   Why nothing was sent, printed on the row rather than behind a click. It is the reason somebody
   opened this screen, and a reason you have to go looking for is one nobody reads. Amber rather
   than red: a suppressed message is usually a finding about how the system is set up, not a fault.
*/
.cell-reason {
    margin-top: 3px;
    color: var(--warn-ink, var(--warn));
    max-width: 60ch;
}

/* The message as it was built. Pre, because the body is assembled with real line breaks and
   collapsing them would run the facts block into one paragraph. */
.mail-body {
    margin: 0;
    font-family: inherit;
    font-size: var(--fs-sm);
    line-height: 1.55;
    white-space: pre-wrap;
    word-break: break-word;
}

/* ---------- The outbox table ----------
   The subject carries the weight and the reason a message did not send sits under it, so Message
   takes the largest share. Job card and State were being squeezed to their content — a job number
   wrapping mid-code and a chip sitting on top of its attempt count — so both are given a floor
   they cannot drop below. */
table.tbl-outbox { table-layout: fixed; min-width: 940px; }

table.tbl-outbox col.c-when  { width: 15%; }
table.tbl-outbox col.c-who   { width: 21%; }
table.tbl-outbox col.c-msg   { width: 34%; }
table.tbl-outbox col.c-card  { width: 16%; }
table.tbl-outbox col.c-state { width: 14%; }

/* Air between the columns. The default cell padding is right for a dense two-line row; this table
   carries three lines in places and needs the gutters to read as columns rather than as a block. */
table.tbl-outbox th,
table.tbl-outbox td { padding-left: 14px; padding-right: 14px; }

/* A long address or subject breaks rather than forcing the table wider than its share. */
table.tbl-outbox tbody td { overflow-wrap: anywhere; }

/* The job number stays on one line — a code split across two is one nobody can read back. */
table.tbl-outbox .c-card .mono { white-space: nowrap; }

/* Narrow windows: When and To give way first. They are the two that can be read off the rest. */
@media (max-width: 1400px) {
    table.tbl-outbox { min-width: 820px; }
    table.tbl-outbox col.c-when { width: 14%; }
    table.tbl-outbox col.c-who  { width: 19%; }
    table.tbl-outbox col.c-msg  { width: 35%; }
    table.tbl-outbox col.c-card { width: 17%; }
}

/* ---------- Who is told, per role ----------
   The dropdown says "Their branches", and at the width the column was taking it said "Their
   branche" with the arrow over the last letter. The scope column is sized for its longest option
   rather than for its heading. */
table.tbl-recipients { table-layout: fixed; min-width: 380px; }

table.tbl-recipients col.c-role  { width: 50%; }
table.tbl-recipients col.c-scope { width: 50%; }

table.tbl-recipients th,
table.tbl-recipients td { padding-left: 14px; padding-right: 14px; }

/* The select fills its column instead of taking a browser default width that has nothing to do
   with the text inside it. */
table.tbl-recipients .inp { width: 100%; min-width: 150px; }

/* ---------- Who that works out to ----------
   The column beside the recipients grid, which used to be a table with one row per resolved
   person. That is fine at ten and unusable at five hundred: a message set to "anybody who can
   assign, everywhere" produced a staff directory, and the six-row grid it exists as evidence FOR
   became a footnote beside it. Counted now, with the names behind a click.

   EVERY LIST HERE IS BOUNDED. Not one of them may decide how tall this screen is — that is the
   whole failure being fixed, and a fix that only holds until somebody hires forty people is not
   one. */

/* Three figures in a column about half the page wide, so the tiles are smaller than the ones that
   run the full width of the outbox. The 96px floor is what "Would receive it" needs.

   .stat-row.aud-stats, NOT .aud-stats. The base .stat-row is declared further down this file with
   a 150px floor and the same one-class weight, so a plain .aud-stats loses the tie on order alone
   — which put two tiles on one line and the third on its own below at 1366. Qualified, it wins
   wherever either rule happens to sit. */
.stat-row.aud-stats {
    grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
    gap: 8px;
    margin-bottom: 10px;
}
.aud-stats .stat { padding: 9px 11px; }
.aud-stats .stat-figure { font-size: var(--fs-xl); }

.aud-line { margin: 0 0 14px; }
.aud-note { margin-bottom: 8px; }

/* A heading inside the column. Small caps weight rather than a rule, because the column already
   carries three tiles, a note and two lists and another horizontal line would fence it into
   sections that are not that separate. */
.aud-head {
    margin: 18px 0 8px;
    font-size: var(--fs-xs);
    font-weight: 650;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--steel);
}

/* ---- a branch or a role, and how many people are under it ----
   A bar, not a third table column. The question is a comparison — is this heavier at one site than
   I meant — and comparing figures is work the screen can do instead. */
.aud-groups { list-style: none; margin: 0; padding: 0; }

.aud-groups > li {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 5px 0;
    font-size: var(--fs-sm);
    color: var(--ink-2);
}

/* The name is given a share rather than its content width, so the bars all start at the same
   place down the column and can actually be read against one another. */
.aud-group-name {
    flex: 0 1 38%;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.aud-bar {
    flex: 1 1 auto;
    min-width: 30px;
    height: 6px;
    border-radius: 3px;
    background: var(--line-2);
    overflow: hidden;
}
.aud-bar > i { display: block; height: 100%; background: var(--navy-300); border-radius: 3px; }

.aud-count {
    flex: 0 0 auto;
    min-width: 2ch;
    text-align: right;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
}

/* ---- the names ----
   One line each and a fixed ceiling. Sixteen rows of about 27px is roughly seven visible, which is
   enough to read as a list rather than as a box with something in it, and short enough that the
   grid on the left stays the taller of the two. */
.aud-names {
    list-style: none;
    margin: 0;
    padding: 4px 0;
    max-height: 232px;
    overflow-y: auto;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--surface);
}

/* THE LAST ROW FADES OUT WHEN THERE ARE MORE BELOW IT.

   A bounded list whose last row is simply sliced in half reads as a rendering fault rather than as
   an invitation to scroll, and this is the one box on the screen where somebody must not conclude
   they have seen everything when they have seen seven of twenty-three.

   The scrollbar cannot be relied on to say it. Edge on Windows 11 draws an overlay scrollbar that
   paints nothing at rest — measured: the box reserves two pixels, which is its own border and no
   scrollbar at all — and scrollbar-width: thin makes it worse rather than better, because this
   file already styles every scrollbar in the application at 10px and the thin keyword overrides
   that down to a hairline.

   So the fade is applied by the SERVER, which knows the count, and only when the count is more
   than the box can hold. A list that fits is not faded, because a fade on a complete list would
   say there is more when there is not — which is the same lie in the other direction. */
.aud-names.is-more {
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 26px), transparent 100%);
    mask-image: linear-gradient(to bottom, #000 calc(100% - 26px), transparent 100%);
}

.aud-names > li {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 4px 12px;
    font-size: var(--fs-sm);
}
.aud-names > li + li { border-top: 1px solid var(--line-2); }

.aud-name { font-weight: 550; color: var(--ink); flex: 0 0 auto; }

/* The role and branches trail off rather than wrapping. Somebody linked to five sites would
   otherwise give one row of this list four lines and push the rest out of view. */
.aud-where {
    color: var(--steel);
    font-size: var(--fs-xs);
    margin-left: auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The people who get nothing. Tinted as a whole, because this is the finding the screen exists to
   make and it must not read as one more list of names. */
.aud-names-bad { border-color: rgba(179, 38, 30, .25); background: var(--bad-bg); margin-bottom: 6px; }
.aud-names-bad > li + li { border-top-color: rgba(179, 38, 30, .18); }
.aud-names-bad .aud-where { color: #8a1d17; }

/* ---- all of them, behind a click ---- */
.aud-all { margin-top: 18px; }

.aud-all > summary {
    display: flex;
    align-items: center;
    gap: 7px;
    cursor: pointer;
    padding: 7px 12px;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--surface-2);
    color: var(--navy-600);
    font-size: var(--fs-sm);
    font-weight: 600;
    list-style: none;
}
.aud-all > summary::-webkit-details-marker { display: none; }
.aud-all > summary:hover { background: var(--canvas); }
.aud-all > summary i { transition: transform .16s var(--ease); }
.aud-all[open] > summary i { transform: rotate(90deg); }

.aud-all-body { padding-top: 10px; }
.aud-find { width: 100%; margin-bottom: 8px; }

/* Taller than the list of people who get nothing, because this one is opened on purpose and the
   ceiling is only there to stop it running off the bottom of the page. */
.aud-all-body .aud-names { max-height: 320px; }
.aud-none { margin: 8px 0 0; }

/* ---------- The database log ----------
   Method carries the weight — it is what somebody scans for — and Request and Response are one
   line of context each. Outcome is a chip and a duration, so it needs a floor rather than a share. */
table.tbl-log { table-layout: fixed; min-width: 980px; }

table.tbl-log col.c-when     { width: 15%; }
table.tbl-log col.c-method   { width: 22%; }
table.tbl-log col.c-request  { width: 21%; }
table.tbl-log col.c-response { width: 30%; }
table.tbl-log col.c-state    { width: 12%; }

/* The same gutters the outbox uses, and for the same reason: rows here run to two and three lines,
   and at the default padding the columns read as one block of text rather than as columns. */
table.tbl-log th,
table.tbl-log td { padding-left: 18px; padding-right: 18px; }

table.tbl-log th:first-child,
table.tbl-log td:first-child { padding-left: 20px; }

table.tbl-log th:last-child,
table.tbl-log td:last-child { padding-right: 20px; }

table.tbl-log tbody td { padding-top: 11px; padding-bottom: 11px; }

/* A serialised request breaks rather than forcing the table wider than its share. */
table.tbl-log tbody td { overflow-wrap: anywhere; }

/* A failed row carries a rule down its left edge, so a page of them can be scanned rather than
   read. The same device the stat tiles use for the same reason. */
table.tbl-log tbody tr.is-error > td:first-child { box-shadow: inset 3px 0 0 var(--bad); }

table.tbl-log tbody tr { cursor: pointer; }
table.tbl-log tbody tr:hover > td { background: var(--surface-2); }
table.tbl-log tbody tr:focus-visible { outline: 2px solid var(--navy-500); outline-offset: -2px; }

/* ---------- The scheduled jobs table ----------
   tbl-sched, NOT tbl-jobs. That name is already taken by the job card list further up this file,
   which is a seven-column grid with a 1180px floor and a ladder of its own — borrowing it would
   have applied all of that to this table and this table's widths to every job card list in the
   system, which is the exact trap the comment on the asset table warns about.

   DECLARED WIDTHS for the same reason the outbox and the log have them, and the reason applies
   most sharply here: five columns whose content lengths are wildly different — a job name with a
   sentence under it beside four cells that are a time and a chip — and on auto layout the browser
   sized every column to its own longest line. The Job column took about half the table before
   Rhythm had a word in it, and the four time columns each came out a different width, so no two
   rows lined up and the grid read as a paragraph with gaps in it.

   FIXED, so the shares below are what is used rather than a suggestion the content can override.
   Job takes the largest share because it carries three lines; the four that follow are equal,
   because they hold the same shape of thing — a value with a caption under it — and equal columns
   are what makes the rows scan down the page as rows.

   The min-width is what the five columns need before the gutters start eating the words. Below
   that the wrapper scrolls, which keeps the page itself from ever scrolling sideways. */
table.tbl-sched { table-layout: fixed; min-width: 980px; }

table.tbl-sched col.c-job    { width: 36%; }
table.tbl-sched col.c-rhythm { width: 16%; }
table.tbl-sched col.c-last   { width: 16%; }
table.tbl-sched col.c-next   { width: 16%; }
table.tbl-sched col.c-state  { width: 16%; }

/* The same 18px gutters the log uses, and for the same reason: rows here run to three lines, and
   at the default padding the columns read as one block of text rather than as columns. */
table.tbl-sched th,
table.tbl-sched td { padding-left: 18px; padding-right: 18px; }

table.tbl-sched th:first-child,
table.tbl-sched td:first-child { padding-left: 20px; }

table.tbl-sched th:last-child,
table.tbl-sched td:last-child { padding-right: 20px; }

/* EVEN VERTICAL RHYTHM, which is the half that cannot be done with widths. The rows carry between
   one and four lines each — a job somebody has changed has an extra line under its name and an
   extra line under its rhythm — so at the default 8px the tall rows and the short ones were
   visibly different heights and the table looked ragged down the page. A floor on the row height
   plus roomier padding makes the difference between a one-line row and a three-line row one line
   rather than three, and the cells are centred in it so a chip does not sit against the top edge
   of a cell whose neighbour is a paragraph. */
table.tbl-sched tbody td {
    padding-top: 13px;
    padding-bottom: 13px;
    vertical-align: middle;
}

table.tbl-sched tbody tr { height: 76px; }

/* A gap between the chip and the times above it, so a two-chip cell does not close up against the
   date it belongs to. */
table.tbl-sched .chip { margin-top: 5px; }
table.tbl-sched .chip + .chip { margin-left: 5px; }

/* A long job name breaks rather than forcing the table wider than its share. */
table.tbl-sched tbody td { overflow-wrap: anywhere; }

/* Narrow windows: the two date columns take MORE rather than less, and the job name gives it up.
   The dates are the one thing here whose content cannot be shortened — "Wednesday 19 August 2026"
   is as short as that day gets — so squeezing them is what makes a row two lines taller, while the
   job's description is a sentence that was always going to wrap somewhere. */
@media (max-width: 1400px) {
    table.tbl-sched { min-width: 900px; }

    table.tbl-sched th,
    table.tbl-sched td { padding-left: 14px; padding-right: 14px; }

    table.tbl-sched col.c-job    { width: 30%; }
    table.tbl-sched col.c-rhythm { width: 14%; }
    table.tbl-sched col.c-last   { width: 20%; }
    table.tbl-sched col.c-next   { width: 20%; }
    table.tbl-sched col.c-state  { width: 16%; }
}

/* Narrower still: LAST RUN GOES, and it is the right one to lose. It is history, and the whole of
   it — including why a run failed — is one click away on the Background Jobs dashboard, which this
   screen links to twice. What cannot be got next door is the decision this screen owns, so Rhythm
   and Status stay. Dropped rather than squeezed, because a column pushed below its content is a
   column that makes every row taller for something nobody can read anyway. */
@media (max-width: 1200px) {
    table.tbl-sched { min-width: 700px; }

    table.tbl-sched .c-last { display: none; }
    table.tbl-sched col.c-last { width: 0; }

    table.tbl-sched col.c-job    { width: 38%; }
    table.tbl-sched col.c-rhythm { width: 18%; }
    table.tbl-sched col.c-next   { width: 24%; }
    table.tbl-sched col.c-state  { width: 20%; }
}

/* A stack trace is long and pre-formatted. Bounded, because an unbounded one pushes the Close
   button off the bottom of the dialog and there is then no way out but Escape.

   IT WRAPS RATHER THAN RUNNING OFF THE SIDE, and it now says so itself. A pre does not wrap by
   default and a trace carries absolute file paths well over a hundred characters, so an unwrapped
   one either scrolls sideways — hiding the frame somebody is looking for behind a scrollbar they
   have to find first — or drags the dialog past the edge of a narrow window. The wrapping was
   coming from the .mail-body class the markup happens to put beside this one; a block whose shape
   depends on a second class staying in the markup is a block that breaks the day somebody tidies
   the markup.

   The face is the system's own identifier stack — one typeface everywhere, see --mono — but the
   size and leading are set for reading a hundred lines of it rather than a branch code.

   Radius was var(--rad), a token this stylesheet never defines, so the corners were square while
   every box around them was rounded. */
.log-trace {
    max-height: 46vh;
    overflow: auto;
    max-width: 100%;
    margin: 0;
    font-family: var(--mono);
    font-size: var(--fs-sm);
    line-height: 1.6;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    background: var(--surface-2);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    padding: 14px 16px;
}

/* A button that reads as a link. The subject opens the message, and an anchor with no address is
   a link that goes nowhere — this is a button that looks right rather than a link that lies. */
.link-btn {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: var(--navy-500);
    text-align: left;
    cursor: pointer;
}
.link-btn:hover { text-decoration: underline; }

/* Double-click opens a row, so the row has to look like something that can be opened. */
.tbl-outbox tbody tr { cursor: pointer; }
.tbl-outbox tbody tr:hover > td { background: var(--surface-2); }
.tbl-outbox tbody tr:focus-visible {
    outline: 2px solid var(--navy-500);
    outline-offset: -2px;
}

/* ---------- The wording beside what it produces ----------
   Even halves: the boxes on the left, the assembled message on the right. They are read together —
   somebody types a sentence and looks straight across — and stacking them put the preview off the
   bottom of the screen by the time the third box was filled in, which made it decoration.

   minmax(0, 1fr) on both, not 1fr: a grid column defaults to min-content, so a long unbroken line
   in the preview would push the column wider than its half and squeeze the form. */
.wording-split {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 26px;
    align-items: start;
}

.wording-edit,
.wording-preview { min-width: 0; }

/* One column, for a split whose two halves are a decision and its consequence rather than two
   views of one thing. Who Gets What uses it: the rules grid is the subject and needs the width,
   and the audience it produces reads underneath it. */
.wording-split.is-stacked { grid-template-columns: minmax(0, 1fr); }

/* The preview follows the page down while somebody works through the boxes beside it. Only where
   there is height to spare — a short viewport would pin it over the form it is meant to sit
   beside. */
@media (min-height: 760px) {
    .wording-preview { position: sticky; top: 16px; }
}

/* Three columns inside a page that already has a sidebar stops working here: the boxes and the
   preview each end up too narrow to judge. The preview goes back under the form. */
@media (max-width: 1250px) {
    .wording-split { grid-template-columns: minmax(0, 1fr); gap: 20px; }
    .wording-preview { position: static; }
}

/* ---------- What the message will actually look like ----------
   Three sentences read as three boxes do not tell you whether the result hangs together, so the
   screen shows them assembled with the facts in place. Styled like the mail rather than like the
   application: plain, narrow, and in the same face the message goes out in. */

.mail-preview {
    border: 1px solid var(--line);
    border-radius: var(--rad);
    overflow: hidden;
    background: #fff;
}

.mail-preview-head {
    padding: 11px 15px;
    background: var(--surface-2);
    border-bottom: 1px solid var(--line);
    font-size: var(--fs-sm);
    color: var(--ink);
}

.mail-preview-body {
    padding: 15px;
    font-family: "Segoe UI", Arial, sans-serif;
    font-size: 14px;
    line-height: 1.5;
    color: #12203a;
}
.mail-preview-body p { margin: 0 0 14px; }
.mail-preview-body p:last-child { margin-bottom: 0; }

/* The block the wording cannot change, shown in the monospace the rest of the system uses for
   recorded fact — so it reads as data rather than as prose somebody could edit. */
.mail-preview-facts {
    font-family: var(--mono);
    font-size: var(--fs-xs);
    line-height: 1.7;
    color: var(--steel);
}

/* ---------- Documents on a job card ----------
   The name carries the weight — it is what somebody clicks — so it gets nearly half the table.
   Number is a short code and What it is a chip, neither of which grows. */
table.tbl-docs { table-layout: fixed; min-width: 620px; }

/* EVEN THIRDS. There were four columns and one of them — a reference number nobody had a use for
   — has gone; what is left is three facts about a document, none of them worth more room than the
   others. */
/* FOUR COLUMNS AND NO SIDEWAYS SCROLL. The actions column was added without taking its width
   from anywhere, so the table went over 100% and grew a horizontal scrollbar under a register of
   one document. */
/* THREE COLUMNS AGAIN. The actions moved to the toolbar where every other register keeps them,
   so the widest column on the table is no longer paying for the least-used controls on it. */
table.tbl-docs col.c-doc  { width: 44%; }
table.tbl-docs col.c-kind { width: 26%; }
table.tbl-docs col.c-who  { width: 30%; }

table.tbl-docs tbody td { overflow-wrap: anywhere; }

/* The file name and its icon on one line, with the icon holding its size rather than shrinking
   into the text beside it. */
.doc-link { display: inline-flex; align-items: baseline; gap: 7px; }
.doc-link > i { flex: 0 0 auto; color: var(--steel-2); }
.doc-link:hover > i { color: var(--navy-500); }

/* Narrow windows. Nothing folds until the type does, below — three columns fit where four did
   not. */
@media (max-width: 1100px) {
    table.tbl-docs { min-width: 420px; }
}

@media (max-width: 620px) {
    table.tbl-docs { min-width: 300px; }
    table.tbl-docs .c-kind { display: none; }
    table.tbl-docs col.c-kind { width: 0; }

    table.tbl-docs col.c-doc { width: 66%; }
    table.tbl-docs col.c-who { width: 34%; }
}

table.tbl-menuperm { table-layout: fixed; min-width: 460px; }
table.tbl-menuperm col.c-title { width: 56%; }
table.tbl-menuperm col.c-show  { width: 22%; }
table.tbl-menuperm col.c-edit  { width: 22%; }

/* ---------- Floors for the narrow tables ----------
   These four hold two or three columns of short content, so there is nothing worth dropping —
   every column IS the table. What was wrong was the floor: sized for a comfortable desktop and
   then scrolling sideways on anything smaller, to save a wrap that these cells can perfectly well
   afford. Each cell already wraps, so a lower floor spends height rather than meaning. */
@media (max-width: 1100px) {
    table.tbl-settings { min-width: 420px; }
    table.tbl-roles    { min-width: 380px; }
    table.tbl-menuperm { min-width: 360px; }

    /* A LITTLE UNDER WHAT THE COLUMN MEASURES, on purpose. The dashboard is long enough to have a
       vertical scrollbar, and a scrollbar is 15-17px of the width the side column would otherwise
       have had. Sizing this to the column exactly meant it fitted while the page was short and
       started scrolling sideways the moment there was enough on it to scroll down — which is
       every real dashboard, and is exactly the sort of fault that is never reproducible on demand. */
    table.tbl-summary  { min-width: 340px; }

    /* THE HEADING IS WHAT SETS THE MINIMUM HERE, not the figures under it. "Unassigned" is a
       single word of ten characters with nowhere to break, so once its column falls below about
       80px the word hangs out over the next one — and the figures below it, being two digits, give
       no hint that the column was ever the problem. Three things buy those pixels back: the
       chevron goes (the whole row is already the link), the name gives up share it can afford to
       lose to a second line, and the headings take the smaller of the two label sizes. */
    table.tbl-summary .c-go, table.tbl-summary td.cell-go { display: none; }
    table.tbl-summary col.c-go   { width: 0; }
    table.tbl-summary col.c-name { width: auto; }
    table.tbl-summary col.c-num  { width: 24%; }

    table.tbl-summary thead th { font-size: var(--fs-2xs); padding-left: 6px; padding-right: 6px; }
    table.tbl-summary tbody td { padding-left: 6px; padding-right: 6px; }
}

/* THE AWKWARD BAND, and the only rule here with a lower bound as well as an upper one.
   Between 1101 and 1300 the split has NOT yet collapsed — it collapses at 1100 — so the dashboard
   still has a side column, and that column is at its narrowest: about 420px at 1150, which leaves
   the three figure columns around 71px each when the word "Unassigned" above them wants 77. Wider
   than this the column has room; narrower, the split has stacked and given it the whole width. It
   is only in between that anything is wrong, which is exactly why it survived every other check. */
@media (min-width: 1101px) and (max-width: 1300px) {
    table.tbl-summary .c-go, table.tbl-summary td.cell-go { display: none; }
    table.tbl-summary col.c-go   { width: 0; }
    table.tbl-summary col.c-name { width: auto; }
    table.tbl-summary col.c-num  { width: 24%; }

    table.tbl-summary thead th { font-size: var(--fs-2xs); white-space: normal;
                                 padding-left: 6px; padding-right: 6px; }
    table.tbl-summary tbody td { padding-left: 6px; padding-right: 6px; }
}

@media (max-width: 620px) {
    table.tbl-settings { min-width: 280px; }
    table.tbl-roles    { min-width: 260px; }
    table.tbl-menuperm { min-width: 280px; }
    table.tbl-summary  { min-width: 290px; }

    /* 12% of a 346px table is 41px, and the headcount chip that sits in it is wider than that.
       The role name can afford the difference; it is the only cell here that wraps well. */
    table.tbl-roles col.c-name   { width: 76%; }
    table.tbl-roles col.c-people { width: 24%; }
}

/* The smallest phones still sold — a 360px handset leaves about 326px inside the panel. At this
   width the job number is the only thing left that cannot be made to fit by giving it a bigger
   share, because the share is a percentage of a table that is already as small as it goes. So it
   takes the smaller label size as well: twelve characters of monospace at 11px rather than 12. */
@media (max-width: 400px) {
    table.tbl-jobs { min-width: 300px; }
    table.tbl-jobs .ref { font-size: var(--fs-2xs); }
    table.tbl-jobs tbody td, table.tbl-jobs thead th { padding-left: 6px; padding-right: 6px; }

    table.tbl-assets tbody td, table.tbl-assets thead th { padding-left: 6px; padding-right: 6px; }
}
table.tbl-menuperm td input[type="checkbox"] { width: 16px; height: 16px; }

/* Wider still: it carries a table of every screen in the system under two form fields. */
.modal-panel .modal-body > .panel { margin-bottom: 16px; }
.modal-panel .modal-body > .panel:last-child { margin-bottom: 0; }

/* ---------- Maintenance: lookups as an accordion, grouped by category ----------
   <details> rather than script. It is open/closed state with a disclosure control, which is what
   the element is; a scripted version would also be one more thing that stops working if the
   script fails to load. */
.lookup-heading {
    font-size: var(--fs-md);
    font-weight: 650;
    color: var(--navy-600);
    margin: 26px 0 10px;
    letter-spacing: -.01em;
}
.lookup-heading:first-of-type { margin-top: 4px; }

.lookup {
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--surface);
    box-shadow: var(--sh-xs);
    margin-bottom: 10px;
    overflow: hidden;
}

.lookup > summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 13px 18px;
    background: var(--navy-600);
    color: #fff;
    font-size: var(--fs-sm);
    font-weight: 600;
    cursor: pointer;
    list-style: none;
    user-select: none;
}
.lookup > summary::-webkit-details-marker { display: none; }
.lookup > summary:hover { background: var(--navy-500); }
.lookup > summary i { transition: transform .16s var(--ease); flex: 0 0 auto; }
.lookup[open] > summary i { transform: rotate(180deg); }

.lookup-body { border-top: 1px solid var(--line); }
.lookup-body > .tbl-wrap { border-top: 0; }

/* A day of the week, on or off. */
.day-toggle {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 7px 13px;
    border: 1px solid var(--line);
    border-radius: 20px;
    background: var(--surface);
    font-size: var(--fs-sm);
    cursor: pointer;
}
.day-toggle:has(input:checked) {
    border-color: var(--navy-400);
    background: color-mix(in srgb, var(--navy-400) 12%, var(--surface));
    font-weight: 500;
}
.day-toggle input { width: 15px; height: 15px; }

/* A footer that has a destructive action on the left and the ordinary ones on the right. */
.modal-foot .push { flex: 1 1 auto; }

.setting-value { font-size: var(--fs-sm); }
.colour-cell { display: inline-flex; align-items: center; gap: 9px; }
.swatch.sm { width: 20px; height: 20px; border-radius: 4px; border: 1px solid var(--line); display: inline-block; }

/* ---------- A stored file ----------
   Shown instead of the file input once something is uploaded. The body is a link that downloads
   it and the X takes it off; there is no upload-over-the-top, so what is stored is never in
   question. Sized to match an .inp so it sits in the same column as the fields around it. */
.file-card {
    position: relative;
    display: flex;
    align-items: stretch;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface);
    min-height: 62px;
}
.file-card:hover { border-color: var(--steel-2); }

.file-card-body {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 40px 10px 13px;
    min-width: 0;
    text-decoration: none;
    color: inherit;
}
/* The image itself, not an icon standing in for it. Fixed box with the picture fitted inside so
   a tall logo and a wide one produce the same shaped card, and a chequerboard behind it so a
   transparent PNG reads as transparent rather than as white. */
.file-card-thumb {
    flex: 0 0 auto;
    width: 46px;
    height: 46px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 3px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background-color: var(--surface);
    background-image:
        linear-gradient(45deg, var(--surface-2) 25%, transparent 25%, transparent 75%, var(--surface-2) 75%),
        linear-gradient(45deg, var(--surface-2) 25%, transparent 25%, transparent 75%, var(--surface-2) 75%);
    background-size: 12px 12px;
    background-position: 0 0, 6px 6px;
    overflow: hidden;
}
.file-card-thumb img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }

.file-card-body i { font-size: 1.5rem; color: var(--navy-500); flex: 0 0 auto; }
.file-card-body span { min-width: 0; }
.file-card-body strong {
    display: block;
    font-size: var(--fs-sm);
    font-weight: 500;
    color: var(--ink);
    overflow-wrap: anywhere;
}
.file-card-body small { display: block; font-size: var(--fs-xs); color: var(--steel); }
.file-card-body:hover strong { color: var(--navy-600); text-decoration: underline; }

/* Top right, over the body rather than beside it, so the card keeps one clean edge. */
.file-card-x {
    position: absolute;
    top: 6px;
    right: 6px;
    width: 26px;
    height: 26px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: var(--r-sm);
    background: transparent;
    color: var(--steel);
    font-size: .78rem;
    cursor: pointer;
}
.file-card-x:hover { background: var(--bad-bg); color: var(--bad); }

/* Carries its own padding and rule, because it is not always inside a panel footer —
   the severity modal puts one straight under the table. */
.pager {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    padding: 12px 18px;
    border-top: 1px solid var(--line);
    background: var(--surface-2);
    font-size: var(--fs-xs);
    color: var(--steel);
}

/* Inside a footer the footer already supplies both. */
.panel-foot.pager { padding: 12px 18px; border-top: 1px solid var(--line); }

/* ---------------------------------------------------------------- login --- */

.login-shell {
    min-height: 100vh;
    display: grid;
    grid-template-columns: 1fr 1fr;
    background: var(--surface);
}
.login-art {
    background: linear-gradient(155deg, var(--navy-700) 0%, var(--navy-900) 100%);
    color: #fff;
    padding: 48px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: clamp(20px, 4vh, 48px);
    position: relative;
    overflow: hidden;
}
/* A faint cold-store grid: racking bays seen from above. Decorative but drawn
   from the subject rather than a stock gradient. */
.login-art::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image:
        linear-gradient(rgba(255,255,255,.045) 1px, transparent 1px),
        linear-gradient(90deg, rgba(255,255,255,.045) 1px, transparent 1px);
    background-size: 46px 46px;
    mask-image: radial-gradient(ellipse at 70% 30%, #000 20%, transparent 72%);
}
.login-art > * { position: relative; z-index: 1; }
/* The uploaded logo occupies the top half of the panel.
   The box is a fixed proportion of the viewport; the image is fitted INSIDE it with
   object-fit:contain, so any shape an administrator uploads — tall, wide or square —
   fills as much of the box as it can without ever being stretched. */
.login-mark {
    flex: 0 0 auto;
    width: 100%;
    height: min(46vh, 460px);
    display: flex;
    align-items: center;
    justify-content: center;
    background: #fff;
    border-radius: var(--r-lg);
    padding: clamp(18px, 3vw, 40px);
    box-shadow: 0 14px 40px rgba(0, 0, 0, .3);
}

.login-mark img {
    display: block;
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
    object-fit: contain;
}

/* Shown until a logo is uploaded, so the panel is never blank. */
.login-wordmark {
    font-size: clamp(2rem, 6vw, 3.4rem);
    font-weight: 650;
    letter-spacing: -.03em;
    color: var(--navy-600);
    line-height: 1;
}
.login-wordmark span { color: var(--gold-500); }

.login-art h2 { font-size: 1.9rem; font-weight: 600; letter-spacing: -.02em; line-height: 1.25; max-width: 16ch; }
.login-art p { color: #a9bcd8; font-size: var(--fs-md); margin-top: 12px; max-width: 42ch; }
.login-art .legal { font-size: var(--fs-xs); color: #7e93b5; }

.login-form-side { display: flex; align-items: center; justify-content: center; padding: 48px 40px; }
.login-form { width: 100%; max-width: 350px; }
.login-form h1 { font-size: var(--fs-xl); font-weight: 650; letter-spacing: -.02em; }
.login-form .lede { color: var(--steel); font-size: var(--fs-base); margin: 6px 0 26px; }

@media (max-width: 900px) {
    .login-shell { grid-template-columns: 1fr; }
    .login-art { padding: 32px; min-height: auto; }
    .login-art h2 { font-size: 1.4rem; }

    /* Stacked, the art panel is only a banner — a full-size mark would push the
       sign-in form below the fold on a phone. */
    /* Stacked, the art panel is only a banner — a full-height mark would push the
       sign-in form below the fold on a phone. */
    .login-mark { height: 150px; padding: 14px 18px; }
    .login-wordmark { font-size: 2rem; }
}

/* ---------------------------------------------------------- responsive --- */

@media (max-width: 992px) {
    .app-sidebar { transform: translateX(-100%); }
    .app-main { margin-left: 0; }
    body.nav-open .app-sidebar { transform: translateX(0); }
    body.nav-open .sb-backdrop { display: block; }
    .app-content { padding: 18px 16px 40px; }
    .app-topbar { padding: 0 16px; }
}

@media (min-width: 993px) {
    body.nav-collapsed .app-sidebar { transform: translateX(-100%); }
    body.nav-collapsed .app-main { margin-left: 0; }
}

@media print {
    .app-sidebar, .app-topbar, .app-foot, .actions, .filter-bar { display: none !important; }
    .app-main { margin-left: 0; }
    .panel { box-shadow: none; border-color: #999; break-inside: avoid; }
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
}

/* ==========================================================================
   Utilities
   A deliberately small set. Bootstrap is not loaded — these cover the layout
   and spacing helpers the views actually use, and nothing more.
   ========================================================================== */

/* Spacing */
.mb-3 { margin-bottom: 14px; }
.mt-2 { margin-top: 8px; }

/* ---------------------------------------------------- branding preview --- */
/* Mirrors the sign-in panel so an administrator sees what they are getting. */

/* ------------------------------------------------------------- evidence --- */

/* A count beside a tab label — how many attachments, comments, history entries. Quiet, because
   it is a hint about what is behind the tab, not the point of it. */
.tab-count {
    display: inline-block;
    min-width: 18px;
    padding: 0 5px;
    margin-left: 5px;
    border-radius: 9px;
    background: var(--surface-3, rgba(11, 22, 48, .08));
    font-size: var(--fs-xs);
    font-variant-numeric: tabular-nums;
    color: var(--steel);
}
.tab.active .tab-count { background: rgba(255, 255, 255, .22); color: #fff; }

/* A person you can open. The whole row is the target, so it reads as one thing to double-click
   rather than a name that happens to be clickable. */
/* A FACE, A NAME AND WHATEVER ELSE — the shape every mention of a person in this system takes,
   whether it is a panel of its own or one cell of a table. It was only ever styled with
   .is-openable on it, so the same markup inside a table cell fell back to inline spans and the
   avatar sat on the text baseline. */
.person {
    display: flex;
    align-items: center;
    gap: 9px;
    min-width: 0;
}

.person .person-detail { display: flex; flex-direction: column; min-width: 0; }
.person .person-detail small { font-size: var(--fs-2xs); color: var(--steel-2); line-height: 1.3; }

.person.is-openable {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 9px 11px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    cursor: pointer;
    transition: border-color .12s var(--ease), background .12s var(--ease);
}
/* No hover treatment. It is a whole block of a person's details, not a word in a sentence, and
   tinting and re-bordering it on the way past made the panel flicker as the cursor crossed it.
   The cursor and the "Open record" affordance already say it is clickable. */
.person.is-openable:hover { border-color: var(--line); background: transparent; }
.person.is-openable, .person.is-openable:hover { color: var(--ink); text-decoration: none; }
.person.is-openable strong { color: var(--ink); }
.person.is-openable .push { flex: 1 1 auto; }

/* ---------- A comment thread ---------- */

.comment-thread { display: flex; flex-direction: column; gap: 14px; }

.comment { display: flex; gap: 11px; align-items: flex-start; }
.comment .avatar { flex-shrink: 0; }
.comment-body { min-width: 0; flex: 1 1 auto; }
.comment-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 2px; }
.comment-head strong { font-size: var(--fs-sm); }

/* pre-wrap so a comment typed over several lines keeps them. overflow-wrap so one long
   unbroken string — a part number, a URL — cannot push the panel wider than the page. */
.comment-text {
    margin: 0;
    font-size: var(--fs-sm);
    line-height: 1.5;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

/* ---------- Several photos of one kind, side by side ---------- */

/* One half of the Before/After split. Left aligned, so the label, the photos and the file box all
   start on the same edge as everything else in the form. */
.photo-half { display: flex; flex-direction: column; align-items: stretch; }

/* The note under the file box gets its own breathing room — against the input it read as part of
   the control rather than as a line about it. */
.photo-half .help { margin-top: 6px; }

/* BEFORE OVER AFTER. They were two columns of a grid, so each half had half the panel and a tile
   could only get smaller by there being more of them. Stacked, each half has the whole width. */
.photo-halves {
    display: flex;
    flex-direction: column;
    gap: 20px;
}

/* TEN OR MORE ACROSS, WHICH IS WHAT THE FULL WIDTH BUYS. These used to be 170px minimum on half
   the panel, on the argument that evidence read as file icons any smaller. That held while a card
   carried two or three photographs; with a dozen it turned one job card into several screens of
   scrolling, and the reader stopped being able to see the set at all.

   AUTO-FILL RATHER THAN AUTO-FIT, so two photographs stay two small tiles instead of stretching to
   half a metre each. A tile that wants a closer look has the full-screen viewer one click away. */
.photo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
    gap: 8px;
}

/* On a phone the Before and After halves stack, so each grid has the whole width to itself. Two
   photographs across still reads as a gallery; one across reads as a list you have to scroll, and
   three angles of a door seal would then be three screens deep. */
@media (max-width: 700px) {
    .photo-grid {
        grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
        gap: 8px;
    }
}

/* PICKED, AND IT HAS TO READ AT A GLANCE ACROSS A GRID OF TWENTY. A border alone disappears
   among tiles that already carry one; the ring sits outside the tile's own edge and the lift
   separates it from its neighbours without moving anything. */
.photo-thumb.is-selectable { cursor: pointer; }
.photo-thumb.is-selectable:hover { border-color: var(--navy-400); }

.photo-thumb.selected {
    border-color: var(--navy-600);
    box-shadow: 0 0 0 2px var(--navy-600), 0 2px 6px rgb(0 0 0 / .12);
}

/* THE PANEL THAT OPENS THE FILE DIALOG. A label pointed at the input, so the whole thing is the
   control and the keyboard reaches it exactly as it reached the box this replaces — no script
   between the reader and the file picker, which is the one part that has to keep working. */
.dropzone {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    padding: 26px 18px;
    border: 2px dashed var(--line);
    border-radius: var(--r-md);
    background: var(--surface-2);
    text-align: center;
    cursor: pointer;
}

.dropzone:hover { border-color: var(--navy-400); background: var(--surface); }

.dropzone:focus-within {
    border-color: var(--navy-600);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--navy-400) 30%, transparent);
}

.dropzone i { font-size: 26px; color: var(--navy-600); }
.dropzone strong { font-size: var(--fs-sm); color: var(--navy-700); }
.dropzone span { font-size: var(--fs-xs); color: var(--steel); max-width: 46ch; }

/* A VISIT YOU CAN PICK. The contractor visits are definition lists rather than table rows —
   they carry seven facts apiece and a row would flatten them — so the selected state is the same
   ring the photo tiles use rather than the table-row treatment. */
/* A COMMENT OF YOUR OWN IS PICKABLE. Somebody else's is read and nothing more, so it carries no
   hover and no ring — the difference has to be visible before you reach for Edit. */
.comment.is-selectable { cursor: pointer; border-radius: var(--r-sm); padding: 6px 8px; margin: -6px -8px; }
.comment.is-selectable:hover { background: var(--surface-2); }
.comment.is-selectable.selected { background: var(--surface-2); box-shadow: 0 0 0 2px var(--navy-600); }

.dl.is-selectable { cursor: pointer; border-radius: var(--r-sm); padding: 6px 8px; margin: -6px -8px; }
.dl.is-selectable:hover { background: var(--surface-2); }
.dl.is-selectable.selected {
    background: var(--surface-2);
    box-shadow: 0 0 0 2px var(--navy-600);
}

.photo-thumb {
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    overflow: hidden;
    background: var(--surface);
}

/* Sized by ratio rather than by a fixed height, so a tile that grows wider grows taller with it
   and the grid reflows down to a phone without the pictures turning into letterbox slots. */
.photo-thumb img {
    display: block;
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    background: var(--surface-2);
}

/* A ROW RATHER THAN A BLOCK, because the foot now carries two things: what is known about the
   photograph on the left, and what can be done with it on the right. */
.photo-thumb-foot {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    padding: 5px 7px;
    border-top: 1px solid var(--line);
    font-size: var(--fs-xs);
    color: var(--steel);
    line-height: 1.35;
}

.photo-thumb-foot .push { flex: 1 1 auto; }

/* THE WAY TO TAKE A PHOTOGRAPH OFF THE CARD, ON THE PHOTOGRAPH. A real submit now, associated with
   the removal form that sits outside the evidence form - the tile cannot NEST one, because browsers
   discard a form inside a form. Understated on purpose: this is the one act on the screen that
   makes the record smaller, and it asks before it does it. */
.photo-thumb-remove {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: var(--steel-2);
    white-space: nowrap;
    cursor: pointer;
}
.photo-thumb-remove:hover { color: var(--bad); text-decoration: none; }
.photo-thumb-remove:focus-visible { outline: 2px solid var(--gold-400); outline-offset: 2px; }

/* WHO UPLOADED IT AND FROM HOW FAR AWAY, on its own line under the rest of the foot. These facts
   used to be columns in a table on the Attachments tab; the photographs have left that tab and the
   anti-fraud detail travels with them rather than being lost on the way. */
.photo-thumb-meta {
    flex: 0 0 100%;
    font-size: var(--fs-2xs);
    color: var(--steel-2);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* AND WHEN THEY MAY NOT, IT SAYS SO. An absent control reads as a feature that does not exist,
   which is exactly how a compulsory-reason removal came to look like no removal at all. */
.photo-thumb-locked {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    color: var(--steel-2);
    white-space: nowrap;
    cursor: help;
}

/* THE ROW THE GALLERY POINTED AT. Arriving from a thumbnail lands on an anchor, and a table of
   near-identical rows with no mark on the one that was asked for is a reader hunting again. */
.tbl tr:target > td { background: var(--gold-100); }

/* Could not be confirmed — amber, so a doubtful shot is visible in a row of good ones. */
.photo-thumb.is-warn { border-color: var(--warn); }
.photo-thumb.is-warn .photo-thumb-foot { border-top-color: var(--warn); background: var(--warn-bg); color: var(--warn); }

/* Chosen but not yet sent. Same amber language as a doubtful one, plus a dashed edge so the two
   are not confused: one is on the card and questionable, the other is not on the card at all. */
.photo-thumb.is-pending { border: 1px dashed var(--warn); }
.photo-thumb.is-pending .photo-thumb-foot { border-top: 1px dashed var(--warn); background: var(--warn-bg); color: var(--warn); }

/* A HALF WITH NOTHING IN IT SAYS SO, IN WORDS. It used to be a dashed tile reading "None yet",
   which on a freshly raised card made the After half look like a slot somebody had failed to
   fill — when an empty After half is simply what a job that has not been done yet looks like.
   The sentence sits below the grid, and the preview script hides it the moment files are chosen. */
.photo-half .empty.is-line { margin-top: 2px; }
.empty.is-line[hidden] { display: none; }

/* The stored photo is a button that opens it full size. A button rather than a link because it
   opens a dialog rather than going anywhere — and it keeps keyboard focus and Enter for free. */
.evidence-photo-open {
    display: block;
    position: relative;
    width: 100%;
    padding: 0;
    border: 0;
    background: none;
    cursor: zoom-in;
    font: inherit;
}

/* The zoom hint only appears on approach; always on, it sat over the evidence itself. */
.evidence-photo-zoom {
    position: absolute;
    right: 8px;
    bottom: 8px;
    display: grid;
    place-items: center;
    width: 28px;
    height: 28px;
    border-radius: var(--r-sm);
    background: rgba(11, 22, 48, .62);
    color: #fff;
    font-size: .8rem;
    opacity: 0;
    transition: opacity .12s var(--ease);
}
.evidence-photo-open:hover .evidence-photo-zoom,
.evidence-photo-open:focus-visible .evidence-photo-zoom { opacity: 1; }
.evidence-photo-open:focus-visible { outline: 2px solid var(--gold-400); outline-offset: 2px; }

/* ---------- Full-screen photo viewer ----------

   Job cards only, and deliberately not a .modal: every dialog in this system is inset by the
   sidebar so the navigation stays visible, which is right for a form and wrong for a photograph.
   Somebody is looking for a hairline split in a door seal. Give them the whole screen. */

.lightbox[hidden] { display: none; }

.lightbox {
    position: fixed;
    inset: 0;
    z-index: 1200;          /* above the dialogs, which sit at 1060 */
    display: flex;
    flex-direction: column;
    background: rgba(6, 12, 26, .94);
}

.lightbox:focus { outline: none; }

.lightbox-bar {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 14px;
    color: #fff;
    font-size: var(--fs-sm);
    font-weight: 600;
    flex-wrap: wrap;        /* on a phone the zoom controls drop to their own line rather than
                               squeezing the title out of the bar */
}
.lightbox-bar .push { flex: 1 1 auto; }

/* Where you are in the group. Quiet — it is a position, not the subject. */
.lightbox-count {
    font-weight: 500;
    font-size: var(--fs-xs);
    color: rgba(255, 255, 255, .72);
}
.lightbox-count[hidden] { display: none; }

/* How far in the photograph is. Fixed width so the bar does not shuffle sideways as it changes
   between "Fit" and "400%". */
.lightbox-scale {
    min-width: 46px;
    text-align: center;
    font-size: var(--fs-xs);
    font-weight: 600;
    color: rgba(255, 255, 255, .82);
}

/* The buttons sit on near-black rather than on a panel, so the ghost style needs its own
   colours here — the default is navy text, which would be invisible. */
.lightbox-bar .btn-ghost {
    color: #fff;
    border-color: rgba(255, 255, 255, .35);
    background: rgba(255, 255, 255, .08);
}
.lightbox-bar .btn-ghost:hover {
    background: rgba(255, 255, 255, .2);
    border-color: rgba(255, 255, 255, .6);
    color: #fff;
}

.lightbox-stage {
    flex: 1 1 auto;
    display: flex;
    align-items: stretch;   /* the frame takes the full height of the stage; the step buttons
                               centre themselves in it */
    gap: 6px;
    padding: 0 14px 4px;
    min-height: 0;          /* lets the image shrink inside the flex column instead of overflowing */
    cursor: zoom-out;
}

/* The frame is what scrolls once the photograph is bigger than the screen. It fills the stage so
   the picture stays centred at fit, and takes the overflow when it is not. */
/* FLEX RATHER THAN GRID, and it is not a preference. The photograph is capped by max-height:100%,
   and a percentage resolves against the box it is measured in — inside an auto-sized grid ROW that
   box has no height to speak of, so the cap was quietly ignored and a tall photograph rendered past
   the bottom of the screen at "fit". A flex container stretched to the stage has a real height, so
   the cap means what it says. */
.lightbox-frame {
    flex: 1 1 auto;
    display: flex;
    /* SAFE centring, and it matters here. A centred item that outgrows its scroll container
       overflows equally in both directions, and the half that spills off the START edge cannot be
       scrolled back to — so the left of a zoomed photograph would simply be unreachable. "safe"
       gives up the centring the moment it would do that. */
    align-items: center;
    justify-content: center;
    align-items: safe center;
    justify-content: safe center;
    min-width: 0;
    min-height: 0;
    overflow: auto;
}
.lightbox-frame.is-zoomed { cursor: grab; }
.lightbox-frame.is-dragging { cursor: grabbing; }

.lightbox-frame img {
    display: block;
    /* NEVER SHRUNK BY THE FLEX BOX. Zooming sets an explicit width in pixels, and a flex item that
       is allowed to shrink would simply be squeezed back to the width of the screen — the zoom
       would appear to do nothing at all. */
    flex: 0 0 auto;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    cursor: default;
    /* A zoom step is a change of width, and easing it stops the picture jumping under the eye.
       The reduced-motion rule at the foot of this file flattens it for anybody who asked. */
    transition: width .12s var(--ease);
}
.lightbox-frame.is-zoomed img { cursor: inherit; }

/* Step to the photograph either side, WITHIN THIS GROUP. Sat over the surround rather than in the
   bar, because that is where a hand goes on a picture — and where a thumb reaches on a phone. */
.lightbox-step {
    flex: 0 0 auto;
    align-self: center;
    display: grid;
    place-items: center;
    width: 44px;
    height: 64px;
    border: 1px solid rgba(255, 255, 255, .28);
    border-radius: var(--r-sm);
    background: rgba(255, 255, 255, .08);
    color: #fff;
    font-size: 1.2rem;
    cursor: pointer;
    transition: background .12s var(--ease);
}
.lightbox-step:hover:not(:disabled) { background: rgba(255, 255, 255, .22); }
.lightbox-step:focus-visible { outline: 2px solid var(--gold-400); outline-offset: 2px; }
.lightbox-step[hidden] { display: none; }

/* THE ENDS OF A GROUP ARE ENDS. Dimmed rather than removed, so the reel does not change width as
   it is walked — and so it is obvious that there is nothing further this way rather than that the
   button has gone missing. */
.lightbox-step:disabled { opacity: .25; cursor: default; }

.lightbox-hint {
    padding: 0 14px 10px;
    text-align: center;
    font-size: var(--fs-xs);
    color: rgba(255, 255, 255, .5);
}

/* On a phone the bar has no room for a word beside every icon, and there are no arrow keys to
   tell anybody about. */
@media (max-width: 700px) {
    .lightbox-word { display: none; }
    .lightbox-hint { display: none; }
    .lightbox-step { width: 34px; height: 52px; }
    .lightbox-stage { padding: 0 6px 4px; gap: 4px; }
}

.checklist {
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    overflow: hidden;
}

.checklist-row {
    display: flex;
    gap: 9px;
    align-items: flex-start;
    padding: 10px 13px;
    border-bottom: 1px solid var(--line-2);
    font-size: var(--fs-sm);
    line-height: 1.45;
    margin: 0;
}
.checklist-row:last-child { border-bottom: none; }
.checklist-row:hover { background: var(--surface-2); }
.checklist-row small { display: block; color: var(--steel-2); font-size: var(--fs-xs); margin-top: 2px; }
.checklist-row i { margin-top: 2px; flex-shrink: 0; }

/* --------------------------------------------------------- severity cards --- */
/* One card per severity, each one a button that opens its own list. The bar
   chart this replaced showed a length relative to whichever severity happened to
   be largest — a shape that changed meaning every time the data moved. */

/* Deliberately compact — these are a secondary cut of work the tiles above already count.
   Two by two rather than four across: in a half-width panel four in a row left the cards thin
   and the panel short, so it sat beside the branch table with a block of empty white under it. */
.sev-cards {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /* Rows share the height the panel gives them, so the cards grow to fill it rather than
       sitting short with white underneath. */
    grid-auto-rows: minmax(118px, 1fr);
    gap: 10px;
}

@media (max-width: 1100px) {
    .sev-cards { grid-template-columns: repeat(auto-fit, minmax(165px, 1fr)); }
}

.sev-card {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    /* Band at the top, figure in the middle, note at the foot — so a taller card spreads its
       content instead of bunching it up and leaving the space at the bottom. */
    justify-content: space-between;
    gap: 1px;
    text-align: left;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    padding: 11px 13px 12px;
    cursor: pointer;
    font: inherit;
    position: relative;
    overflow: hidden;
    transition: border-color .12s var(--ease), box-shadow .12s var(--ease), transform .12s var(--ease);
}

/* A band with nothing in it still opens — confirming it is empty is a real answer — but it
   recedes so the bands that do have work read first. */
.sev-card.is-quiet { background: var(--surface-2); }
.sev-card.is-quiet .sev-card-value { color: var(--steel-2); font-weight: 500; }

/* ------------------------------------------------------------ risk strip --- */
/* One line, full width, for something that is neither a job card count nor a list: right now
   competency expiry. It reads as a statement rather than a figure because the number on its
   own means nothing — "3" is only alarming once you know three WHAT. */

.risk-strip {
    display: flex;
    align-items: center;
    gap: 11px;
    width: 100%;
    margin-top: 16px;
    padding: 13px 16px;
    border: 1px solid var(--line);
    border-left-width: 3px;
    border-radius: var(--r-md);
    background: var(--surface);
    font: inherit;
    font-size: var(--fs-sm);
    color: var(--ink);
    text-align: left;
    cursor: pointer;
    transition: border-color .12s var(--ease), box-shadow .12s var(--ease);
}
.risk-strip:hover { box-shadow: var(--shadow-sm); }
.risk-strip:focus-visible { outline: 2px solid var(--gold-400); outline-offset: 2px; }

.risk-strip > i:first-child { font-size: 1.05rem; flex-shrink: 0; }
.risk-text { flex: 1; line-height: 1.5; }
.risk-go { color: var(--steel-2); opacity: 0; transition: opacity .12s var(--ease); }
.risk-strip:hover .risk-go, .risk-strip:focus-visible .risk-go { opacity: 1; }

.risk-strip.is-bad  { border-left-color: var(--bad);  background: var(--bad-bg); }
.risk-strip.is-bad > i:first-child { color: var(--bad); }
.risk-strip.is-warn { border-left-color: var(--warn); background: var(--warn-bg); }
.risk-strip.is-warn > i:first-child { color: var(--warn); }
.risk-strip.is-ok   { border-left-color: var(--ok); }
.risk-strip.is-ok > i:first-child { color: var(--ok); }

/* The severity colour as a stripe, so the card is identifiable before it is read. */
.sev-card::before {
    content: "";
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 3px;
}
.sev-card.s-critical::before { background: var(--sev-critical); }
.sev-card.s-high::before     { background: var(--sev-high); }
.sev-card.s-medium::before   { background: var(--sev-medium); }
.sev-card.s-low::before      { background: var(--sev-low); }

.sev-card:hover {
    border-color: var(--navy-400);
    box-shadow: var(--shadow-sm);
    transform: translateY(-1px);
}
.sev-card:focus-visible {
    outline: 2px solid var(--gold-400);
    outline-offset: 2px;
}

.sev-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 4px;
}
.sev-card-head i { color: var(--steel-2); font-size: .75rem; opacity: 0; transition: opacity .12s var(--ease); }
.sev-card:hover .sev-card-head i,
.sev-card:focus-visible .sev-card-head i { opacity: 1; color: var(--navy-500); }

.sev-card-value {
    font-size: 1.35rem;
    font-weight: 600;
    line-height: 1.15;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
}

.sev-card-foot {
    font-size: var(--fs-xs);
    color: var(--steel);
    margin-top: 2px;
    display: flex;
    align-items: center;
    min-height: 22px;
}

/* ---------------------------------------------------------------- modal --- */
/* Used by the severity drill-in. Deliberately plain: a backdrop, a panel that
   scrolls its own body, escape and click-outside to close.

   THE OVERLAY STOPS AT THE SIDEBAR. A modal spanning the whole viewport put its
   panel in the middle of the screen, which is left of the middle of the working
   area — and since the sidebar sits at z-index 1040 against the modal's 100, the
   navigation painted straight over the panel and clipped the first column off it.

   Offsetting by the sidebar width centres the panel in the pane the work is
   actually in, and leaves navigation legible and un-dimmed rather than half
   buried under a backdrop. The z-index still has to clear the topbar (1030),
   which the modal does cover. */

.modal[hidden] { display: none; }

.modal {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: var(--sidebar-w);
    z-index: 1060;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;

    /* A DIALOG DOES NOT INHERIT TYPOGRAPHY FROM WHEREVER ITS MARKUP HAPPENS TO SIT.
       ==========================================================================
       This is a real fault that shipped, and it took three goes to find because it is invisible
       in the file you are reading. The owner: "the text is right aligned??????? its looks
       aweful!"

       A dialog is fixed to the viewport and covers the screen, but in the DOM it is declared
       wherever its opener is — and the time-off dialog is declared inside the morning review's
       button holder:

           .ev-line-do { flex: 0 0 auto; text-align: right; white-space: nowrap; }

       Right-aligned and nowrap, so a row of buttons ends on one clean edge. Both of those
       properties INHERIT, so the dialog took them: every paragraph in it was right-aligned, and
       none of them wrapped — the lead sentence laid out 1359px wide inside a 798px panel, and
       575px of the form was off the side.

       READING THE MARKUP WOULD NEVER HAVE SHOWN THIS. Nor did rendering the dialog on its own,
       which is what I did first and why I wrongly called it a screenshot artefact. It only
       appears in the page it lives in.

       SO EVERY DIALOG SAYS IT, ONCE, HERE. Any dialog declared inside a nowrap cell, a
       right-aligned toolbar or a centred empty state would have inherited the same thing. */
    white-space: normal;
    text-align: left;
}

/* Wherever the sidebar is not taking up space, the modal takes the full width —
   these must track the .app-main margin rules exactly or the panel sits off-centre. */
@media (max-width: 992px) {
    .modal { left: 0; padding: 16px; }
}

@media (min-width: 993px) {
    body.nav-collapsed .modal { left: 0; }
}

.modal-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(15, 34, 49, .48);
    backdrop-filter: blur(1.5px);
}

.modal-panel {
    position: relative;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-lg);

    /* ---------------------------------------------------------------------------
       THERE ARE THREE SIZES OF DIALOG IN THIS SYSTEM AND THERE WILL NOT BE A FOURTH.

         .modal-panel.is-ask   — one short question. Narrow.
         .modal-panel.is-form  — a form. The ordinary one.
         .modal-panel          — everything else, and it fills the screen.

       There were six. Every dialog was its own size, so the screen jumped as you moved
       between them and nothing looked like it belonged to the same system. The owner:
       "all modals are their own size. like it must if large info be almost the whole
       screen?" — which is exactly what the bare class now does.

       THE BARE CLASS IS THE BIG ONE ON PURPOSE. A dialog carrying a table is the case
       that suffers from being small, and a dialog that forgets to say which size it is
       should land on the forgiving end rather than the cramped one.

       A FLOOR AS WELL AS A CEILING. Height decided by content alone made a dialog jump
       under the cursor as you paged through it: four rows gave a squat panel and ten a
       tall one.
       --------------------------------------------------------------------------- */
    width: min(1560px, 100%);
    min-height: min(80vh, 800px);
    max-height: min(92vh, 1100px);

    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.modal-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 14px;
    /* The same navy bar a panel gets. A dialog is a panel that happens to be floating, and it was
       the last place in the system still announcing itself in grey. */
    background: var(--navy-600);
    color: #fff;
    flex-shrink: 0;
}

/* The close button and anything else on the bar, legible against navy rather than white. */
.modal-head .btn-ghost {
    background: rgba(255, 255, 255, .12);
    border-color: rgba(255, 255, 255, .28);
    color: #fff;
}
.modal-head .btn-ghost:hover {
    background: rgba(255, 255, 255, .22);
    border-color: rgba(255, 255, 255, .45);
    color: #fff;
}

.modal-head h2 {
    font-size: var(--fs-sm);
    font-weight: 650;
    color: #fff;
    margin: 0;
}

/* Takes the slack left by the minimum height, so the extra room lands below the table
   rather than leaving the panel with a gap under its own bottom border.

   THE PADDING BELONGS HERE, NOT ONLY ON .modal-form. It was set on the form variant alone, so
   every dialog that merely shows something — the log entry, the sent message, the dashboard
   drill-down — ran its text hard into the panel border on all four sides while the dialogs
   beside them sat properly inset. A dialog that genuinely wants its content edge to edge already
   says so with .is-flush or an inline zero, and both of those still win. */
.modal-body { flex: 1 1 auto; overflow-y: auto; padding: 14px; }

/* Stops the page behind scrolling while the modal is up. */
body.modal-open { overflow: hidden; }

/* ---------- A modal that asks a question rather than showing a table ----------
   Most of the content area, not a 560px column down the middle of it. At 560px a dialog with
   eight fields — the menu item editor — stacked them in a strip barely wider than the labels,
   with the whole right-hand side of the screen sitting empty behind the dim. Height still comes
   from the content, so a two-line confirmation does not become a tall empty box. */
.modal-panel.is-form {
    width: min(80%, 980px);

    /* A FLOOR, SMALL. A form is as tall as its fields, but a two-field dialog whose last field
       is a dropdown had the list hanging below the panel's own edge, reading as text falling out
       of it. This is what .is-roomy used to be, and it is now simply what a form does. */
    min-height: min(46vh, 420px);

    /* SAID OUTRIGHT RATHER THAN INHERITED. A form is read from the left, every time, whatever the
       panel it opens over happens to be doing. */
    text-align: left;
}

/* A flex item will not shrink below its content unless it is told it may, which is the ordinary
   reason a column of fields refuses to narrow.

   THERE WAS AN `overflow-x: hidden` HERE AND IT HAD TO GO. It was put in to stop a sideways
   scrollbar, and it did — by clipping 575px of the form and taking the scrollbar away with it,
   which is strictly worse: a scrollbar at least tells somebody there is more. The overflow was
   inherited nowrap, and it is fixed where it starts, on .modal above. */
.modal-panel.is-form .modal-body { min-width: 0; }

/* A dialog body holding a toolbar and a table rather than a stack of fields. The padding moves
   onto the pieces that need it, so the toolbar and the table headings run edge to edge the way
   they do on a panel. */
.modal-body.is-flush { padding: 0; }
.modal-body.is-flush .grid-toolbar { border-radius: 0; border-top: 0; }
.modal-body.is-flush .tbl-wrap { border-radius: 0; }

/* A dialog that only asks one short question. Kept narrow deliberately: a single sentence and
   two buttons stretched across 980px reads as a page, not as a prompt. */
.modal-panel.is-form.is-ask { width: min(52%, 560px); }

/* ---------- The allocation board: picking a person and a day in one row ----------
   The picker and the button are the two halves of one decision, so they sit on one line and are
   lined up by their BOTTOM edges — the picker carries a caption above it and the button does not,
   and aligning by the top left the button hanging against the caption instead of the control.
   Pushed right, like every other action row in this system. */
.assign-day {
    margin-left: auto;
    display: flex;
    align-items: flex-end;
    gap: 10px;
}
.assign-day-field { display: flex; flex-direction: column; gap: 3px; }
.assign-day-field .inp { min-height: 31px; padding: 5px 10px; }

/* The handover reason, which only appears on a reassignment. It is a sentence rather than a date,
   so it takes what room is going — and the row wraps rather than squeezing the picker and the
   button when a panel is narrow. */
.assign-day-field.is-why { flex: 1 1 260px; min-width: 0; }
.assign-day { flex-wrap: wrap; }

/* Add and Reassign sit in one row as two separate forms — they post to two different handlers and
   only one of them carries a reason and a day. display:contents would collapse the forms into the
   row, but the reassign form needs to stay a row of its own so its own fields line up.

   IT WRAPS, AND .assign-day AROUND IT ALREADY DID. The row outside was told to wrap and this one
   was not, so on a handset the reassign form stayed 491px wide inside a 356px panel and ran 148px
   past it — taking the Reassign button with it, into a panel that clips. Wrapping one of two
   nested flex rows is half a rule. */
.assign-inline { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }

/* Matched to the picker exactly. A button two pixels taller lines its bottom up and stands proud
   at the top, which reads as a wobble along the row rather than as two things on one line. */
.assign-day .btn { min-height: 31px; }

/* THE DAY THAT WAS PICKED, MARKED ON THE WEEK. Clicking a column fills the picker in, and without
   this the only sign it worked is a date field several inches away that the reader was not
   looking at. */
.day-col[data-day] { cursor: pointer; }
/* The allocation board: a day under a name is clicked to make it the day the work starts. */
.picks-a-day .day-col.is-static { cursor: pointer; }
.picks-a-day .day-col.is-static:hover { background: var(--surface-2); }

.day-col[data-day].is-chosen {
    outline: 2px solid var(--gold-500, #c8a24a);
    outline-offset: -2px;
    border-radius: var(--r-sm, 6px);
}

/* A dialog whose whole point is looking at something — a filed PDF, a scanned certificate. It
   takes the window, because a document read at four fifths of the screen is one the reader gives
   up on and downloads instead, which is the register losing its purpose. Dillan: "make attachments
   view bigger ... view modal bigger too". */
.modal-panel.is-viewer {
    width: min(100%, 1800px);
    height: min(94vh, 1200px);
    min-height: min(94vh, 1200px);
}
.modal-panel.is-viewer .modal-body { flex: 1 1 auto; min-height: 0; }
.modal-panel.is-viewer .modal-body > iframe,
.modal-panel.is-viewer .modal-body > img { width: 100%; height: 100%; }

/* ROOM BELOW A SHORT FORM. A dialog with two fields is shorter than the option list a browser
   draws when the second of them is a select, so the list hung below the dialog's own edge and read
   as text falling out of it. The floor gives the list somewhere to land. */

/* The form IS the panel below the header — head, scrolling body, fixed footer — so the
   buttons stay put while a long list of candidates scrolls behind them. */
.modal-form {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}
.modal-form > .modal-body { padding: 14px; }
.modal-form .txt { min-height: 92px; }

/* .field only spaces a label from its own input. Stacked, that put the same 5px between
   "Currently assigned to" and its box as between that box and the next label, so the two
   questions read as one four-line block. */
.modal-form .field + .field { margin-top: 15px; }

/* Except inside a grid, where the gap already does it. The sibling margin was landing on every
   cell but the first, so the top row of a two-column dialog sat fifteen pixels above the one
   beside it — "First name" and "Last name" on different lines, which reads as a mistake because
   it is one. */
.modal-form .form-grid > .field + .field { margin-top: 0; }

/* A tickbox following a field, and one following another tickbox. These inherited nothing at all,
   so "Active" sat hard against the box above it while every other pair in the dialog had 15px —
   the last question always looked like part of the one before it. */
.modal-form .field + .check,
.modal-form .check + .check,
.modal-form .help + .check { margin-top: 15px; }

/* Guidance under a tickbox belongs to that tickbox, so it stays close to it. */
.modal-form .check + .help { display: block; margin-top: 5px; }

.modal-foot {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    padding: 10px 14px;
    border-top: 1px solid var(--line);
    background: var(--surface-2);
    flex-shrink: 0;
}

/* A heading inside a dialog that has more than one part to it. A plain rule and a label rather
   than a nested panel — a card inside a card gives the same content two title bars. */
.dialog-section {
    font-size: var(--fs-sm);
    font-weight: 650;
    color: var(--navy-600);
    margin: 20px 0 10px;
    padding-top: 14px;
    border-top: 1px solid var(--line);
}

/* What the action does, restated inside the dialog. The panel behind is dimmed, so the
   dialog cannot assume the sentence next to the button is still readable. */
.dialog-lead {
    margin: 0 0 16px;
    font-size: var(--fs-sm);
    color: var(--steel);
    line-height: 1.55;
}

/* ---------- The list of what is about to be saved ----------
   Shown before anything is written. Each line carries its own mark and colour so granting and
   revoking can be told apart at a glance rather than by reading the verb on every row — the
   case this exists for is a box ticked by accident among twenty-two that were not. */
.change-list {
    margin: 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.change {
    position: relative;
    padding: 9px 12px 9px 34px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface-2);
    font-size: var(--fs-sm);
    line-height: 1.45;
    color: var(--ink-2);
}

.change::before {
    position: absolute;
    left: 12px;
    top: 9px;
    font-weight: 700;
    font-family: var(--mono, monospace);
}

.change.is-add {
    border-color: color-mix(in srgb, var(--ok) 40%, var(--line));
    background: var(--ok-bg);
}
.change.is-add::before { content: "+"; color: var(--ok); }

.change.is-remove {
    border-color: color-mix(in srgb, var(--bad) 40%, var(--line));
    background: var(--bad-bg);
}
.change.is-remove::before { content: "\2212"; color: var(--bad); }

.change.is-edit {
    border-color: color-mix(in srgb, var(--warn) 40%, var(--line));
    background: var(--warn-bg);
}
.change.is-edit::before { content: "\2192"; color: var(--warn); }

/* A value the dialog is showing, not asking for — the current assignee. Shaped like an
   input so it sits in the same column as the field under it, but flat and unfocusable so
   it never reads as something you can change here. */
.field-static {
    display: flex;
    align-items: center;
    min-height: 38px;
    padding: 7px 12px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface-2);
}

/* The strip above the drill-in table, and above a single job card. */
.drill-head {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 18px;
    border-bottom: 1px solid var(--line-2);
    flex-wrap: wrap;
}

/* ------------------------------------------------- job card in the modal --- */
/* Level three of the drill-down: one card, read-only. Two columns of facts over a
   timeline, so the state of the work and how it got there are on one screen. */

.card-detail { padding: 16px 18px 20px; }

.cd-lead { margin-bottom: 16px; }
.cd-lead h3 {
    font-size: var(--fs-lg);
    font-weight: 600;
    color: var(--ink);
    line-height: 1.35;
    margin: 0 0 6px;
}
.cd-where {
    font-size: var(--fs-sm);
    color: var(--steel);
    margin: 0;
    display: flex;
    align-items: center;
    gap: 6px;
}
.cd-action {
    font-size: var(--fs-sm);
    color: var(--ink);
    background: var(--surface-2);
    border-left: 3px solid var(--navy-400);
    border-radius: var(--r-xs);
    padding: 9px 12px;
    margin: 10px 0 0;
    line-height: 1.5;
}

/* Panels run the full width and stack. Two narrow columns forced every fact onto its own
   line and made the card twice as tall as it needed to be; across the full width the facts
   sit side by side and the whole thing fits on one screen. */
.cd-panel {
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    padding: 14px 16px;
    background: var(--surface);
    min-width: 0;
    margin-bottom: 12px;
}
.cd-panel:last-child { margin-bottom: 0; }
.cd-panel h4 {
    font-size: var(--fs-xs);
    font-weight: 650;
    color: var(--navy-600);
    margin: 0 0 12px;
}

/* Facts sit across the panel, each label above its value. Every cell is the same
   width, so the labels line up on one baseline grid instead of stepping down the page. */
.cd-facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: 12px 18px;
    margin: 12px 0 0;
    align-items: start;
}

.cd-facts dt {
    font-size: var(--fs-xs);
    font-weight: 500;
    color: var(--steel);
}
.cd-facts dd {
    margin: 4px 0 0;
    font-size: var(--fs-sm);
    color: var(--ink);
    line-height: 1.5;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;

    /* A flex item will not shrink below its content without this, so one long value pushed
       its own cell wider than the grid track and the last column ran off the panel. */
    min-width: 0;
}

/* NOTHING in a facts cell may run past its column. A chip is nowrap by default, which is
   right in a table where a column can be widened to suit it, and wrong in this grid where
   the track width is fixed and the chip must fit whatever it is given. */
.cd-facts .chip { white-space: normal; }

/* A cell whose value is a state and a timestamp stacked, rather than one line. */
.cd-facts dd.cell-stack {
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
}
.cd-facts dd.cell-stack .stamp-note { margin-top: 0; }

/* The history runs to its full length and the DIALOG scrolls it, along with everything else
   on the card. Capping it here gave the modal two scrollbars sitting inside one another —
   whichever one the wheel happened to be over moved, which is never the one you meant. One
   scrollable region per dialog; the trail simply carries on to the bottom of the card. */

/* A panel whose point is the button on its right. */
.cd-action-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
}
.cd-action-row h4 { margin-bottom: 8px; }
.cd-action-chips { display: flex; flex-wrap: wrap; gap: 5px; }

/* ------------------------------------------------------- evidence modal --- */

.ev-pair {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 12px;
    align-items: stretch;
}

.ev-shot {
    display: block;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    overflow: hidden;
    background: var(--surface-2);
}
.ev-shot img { display: block; width: 100%; height: auto; }

.ev-meta { margin-top: 9px; }
.ev-finding {
    font-size: var(--fs-xs);
    color: var(--steel);
    line-height: 1.5;
    margin-top: 3px;
}

.ev-empty {
    border: 1px dashed var(--line);
    border-radius: var(--r-sm);
    padding: 34px 16px;
    text-align: center;
    color: var(--steel-2);
    background: var(--surface-2);
}
.ev-empty i { font-size: 1.6rem; display: block; margin-bottom: 7px; opacity: .5; }
.ev-empty p { font-size: var(--fs-sm); margin: 0; }

.ev-list { margin-top: 4px; }
.ev-line {
    display: flex;

    /* IT WRAPS, AND ON A HANDSET IT HAS TO.
       ====================================
       This was a flex row that never wrapped, holding a sentence and a button. A flex item will
       not shrink below its own content unless it is told it may, so the sentence kept its width
       and pushed the button along — 199px past the panel at 390px wide. The panel clips, so on a
       phone the "Seen" button on the morning review was not merely awkward: it was not there, and
       nothing on the screen said so.

       THE PANEL WAS NOT AT FAULT FOR CLIPPING. A card with rounded corners has to. The row is at
       fault for handing it something wider than itself.

       Wrapping was already switched on for the one case somebody had met — a time-off form opened
       inside the line — which is the tell: the rule was right and its condition was too narrow. */
    flex-wrap: wrap;

    gap: 9px;
    align-items: flex-start;
    padding: 9px 0;
    border-bottom: 1px solid var(--line-2);
    font-size: var(--fs-sm);
    line-height: 1.45;
}

/* THE SENTENCE GIVES WAY BEFORE THE BUTTON DOES. min-width: 0 lets it narrow past its longest
   line, and a basis of 220px means that below about that it takes the whole row and the buttons
   drop underneath — where they are still on the same clean right edge. */
.ev-line-text { min-width: 0; flex: 1 1 220px; }

/* The description beside the reference chip. It is prose and wraps; the chip beside it is a
   number and does not. */
.ev-line-what { overflow-wrap: anywhere; }
.ev-line:last-child { border-bottom: none; }
.ev-line i { margin-top: 2px; flex-shrink: 0; color: var(--steel-2); }
.ev-line.is-done i { color: var(--ok); }
.ev-line-text { color: var(--ink); }
.ev-line:not(.is-done) .ev-line-text { color: var(--steel); }

/* A LINE THAT GOES A LEVEL DEEPER, in the drill panel. The table rows have said so since the
   dashboard was written — see tr.is-drillable — and My Work's day drill lists its job cards as
   lines rather than as a table, because a day holds three of them and a seven-column table inside
   a dialog is a sideways scrollbar. Same promise, same hover, same focus ring. */
.ev-line.is-drillable {
    cursor: pointer;
    padding-left: 6px;
    padding-right: 6px;
    margin: 0 -6px;
    border-radius: 6px;
}

.ev-line.is-drillable:hover { background: var(--surface-2); }

/* THE WHOLE LINE OPENS THE CARD, NOT THE NUMBER ON IT.
   ===================================================
   The owner: "let that also be clickable to open the job card for a manager, like it says open, i
   might want to see when its due or a quick check, now i have to click the actual JC- part, which
   the users wont know."

   HE IS DESCRIBING A TARGET NOBODY CAN FIND. The reference chip is eight characters of mono text
   at the far left of a line whose readable part — the description, the person, the dates — is
   everything to the right of it, and none of that was clickable. So the one part of the row that
   looked like data was the only part that was a control.

   A STRETCHED LINK RATHER THAN A ROW CLICK HANDLER. The line already contains a form with a
   button in it, and an anchor cannot legally wrap a form — so the anchor stays exactly where it
   is and grows a transparent layer over the row instead. It is still a real link: middle-click,
   open in a new tab, tab to it, all unchanged. The Seen button is lifted above the layer so it
   keeps its own click. */
.ev-line.is-openable { position: relative; cursor: pointer; }

.ev-line.is-openable .ref::after {
    content: "";
    position: absolute;
    inset: -1px -6px;
    border-radius: 6px;
}

/* Above the layer, so the button and the form still take their own clicks. */
.ev-line.is-openable form,
.ev-line.is-openable .btn { position: relative; z-index: 1; }

.ev-line.is-openable:hover { background: var(--surface-2); }

/* THE FACTS THAT ARE WORTH A COLUMN, at the end of the line and out of the sentence.
   The owner: "maybe even add the due date as a column... and also who is assigned."
   Right-aligned so they make an edge down the panel rather than tracking the length of each
   description, and they wrap under the sentence on a handset with the rest of the row. */
.ev-line-facts {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 3px;
    text-align: right;
    font-size: var(--fs-sm);
    line-height: 1.35;
}

/* The date and its verdict on one line, wrapping together if the panel is narrow. */
.ev-line-due { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }

.ev-line-facts .lbl-soft { font-size: var(--fs-xs); }
.ev-line-facts strong { color: var(--ink); font-weight: 550; }
.ev-line.is-drillable:focus-visible { outline: 2px solid var(--gold-400); outline-offset: -2px; }
.ev-line.is-drillable .ref { background: var(--surface-2); }
.ev-line.is-drillable:hover .ref {
    background: var(--gold-100);
    border-color: var(--gold-400);
    color: var(--gold-600);
}

/* ---------------------------------------------------------------- people --- */

.person { display: flex; gap: 10px; align-items: flex-start; }

.person-detail { min-width: 0; line-height: 1.4; }
.person-detail strong { display: block; font-weight: 600; }
.person-detail small {
    display: block;
    font-size: var(--fs-2xs);
    color: var(--steel-2);
}

/* An inactive row recedes without disappearing. */
tr.row-muted td { opacity: .58; }
tr.row-muted:hover td { opacity: 1; }

/* ============================================================== form pages === */

.form-hero {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 18px 22px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--sh-xs);
    margin-bottom: 16px;
    flex-wrap: wrap;
}

/* The account menu's rows. Separate controls, so they are spaced as separate controls rather
   than stacked edge to edge with their hover states touching. */
.menu-stack { display: flex; flex-direction: column; gap: 5px; padding: 7px; }

/* "6 of 9" — the denominator is context, not part of the figure, so it does not take the figure's
   size. Without this the tile reads as a four-digit number. */
.stat-of { font-size: var(--fs-sm); font-weight: 400; color: var(--steel); margin-left: 5px; }

/* ---------- A record, read rather than edited ---------- */

/* No ceiling. This page fills the content area like every other screen in the system — a panel
   that stops short of the width its neighbours use reads as cut off, not as restrained. The
   three-column grid inside is what keeps the fields grouped at any width. */

.form-hero .avatar.lg { width: 46px; height: 46px; font-size: var(--fs-md); }
.form-hero-text { flex: 1; min-width: 200px; }
.form-hero-text h1 { font-size: var(--fs-xl); font-weight: 650; letter-spacing: -.015em; }
.form-hero-text p { font-size: var(--fs-sm); color: var(--steel); margin-top: 3px; }

/* ---------- A form read top to bottom ----------
   Sections stack, so the page is still read in one order — but each section's CONTENT fills the
   width, the way every other page in the system does.

   The first attempt capped the whole form at 860px. That fixed the line length and created two
   worse problems: all the leftover width piled up on one side rather than being shared, and the
   fields, branches and pick-lists were forced into single file, so a form with about twenty
   controls on it ran to several screens of mostly empty panel. Width is not the enemy here —
   an unfilled row is. */
.form-single > .panel { margin-bottom: 14px; }
.form-single .panel-body { padding: 16px 18px; }

/* ---------- One question per row ----------
   For panels where the fields are a sequence rather than a set: username then password, job
   title then employment then capacity. Stacked, so they are read and answered in order.

   The guidance sits BESIDE the control rather than under it. That is what stops a stacked form
   being a narrow column of inputs against an empty half-panel — the same complaint that a
   fixed-width form produced — and it puts each explanation level with the thing it explains
   instead of pushing the next question further down the page. */
.form-rows { display: flex; flex-direction: column; }

.form-row {
    display: grid;
    grid-template-columns: minmax(0, 380px) minmax(0, 1fr);
    gap: 22px;
    align-items: start;
    padding: 14px 0;
    border-top: 1px solid var(--line-2);
}
.form-row:first-child { padding-top: 0; border-top: 0; }
.form-row:last-child { padding-bottom: 0; }

/* Below the point where two columns would squeeze the control, the note goes back underneath. */
@media (max-width: 820px) {
    .form-row { grid-template-columns: minmax(0, 1fr); gap: 6px; }
}

/* Aligned to the control, not the label above it. */
.row-note {
    margin: 0;
    padding-top: 21px;
    font-size: var(--fs-xs);
    color: var(--steel-2);
    line-height: 1.5;
}
@media (max-width: 820px) { .row-note { padding-top: 0; } }

.row-note strong { color: var(--ink-2); font-weight: 600; }

/* A row whose control needs the whole panel rather than the 380px field column — a set of choice
   cards, not a field. The note becomes a caption under it instead of a column beside it. */
.form-row.is-wide { grid-template-columns: minmax(0, 1fr); gap: 8px; }
.form-row.is-wide .row-note { padding-top: 0; }

/* ---------- Severity: one choice between four, laid out across ---------- */

.sev-picker {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
    gap: 8px;
}

.sev-option {
    display: flex;
    align-items: flex-start;
    gap: 9px;
    padding: 10px 12px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    cursor: pointer;
    transition: border-color .12s var(--ease), background .12s var(--ease);
}

.sev-option:hover { border-color: var(--navy-400); }

/* The whole card reads as chosen, not just the dot in the corner of it. */
.sev-option:has(input:checked) {
    border-color: var(--navy-600);
    background: color-mix(in srgb, var(--navy-400) 8%, var(--surface));
}

.sev-option input { margin-top: 2px; flex-shrink: 0; accent-color: var(--navy-600); cursor: pointer; }
.sev-option small { display: block; margin-top: 4px; font-size: var(--fs-xs); color: var(--steel); line-height: 1.5; }

/* A second sentence that is a consequence rather than a description — the forced password
   change, the account still holding a temporary one. Its own line, so it is not skimmed past. */
.row-note-strong {
    display: block;
    margin-top: 7px;
    color: var(--steel);
}

/* A tickbox sitting in a field grid alongside inputs. Boxed so it reads as a control on the
   same row rather than a stray line of text, and sized to match the inputs beside it. */
.check-boxed {
    padding: 8px 11px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface);
    min-height: 34px;
}
.check-boxed:has(> input:checked) {
    border-color: var(--navy-400);
    background: color-mix(in srgb, var(--navy-400) 6%, var(--surface));
}

/* ---------- A value shown once, to be copied ----------
   The generated password. Readonly rather than disabled — a disabled input does not post, and
   this value has to reach the server to become the account's password. */
.secret { display: flex; gap: 8px; align-items: stretch; max-width: 420px; }

.secret-value {
    flex: 1 1 auto;
    min-width: 0;
    font-size: var(--fs-md);
    letter-spacing: .04em;
    background: var(--surface-2);
    font-weight: 500;
}
.secret-value:focus { outline: 2px solid var(--gold-400); outline-offset: 1px; }

.secret-copy { flex: 0 0 auto; }
.secret-copy.is-done { color: var(--ok); border-color: var(--ok); }

/* ---------- Branch access ----------
   One row per branch: can they see it, and is it their base. Two controls on one line, because
   they are two halves of the same decision and splitting them across the page is what made the
   old version impossible to follow. */
.branch-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 8px;
}

.branch-row {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 11px 14px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface);
    transition: border-color .14s var(--ease), background .14s var(--ease);
}
.branch-row.is-on { border-color: var(--navy-300, var(--line)); background: var(--surface-2); }

.branch-access { flex: 1 1 auto; }
.branch-access span { font-weight: 550; }

/* The base marker reads as a quiet option until the branch is ticked, so an untickable one
   does not invite a click. */
.branch-primary {
    display: flex;
    align-items: center;
    gap: 7px;
    flex: 0 0 auto;
    font-size: var(--fs-xs);
    color: var(--steel-2);
    cursor: pointer;
}
.branch-primary input {
    width: 15px; height: 15px;
    accent-color: var(--navy-600);
    cursor: pointer;
}
.branch-primary input:disabled { cursor: not-allowed; opacity: .4; }
.branch-primary input:disabled + span { opacity: .4; }
.branch-row.is-on .branch-primary { color: var(--ink-2); }

.form-footer {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 16px;

    /* Space BELOW as well as above. With none, anything following the footer sat flush against
       the buttons — the two touched with no gap, which reads as an overlap. */
    margin-bottom: 22px;
    padding-top: 16px;
    border-top: 1px solid var(--line);
}

/* ---------- Pickable rows ----------
   Roles, competencies and branches are choices with consequences, so each one is a
   proper target with its explanation attached rather than a bare checkbox and a word. */

/* One certificate: the link, what it is, and the way to take it off again. */
.file-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-top: 8px;
}
.file-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 4px 10px;
}

/* Reads as a file, not as a sentence with a link in it. */
.file-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    max-width: 320px;
    padding: 4px 9px;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface-2);
    font-size: var(--fs-xs);
    font-weight: 550;
    color: var(--navy-600);
    text-decoration: none;
}
.file-chip:hover {
    border-color: var(--navy-400);
    color: var(--navy-400);
    text-decoration: none;
}
.file-chip > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.file-chip .bi { flex: none; }

.file-meta {
    font-size: var(--fs-xs);
    color: var(--steel-2);
}

/* ============================================================================
   The person record — five tabs
   ----------------------------------------------------------------------------
   Same shape as the job card, for the same reason: a person is a subject with
   several faces, and stacking every face down one page made a screen nobody
   read to the bottom of. What differs is the opening tab. A job card is opened
   by somebody about to act, so it opens on the work; a person is opened by
   somebody about to read, so it opens on the record and the measurements sit
   one click behind.
   ========================================================================= */

/* Supporting copy inside a panel body. .help existed only inside .field and
   .set-row, so a bare one under a table inherited nothing and read as body text
   competing with the table above it. */
.help {
    font-size: var(--fs-xs);
    color: var(--steel-2);
    line-height: 1.45;
    margin: 9px 0 0;
}
.panel-body > .help:first-child { margin-top: 0; }

/* ---- the band between the hero and the tabs ---- */
.person-band {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--sh-xs);
    margin-bottom: 14px;
    overflow: hidden;
}

/* The figures a manager opens the record for, above the tabs so the decision
   "can this person take another job" costs no clicks. */
.person-figures {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
    margin: 0;
    border-top: 1px solid var(--line);
    background: var(--surface-2);
}
.person-figures > div {
    padding: 10px 14px;
    border-right: 1px solid var(--line);
}
.person-figures > div:last-child { border-right: 0; }

.person-figures dt {
    font-size: var(--fs-2xs);
    text-transform: uppercase;
    letter-spacing: .05em;
    color: var(--steel);
    margin-bottom: 1px;
}
.person-figures dd {
    margin: 0;
    font-size: var(--fs-xl);
    font-weight: 650;
    line-height: 1.2;
    letter-spacing: -.02em;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
}
.person-figures dd small {
    font-size: var(--fs-xs);
    font-weight: 400;
    color: var(--steel);
    letter-spacing: 0;
    display: block;
    margin-top: 2px;
}
.person-figures dd small.is-bad { color: var(--bad); font-weight: 500; }
.figure-none { color: var(--steel-2); font-weight: 400; }

@media (max-width: 700px) {
    .person-figures > div { border-right: 0; border-bottom: 1px solid var(--line); }
    .person-figures > div:last-child { border-bottom: 0; }
}

/* ---- groups inside ONE panel, not a panel per group ---- */
/*
   The first version of the person record gave every fact its own navy panel —
   six of them on the Profile tab — which turned a summary into a wall of
   headers with three lines under each. A group is now a light ruled heading
   inside a shared panel, with a hairline between the columns: still clearly
   separate subjects, one card instead of six.
*/
.info-cols {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(255px, 1fr));
    gap: 22px 32px;
}

/* The rule between columns, drawn on the group rather than as a separate
   element so it disappears by itself when the grid wraps to one column. */
.info-group { position: relative; min-width: 0; }
.info-group + .info-group { border-left: 1px solid var(--line-2); padding-left: 32px; }

@media (max-width: 860px) {
    .info-group + .info-group { border-left: 0; padding-left: 0; }
}

/* A group that carries a chart rather than a list needs the room of two. */
.info-group.is-wide { grid-column: span 2; }
@media (max-width: 860px) {
    .info-group.is-wide { grid-column: auto; }
}

/* FOUR SHORT GROUPS, TWO ABREAST.

   Left to auto-fit, four groups of three lines each spread across the full width as four thin
   columns, which reads as one long row of fragments rather than four subjects. Fixed at two, they
   fall as a 2x2 block: name and contact on the top row, hours and sign-in under them. */
.info-cols.is-pairs { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* The rule belongs BETWEEN the columns, so it goes on the right-hand group of each pair. The
   generic every-group-but-the-first rule would put one down the middle of the second row's left
   column, which is the edge of the panel. */
.info-cols.is-pairs > .info-group + .info-group { border-left: 0; padding-left: 0; }
.info-cols.is-pairs > .info-group:nth-child(even) {
    border-left: 1px solid var(--line-2);
    padding-left: 32px;
}

@media (max-width: 860px) {
    .info-cols.is-pairs { grid-template-columns: 1fr; }
    .info-cols.is-pairs > .info-group:nth-child(even) { border-left: 0; padding-left: 0; }
}

.info-group > h3 {
    margin: 0 0 12px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--line);
    font-size: var(--fs-sm);
    font-weight: 650;
    color: var(--navy-600);
}
.info-group > .help:first-of-type { margin-top: 10px; }
.info-group > .muted { font-size: var(--fs-sm); margin: 0; }

/* Label beside value, not above it.

   Stacked with an uppercase label over each value, five facts took ten lines and
   the panel became a tall column of shouting captions. Read as a record — which
   is what it is — the label belongs in a narrow left column and the values line
   up down a single edge, so the eye runs down the answers rather than zig-zagging
   between caption and content. */
.info-list { margin: 0; display: grid; gap: 9px; }

/* HALF AND HALF. The label used to get a third of the width, which was enough for "Job
   title" and not for "Median time to complete" — that one wrapped onto two lines and left
   its value stranded underneath. An even split fits the longest label in the system on one
   line, and the values still line up down a single edge because every row splits the same. */
.info-list > div {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 4px 14px;
    align-items: baseline;
}
.info-list dt {
    font-size: var(--fs-sm);
    color: var(--steel);
    line-height: 1.45;
}
.info-list dd {
    margin: 0;
    font-size: var(--fs-sm);
    font-weight: 500;
    color: var(--ink);
    line-height: 1.45;
    overflow-wrap: anywhere;
}

/* Narrow enough that the two columns would each be a word wide — stack instead. */
@media (max-width: 520px) {
    .info-list > div { grid-template-columns: 1fr; gap: 0; }
    .info-list dt { font-size: var(--fs-xs); }
}
.info-list dd .is-ok   { color: var(--ok);   font-weight: 550; }
.info-list dd .is-warn { color: var(--warn); font-weight: 550; }
.info-list dd .is-bad  { color: var(--bad);  font-weight: 550; }

/* ---- the counts along the top of a tab ---- */
.stat-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 12px;
    margin-bottom: 16px;
}
.stat {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--sh-xs);
    padding: 12px 14px;
}
/* State in form as well as in number: a count that matters carries a rule down
   its left edge, so a row of tiles can be scanned rather than read. */
.stat.is-bad  { border-left: 3px solid var(--bad); }
.stat.is-warn { border-left: 3px solid var(--warn); }
.stat.is-bad  .stat-figure { color: var(--bad); }
.stat.is-warn .stat-figure { color: var(--warn); }

.stat-figure {
    display: block;
    font-size: var(--fs-2xl);
    font-weight: 650;
    line-height: 1.1;
    letter-spacing: -.025em;
    font-variant-numeric: tabular-nums;
}
.stat-label {
    display: block;
    margin-top: 3px;
    font-size: var(--fs-xs);
    color: var(--steel);
}

/* One tile, on its own. auto-fit hands a lone child the whole row, which turns a count into a
   banner running the width of the screen; this holds it to about the width it would have had
   with neighbours beside it, so the row still reads as a tile rather than as a header. */
.stat-row.is-single { grid-template-columns: minmax(150px, 240px); }

/* ---- service level ---- */
.time-story { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: 28px; align-items: start; }
.time-story .chart { margin: 0; }
@media (max-width: 1000px) { .time-story { grid-template-columns: 1fr; } }

.meter-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: 18px;
}
.meter-figure {
    display: block;
    font-size: var(--fs-2xl);
    font-weight: 650;
    line-height: 1.1;
    letter-spacing: -.025em;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
}
.meter-figure small { font-size: var(--fs-md); font-weight: 500; }
.meter-figure.is-ok   { color: var(--ok); }
.meter-figure.is-warn { color: var(--warn); }
.meter-figure.is-bad  { color: var(--bad); }

.meter-label { display: block; margin-top: 2px; font-size: var(--fs-sm); color: var(--ink-2); }
.meter-sub   { display: block; margin-top: 5px; font-size: var(--fs-xs); color: var(--steel-2); }

.meter-bar {
    display: block;
    height: 6px;
    border-radius: 3px;
    background: var(--line-2);
    overflow: hidden;
    margin-top: 7px;
}
.meter-bar > i { display: block; height: 100%; border-radius: 3px; background: var(--navy-500); }
.meter-bar.is-ok   > i { background: var(--ok); }
.meter-bar.is-warn > i { background: var(--warn); }
.meter-bar.is-bad  > i { background: var(--bad); }

/* ---- completed per month ---- */
.bars {
    display: flex;
    align-items: flex-end;
    gap: 8px;
    height: 132px;
}
.bars-col {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    align-items: center;
    height: 100%;
    gap: 4px;
}
.bars-value {
    font-size: var(--fs-xs);
    font-weight: 550;
    font-variant-numeric: tabular-nums;
    color: var(--ink-2);
}
.bars-bar {
    width: 100%;
    max-width: 46px;
    background: var(--navy-300);
    border-radius: 3px 3px 0 0;
    min-height: 2px;
}
/* The month still running is always short until it ends. Marked rather than
   left to be read as a collapse in output. */
.bars-bar.is-current { background: var(--gold-500); }

/* A month that ran LONGER than the jobs were quoted at. Marked rather than left the same navy as a
   month that came in under, which would make the trend a row of identical bars whose only
   difference was a sign printed above them. */
.bars-bar.is-over { background: var(--warn); }
.bars-label { font-size: var(--fs-2xs); color: var(--steel-2); }

/* Allocated beside worked, one month: two thin bars in the column's width. */
.bars-pair { display: flex; align-items: flex-end; gap: 3px; width: 100%; max-width: 46px; height: 100%; }
.bars-pair .bars-bar { flex: 1; max-width: none; }
.bars-bar.is-allocated { background: var(--steel-2); }

/* A month stacked by category, the colour each category carries on the breakdown beside it. */
.bars-stack { display: flex; flex-direction: column-reverse; width: 100%; max-width: 46px; min-height: 2px; border-radius: 3px 3px 0 0; overflow: hidden; }
.bars-stack > i { display: block; min-height: 2px; }
.bars-stack > i.t-navy,  .mix-track > i.t-navy  { background: var(--navy-400); }
.bars-stack > i.t-gold,  .mix-track > i.t-gold  { background: var(--gold-500); }
.bars-stack > i.t-ok,    .mix-track > i.t-ok    { background: var(--ok); }
.bars-stack > i.t-info,  .mix-track > i.t-info  { background: var(--info); }
.bars-stack > i.t-busy,  .mix-track > i.t-busy  { background: var(--busy); }
.bars-stack > i.t-warn,  .mix-track > i.t-warn  { background: var(--warn); }
.bars-stack > i.t-bad,   .mix-track > i.t-bad   { background: var(--bad); }
.bars-stack > i.t-steel, .mix-track > i.t-steel { background: var(--steel-2); }

/* ---- a breakdown row: label, track, count ---- */
.mix {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 5px 0;
    font-size: var(--fs-sm);
}
.mix-label { width: 108px; flex: none; }

/* Category names are free text and run long. Truncated rather than wrapped: a
   two-line label would push the track out of alignment with the rows above it,
   and the full name is on the element's title. */
.mix-label.is-wide {
    width: 168px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.mix-track {
    flex: 1;
    height: 9px;
    border-radius: 5px;
    background: var(--line-2);
    overflow: hidden;
}
.mix-track > i { display: block; height: 100%; border-radius: 5px; background: var(--navy-400); }
.mix-value {
    width: 32px;
    text-align: right;
    font-variant-numeric: tabular-nums;
    color: var(--ink-2);
}

/* Solid severity for a bar fill. The .sev-* pills are a pale background with
   dark text, which at 9px tall reads as an empty track.
   Qualified by .mix-track, or the plain .mix-track > i above outweighs them and
   every severity draws navy — which is the bug this replaced. */
.mix-track > i.sev-fill-critical { background: var(--sev-critical); }
.mix-track > i.sev-fill-high     { background: var(--sev-high); }
.mix-track > i.sev-fill-medium   { background: var(--sev-medium); }
.mix-track > i.sev-fill-low      { background: var(--sev-low); }

/* ---- a row that opens something on a double-click ---- */
.tbl tr.is-openable { cursor: pointer; }
.tbl tr.is-openable:hover > td { background: var(--surface-2); }

/* ---- the window Performance measures ---- */
.window-picker {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 14px;
}
.window-label { font-size: var(--fs-xs); color: var(--steel); }

/* The typed pair, pushed to the far end so the one-click presets read as a group of their own
   rather than as the first two of nine controls. */
.window-range {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-left: auto;
    font-size: var(--fs-xs);
    color: var(--steel);
}
.window-range .inp { width: auto; padding: 4px 8px; font-size: var(--fs-xs); }

@media (max-width: 900px) {
    .window-range { margin-left: 0; width: 100%; }
}

/* ---- where a list is in its pages ---- */
.record-pager {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 8px 14px;
    padding: 10px 14px;
    border-top: 1px solid var(--line);
    background: var(--surface-2);
    font-size: var(--fs-xs);
    color: var(--steel);
}
.record-pager-steps { display: flex; align-items: center; gap: 8px; }
.record-pager-page { color: var(--ink-2); font-weight: 550; }

/* Dimmed rather than removed at either end, so the control does not move under the cursor as you
   page — a Next that jumps left when Previous appears is a button you click twice by accident. */
.record-pager .is-off { opacity: .4; pointer-events: none; }
.window-option {
    font-size: var(--fs-xs);
    font-weight: 550;
    padding: 4px 11px;
    border: 1px solid var(--line);
    border-radius: 20px;
    background: var(--surface);
    color: var(--ink-2);
    text-decoration: none;
}
.window-option:hover { border-color: var(--navy-400); color: var(--navy-600); text-decoration: none; }
.window-option.active {
    background: var(--navy-600);
    border-color: var(--navy-600);
    color: #fff;
}

@media (max-width: 720px) {
    .mix-label { width: 84px; }
}

/* ---------------------------------------------------------------- drop zones ---- */
/*
   Every "Choose file" in the system gets one, added around the native control by the shared
   script rather than by each page writing its own — so a certificate, a logo and six photos of a
   door seal are all attached the same way.

   The native input STAYS. It is what Tab reaches and what a screen reader announces; this is a
   target to drop on and a line that says what is attached, not a replacement for it.
*/
.dropzone {
    display: flex;
    flex-direction: column;
    gap: 7px;
    padding: 10px 12px;
    border: 1px dashed var(--line);
    border-radius: var(--r-md);
    background: var(--surface-2);
    transition: border-color .12s ease, background .12s ease;
}

.dropzone-hint {
    display: flex;
    align-items: center;
    gap: 7px;
    margin: 0;
    font-size: var(--fs-xs);
    color: var(--steel-2);
    line-height: 1.4;
}
.dropzone-hint i { font-size: 15px; flex: 0 0 auto; }

/* Dragging over it. Deliberately loud — the whole point is knowing which of two photo boxes on
   the same screen is about to take the file. */
.dropzone.is-over {
    border-color: var(--navy-400);
    border-style: solid;
    background: color-mix(in srgb, var(--navy-400) 8%, var(--surface));
}

/* Something is attached and not yet sent. Said in the same words the job card's photo tiles use
   ("Not uploaded yet"), so a file sitting in a control is never mistaken for one that is saved. */
/* SOMETHING IS IN IT, AND THE BOX SAYS SO BY SETTLING RATHER THAN BY TALKING. The dashed edge is
   an invitation; once there is a file the invitation has been accepted, so the border goes solid
   and the hint inside is hidden by the script — see _Layout. The two rules that restyled that hint
   went with it: it no longer has any text to style. */
.dropzone.has-file { border-style: solid; border-color: var(--line); background: var(--surface); }

.mail-error {
    display: block;
    font-size: var(--fs-xs);
    color: var(--steel-2);
    line-height: 1.45;

    /* An SMTP error can arrive as one unbroken token — a host name, a URL to a Microsoft support
       article. Without this it stretches the table sideways instead of wrapping. */
    overflow-wrap: anywhere;
}

/* ---------------------------------------------------------------- planned work ---
   The next-due date on a maintenance schedule.

   It carries a tone only when the answer is not "in good time". A schedule due in four months and
   one due tomorrow are both perfectly normal, and colouring every row would leave the one that has
   actually slipped competing with fifteen that have not — the same reason most job card statuses
   are deliberately neutral.

   is-soon: the raise date falls inside the notice window, so the card is about to appear.
   is-late: the raise date has gone and no card has been raised, which is the one that needs doing
            something about.
*/
.plan-due { font-weight: 500; }
.plan-due.is-soon { color: var(--warn); font-weight: 600; }
.plan-due.is-late { color: var(--bad); font-weight: 600; }

/* ------------------------------------------------------------- morning review ---
   The two actions on a person's line — logging overtime and marking them not in.

   A <details> RATHER THAN A DIALOG, AND THE REASON IS NOT ONLY THAT IT NEEDS NO SCRIPT. Both
   actions are about the line they sit under: "Ndlovu · Tuesday — 3.0h on cards, 3.5h off cards,
   3.5h beyond shift" is the whole of the context for the hours somebody is about to type, and a
   modal would cover it up at the moment it is needed. Opening in place keeps the three figures on
   screen beside the box.

   THE SUMMARY IS STYLED AS A GHOST BUTTON because that is what it is — the owner drew the line
   with "[ Log overtime ]" on the end of it. The marker is removed for the same reason: a
   disclosure triangle beside a button reads as two controls.

   Closed, always, on every draw. The form is an offer and not a prompt; a page that opened with
   half a dozen empty hour boxes showing would be asking six questions nobody asked for. */
.review-do { display: inline-block; margin: 6px 8px 0 0; }

.review-do > summary {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    cursor: pointer;
    list-style: none;
    user-select: none;
}

/* Safari and older WebKit draw their own triangle through a pseudo-element rather than through
   list-style, so both have to be said. */
.review-do > summary::-webkit-details-marker { display: none; }
.review-do > summary::marker { content: ""; }

/* Once open the form is a block on its own, not something squeezed beside the sentence that
   opened it — the fields have labels and the labels need the width. */
.review-do[open] { display: block; margin-right: 0; }

.review-do[open] > summary { margin-bottom: 2px; }

/* The hours box is the one figure being typed and is held narrow so it reads as a quantity rather
   than as a free-text field. The date boxes are sized to the date they hold. */
.review-do .field { flex: 0 0 auto; }
.review-do input[type="number"] { width: 7.5rem; }
.review-do input[type="date"] { width: 10.5rem; }

/* --- The actions at the end of a review line ---------------------------------
   THEIR OWN COLUMN, HARD AGAINST THE RIGHT EDGE. These buttons used to follow the sentence, so
   they began wherever that person's name happened to end — one line's pair starting after
   "Ndlovu" and the next line's after "van der Merwe". A control that moves with the length of a
   surname is one the reader has to hunt for on every row. The owner: "put buttons right aligned at
   the end please, so they all look even and symmetrical, not screw based on text length, like
   their own column."

   Every line carries the same two buttons in the same order, so an aligned right edge aligns the
   left edge too, and the whole block reads as a column. A line with only one of them still ends on
   that same edge, which is the point of aligning to the end rather than to a fixed width. */
.ev-line-do {
    flex: 0 0 auto;
    text-align: right;
    white-space: nowrap;
}

/* The margin that spaced them from the sentence is now the gap between the two of them, and there
   is none above: they sit on the line's first row rather than under it. */
.ev-line-do .review-do { margin: 0 0 0 8px; }

/* ONCE ONE IS OPEN IT IS A FORM AND NEEDS THE WIDTH OF THE LINE. The line wraps and the column
   takes the whole of the second row, left aligned like every other form in this system — a set of
   labelled fields ranged right would read as a footnote rather than as something to fill in.
   Closed, which is how they always start, nothing here applies. */
.ev-line:has(.ev-line-do .review-do[open]) { flex-wrap: wrap; }

.ev-line:has(.ev-line-do .review-do[open]) .ev-line-do {
    flex: 1 1 100%;
    margin-top: 8px;
    text-align: left;
    white-space: normal;
}

.ev-line:has(.ev-line-do .review-do[open]) .ev-line-do .review-do { margin-left: 0; }

/* ---------------------------------------------------------------- the card in your hands ----
   Two words on the calendar, where a whole panel used to be. The dot is filled rather than
   hollow so the row reads as "started" at a glance, in the same vocabulary the done tick uses. */
.day-card.is-open-now > .bi { color: var(--accent, #b4690e); }

.day-card-now {
    display: inline-block;
    margin-top: 3px;
    padding: 1px 6px;
    border-radius: 3px;
    background: var(--chip-warn-bg, #fdf1df);
    color: var(--chip-warn-fg, #8a5000);
    font-size: 10px;
    font-weight: 650;
    letter-spacing: .04em;
    text-transform: uppercase;
}

/* ---------------------------------------------------------------- five at a time -------------
   A list of everything with no hours on it is five rows on a quiet branch and fifty on a busy
   one, and fifty printed one under another buries whatever sits below it.

   EVERY ROW STAYS IN THE MARKUP and only the window moves, so Ctrl+F still finds row forty and a
   screen reader still reads the lot. Without the script — and there is none on a printed page —
   nothing is hidden at all, which is the right failure. */
.ev-list.is-paged.is-windowed > .ev-line { display: none; }
.ev-list.is-paged.is-windowed > .ev-line.is-shown { display: flex; }

.pager-line {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 6px;
    font-size: 12px;
    color: var(--muted, #6b7280);
}

.pager-line button {
    border: 1px solid var(--line, #d7dde3);
    background: #fff;
    border-radius: 4px;
    width: 24px;
    height: 24px;
    line-height: 1;
    cursor: pointer;
    color: inherit;
}

.pager-line button:disabled { opacity: .4; cursor: default; }

/* ---------------------------------------------------------------- the life of a card ---------
   One card per hop, read left to right. It was a vertical timeline, which is the same facts
   arranged so that comparing two of them means scrolling — and the only question anybody asks of
   this panel is which step took the longest.

   IT SCROLLS SIDEWAYS RATHER THAN WRAPPING. A wrapped row puts hop six under hop one and the
   order stops being obvious, which is the one thing the row exists to show. */
.hop-row {
    /* A GRID, NOT A FLEX ROW, so every hop is an equal column rather than as wide as its own
       words. It scrolls sideways once there are more hops than fit - a job card that went to
       rework twice has a long life - rather than wrapping into a second ragged row. */
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(140px, 1fr);
    overflow-x: auto;
    gap: 2px;
    padding-bottom: 4px;
}

.hop {
    /* EVERY HOP THE SAME WIDTH. They used to size to their own text between a floor and a ceiling,
       so a row of them came out ragged - "its all its own lenght, width, it just looks terrible."
       The row is a grid now (see .hop-row) and every column is an equal share of it, which also
       makes them the same HEIGHT, because a grid stretches its items. */
    padding: 10px 14px 10px 12px;
    border-left: 3px solid var(--line, #d7dde3);
    background: #fff;
}


.hop-what {
    display: block;
    font-size: 11px;
    line-height: 1.3;
    color: var(--muted, #6b7280);
    margin-bottom: 3px;
}

.hop-time {
    display: block;
    font-size: 17px;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    color: var(--ink, #16202b);
}

.hop-note {
    display: block;
    font-size: 10px;
    color: var(--muted, #6b7280);
    margin-top: 2px;
}

/* The stage it is sitting in now. Amber down the edge, the same vocabulary every other
   "look at this one" carries, and the figure counts up rather than standing still. */
.hop.is-running { border-left-color: var(--warn, #b4690e); }
.hop.is-running .hop-time { color: var(--warn, #b4690e); }
.hop.is-running .hop-note { color: var(--warn, #b4690e); font-weight: 600; }

/* ---------------------------------------------------------------- hold to see the password ---
   The button sits inside the field rather than beside it, so the row keeps its width and every
   form it appears on lines up exactly as it did before. */
.pw-wrap { position: relative; display: block; }

.pw-wrap > input { width: 100%; padding-right: 38px; }

.pw-eye {
    position: absolute;
    top: 50%;
    right: 4px;
    transform: translateY(-50%);
    display: grid;
    place-items: center;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 0;
    border-radius: 4px;
    background: transparent;
    color: var(--steel, #6b7280);
    cursor: pointer;
    line-height: 1;
}

.pw-eye:hover { color: var(--ink, #16202b); }
.pw-eye:active { background: var(--line, #e5e9ed); }

/* ---------------------------------------------------------------- groups inside one panel ----
   Creating somebody is ONE job. It read as three because each group of fields had its own
   bordered card, which is the treatment this system uses for separate things — so a person
   filling the form in believed they were being asked for three unrelated lots of information.

   A subheading separates a group perfectly well. The rule under it does the work the card
   border was doing, without claiming the group stands on its own. */
h3.form-section {
    margin: 22px 0 12px;
    padding-top: 16px;
    border-top: 1px solid var(--line, #e5e9ed);
    font-size: 13px;
    font-weight: 650;
    letter-spacing: .01em;
    color: var(--ink, #16202b);
}

h3.form-section:first-of-type { margin-top: 4px; padding-top: 0; border-top: 0; }

/* ---------------------------------------------------------------- the person form -------------
   TWO COLUMNS AND A NARROWER MEASURE. It was four across the full width of a desktop panel, which
   gave a first name a box wide enough for a sentence and made a form of eight fields read as a
   wall. The owner: "the add is way too big at the top, the fields look terrible... maybe 2
   columns, not 4."

   FIFTY-FIFTY, NOT CAPPED. Capping it left the fields huddled in the left half of a wide panel
   while the branch list underneath ran the full width, so the two halves of one panel disagreed
   about where the page ended. The owner: "make the columns 50/50 for the top person details." */
.panel-body > .form-grid.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* Consistent air between rows. Two grids sitting one under the other were separated by a
   hand-placed margin on the second, which is the sort of thing that goes wrong the moment
   somebody reorders them. */
.panel-body > .form-grid + .form-grid { margin-top: 18px; }

/* A field's own parts, so a label, its box and its help line are not equally spaced —
   the label belongs to the box under it, the help belongs to the box above it. */
.form-grid .field > label { margin-bottom: 5px; }
.form-grid .field > .help { margin-top: 5px; line-height: 1.45; display: block; }

/* ---------------------------------------------------------------- the add form's header ------
   SMALLER. It is a title and one line of explanation, and it was taking the vertical space of a
   dashboard tile before a single field appeared. The owner: "make this card at the top way
   smaller please! its to big."

   Only on the ADD form. A person's own record uses the same hero to carry their name, initials
   and when they last signed in, and that one has earned its size. */
.form-hero.is-compact {
    gap: 12px;
    padding: 10px 14px;
    margin-bottom: 12px;
}

.form-hero.is-compact .avatar.lg { width: 30px; height: 30px; font-size: var(--fs-sm); }
.form-hero.is-compact .form-hero-text h1 { font-size: var(--fs-md); }
.form-hero.is-compact .form-hero-text p { font-size: var(--fs-xs); margin-top: 1px; }

/* ---------------------------------------------------------------- saving photographs ---------
   THE ACTION, WHERE THE THING IT ACTS ON IS. The form's Save sits at its foot, past the rest of
   the gallery and the whole completion checklist, so choosing a file at the top produced a preview
   and no visible way to keep it. The owner: "i can upload but it never saves it, no button?"
   Revealed by the preview script the moment files are chosen. */
.photo-save {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    margin-top: 12px;
}

.photo-save .help { margin: 0; }

/* The tile's own foot, now that it carries a timestamp and — where one is allowed — a Remove.
   It used to carry a locked-state sentence and the uploader's name as well, on every photograph
   on the card. */
.photo-thumb-foot { align-items: center; }

/* ---------------------------------------------------------------- a day's heading on a list ---
   THE DAY, THEN THE JOBS ON IT. My Work is one table per day now rather than one flat list, so
   somebody standing on the floor reads "Today" and the work under it rather than working out which
   rows are which. Sits flush with the tables it heads. */
.day-heading {
    display: flex;
    align-items: baseline;
    gap: 10px;

    /* LINED UP WITH THE TABLE UNDERNEATH IT, which means the table's own cell padding rather than
       a number that looks about right. It had a margin of its own and sat further left than the
       column headings it introduced. */
    margin: 20px 0 0;
    padding: 0 12px 8px;
}

.day-heading:first-of-type { margin-top: 0; }

/* The tables stack straight under their headings; the rule on the heading is the divider. */
.day-heading + .tbl-wrap { margin-bottom: 4px; }

/* ---------------------------------------------------------------- a heading's hint icon -------
   AIR BETWEEN THE WORDS AND THE MARK. The info icon sat hard against the last letter of the
   heading, reading as punctuation rather than as something to hover. */
.heading-hint { margin-left: 8px; }

/* ---------------------------------------------------------------- My Work's rows --------------
   ONE CLICK, LIKE EVERY OTHER LIST. They opened on a double click for a while, to stop a stray tap
   on a phone losing somebody the list; the owner did not want the exception, and he is right that
   a screen behaving unlike its neighbours costs more than the tap it saves. */
/* The description and whatever follows it on the same line are separate things. They were run
   together with no space at all. */
table.tbl .cell-title + .chip { margin-left: 8px; }

/* ---------------------------------------------------------------- a flash with a list in it ---
   Several things reported at once — five photographs refused for five reasons — read as a
   paragraph somebody had to parse to find out which file was which. One per line. */
.flash-list {
    margin: 8px 0 0;
    padding-left: 20px;
}

.flash-list li + li { margin-top: 4px; }

/* ---------------------------------------------------------------- a photograph that will not go --
   RED BEFORE IT IS SENT. The refusals a reader hits most — already on this card, too small to have
   come off a camera — are answerable without the file leaving the phone, so the tile says so while
   they can still take it out. The others are still found on save; a tile with no mark here is not a
   promise, it is the absence of a known problem. */
.photo-thumb.is-refused {
    border-color: var(--bad, #b42318);
    background: var(--bad-bg, #fef3f2);
}

.photo-thumb.is-refused img { opacity: .55; }

.photo-thumb-refused {
    flex: 1 1 100%;
    color: var(--bad, #b42318);
    font-weight: 600;
}

.photo-thumb-refused:empty { display: none; }

/* ---------------------------------------------------------------- reading a filed document ----
   IT USED TO OPEN IN A NEW TAB: a bare file on a blank background, with the way back being the
   browser's own history and the job card lost behind it. */
.doc-viewer-body {
    padding: 0;
    background: var(--surface-2);
}

.doc-viewer-frame {
    display: block;
    width: 100%;
    height: 70vh;
    border: 0;
    background: var(--surface-2);
}

/* A photograph is sized to fit rather than stretched to the frame's height. */
img.doc-viewer-frame { height: auto; max-height: 70vh; object-fit: contain; }

/* The name is a button now rather than a link, so it needs the link's own shape back. */
button.doc-link {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

/* The form that every Remove button posts to. It carries one hidden field and must take no room. */
.is-hidden-form { display: none; }

/* ============================================================================================
   TOUCH: EVERY CONTROL BIG ENOUGH TO HIT WITH A THUMB
   ============================================================================================

   THIS IS THE ONE THING THAT WAS WRONG ON EVERY SCREEN. Measured across all twenty-three screens
   at 375px and at 768px, nothing overflowed and nothing was clipped — the layout was already
   responsive. What failed was the size of the things a person has to press:

       the sidebar toggle        25px      the only way to reach the menu on a phone
       Report a fault            30px
       Raise job card            30px
       Back to Dashboard         24px
       Clear (filter)            24px
       every select              30px
       Submit                    34px

   Apple asks for 44pt and Android for 48dp. Twenty-four pixels is a control you miss, and the
   person missing it is wearing a cold-store glove.

   ON POINTER RATHER THAN ON WIDTH, and that distinction matters here. A tablet held in landscape
   is 1024px wide and still operated with a thumb; a narrow window on a desk is 700px wide and
   still has a mouse. Width answers "how much room is there", which is a different question from
   "what is pressing this". Both are handled: coarse pointers at any width, and any screen small
   enough that it is probably a handset.

   THE DESKTOP IS UNTOUCHED. Nothing above applies to a mouse on a wide screen, so the dense
   tables and small buttons a manager works with all day stay exactly as they are.
   ============================================================================================ */

@media (pointer: coarse), (max-width: 820px) {

    /* 44px is the floor, not the height — a button with a lot of words still grows. */
    .btn,
    .inp, .sel, .txt,
    input[type="text"], input[type="number"], input[type="date"], input[type="search"],
    input[type="email"], input[type="password"], select, textarea {
        min-height: 44px;
    }

    /* The padding has to come with it, or the text sits in the middle of a tall thin box and the
       control reads as broken rather than as roomy. */
    .btn                  { padding-top: 8px; padding-bottom: 8px; padding-left: 16px; padding-right: 16px; }
    .inp, .sel, .txt,
    input, select, textarea { padding-top: 9px; padding-bottom: 9px; }

    /* THE SMALL BUTTON IS STILL SMALL, but only in width. It is used where several sit in a row —
       a table's action column — and making those 44px tall as well is right; making them 16px
       wider each would push the row off the screen. */
    .btn.btn-sm { padding-left: 10px; padding-right: 10px; }

    /* THE SIDEBAR TOGGLE IS THE MOST IMPORTANT CONTROL ON A PHONE and was the smallest thing on
       the screen. Every other way into the menu is hidden at this width. */
    .topbar-toggle {
        min-width: 44px;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        font-size: 1.25rem;
    }

    /* A checkbox or radio is pressed by its LABEL as often as by the box, so the label carries the
       target. The box itself grows too — a 13px tickbox is a miss with a glove on. */
    .check { min-height: 44px; display: inline-flex; align-items: center; gap: 10px; }
    .check input[type="checkbox"], .check input[type="radio"] { width: 20px; height: 20px; }

    /* A .check THAT IS A ROW, OR SITS INSIDE ONE, IS NOT AN INLINE CONTROL.
       ===================================================================
       inline-flex above is right for a tick box and its words sitting in a line of other things.
       It is wrong for the two places .check is used as a BLOCK:

       A CHECKLIST ROW is a .check, so on a handset the whole row became an inline box that
       shrink-wrapped to its text instead of filling the panel — which is why the rows in the
       completion checklist ended at ragged widths.

       AND "NOT APPLICABLE" IS A .check NESTED INSIDE THAT ROW'S LABEL. Inline, it flowed on
       after the item's words and started wherever they happened to stop: 216px in on one row,
       82 on the next, 201 on the one after. The owner: "the completion checklist, doesnt auto
       resize nicely".

       Both are blocks. The tick box and its words still sit in a row INSIDE them — that is what
       flex does; it is only the outer box that stops being inline. */
    .check.checklist-row,
    .na-check { display: flex; }

    /* Back to flex-start with it. Centring a 20px box against a label that has wrapped to three
       lines floats it in the middle of them, away from the words it belongs to. */
    .check.checklist-row { align-items: flex-start; }

    /* Its own line under the item, aligned with the words rather than with the item's tick box. */
    .na-check { margin-top: 4px; }

    /* A row of buttons wraps rather than squeezing. Three 44px buttons do not fit across a phone,
       and shrinking them to fit would undo everything above. */
    .modal-foot, .filter-actions, .panel-head .actions { flex-wrap: wrap; }

    /* AND A LINK IN A TABLE CELL IS A TAP TARGET TOO. The job number is how a person opens a card
       from a list, and it was a 17px line of text. */
    table.tbl a.ref { display: inline-flex; align-items: center; min-height: 32px; }

    /* THE THREE CONTROLS THAT ARE NOT .btn AND ARE PRESSED JUST AS OFTEN.

       A tab is how somebody moves between Internal and Contractors, or between the six faces of a
       job card; a .tool is Add, View, Edit above a table. Neither carries the button class, so
       neither was covered — they came out at 36px and 28px. */
    .tab  { min-height: 44px; }
    .tool { min-height: 44px; padding-left: 12px; padding-right: 12px; }

    /* AND THE FILTER BAR, which sets its own height to line the button up with the boxes beside
       it — see the note where that 31px is declared. The boxes are 44px on a touch screen now, so
       matching them means 44 here as well: the rule still says "be the height of an .inp", it is
       just that an .inp is a different height under a thumb. Specificity has to match theirs to
       win, which is why this is not folded in with the .btn rule above. */
    .filter-bar .filter-actions .btn { min-height: 44px; padding: 8px 16px; }

    .assign-day .btn        { min-height: 44px; }
    .assign-day-field .inp  { min-height: 44px; padding: 9px 10px; }
}

/* --- The columns that were too narrow to read ------------------------------------------------
   Measured, not guessed: "What it needs" came out at 61px on a phone, which is not enough for
   "Start it" without breaking it across two lines, and it is the column that tells somebody what
   to do. The job number gives up the room — it is a fixed twelve characters and had 118px. */
@media (max-width: 700px) {
    /* FIXED, SO THE TABLE CANNOT EXCEED ITS CONTAINER WHATEVER IS IN IT. With table-layout: auto
       the columns are as wide as their content wants, and one long word — an asset code, a
       chamber name — quietly drags the whole table past the edge of the screen. On a desk that
       shows up as a scrollbar; on a phone it is a column nobody knows is there.

       The percentages are then a promise rather than a suggestion, and the text wraps to fit
       them. */
    table.tbl-cards          { table-layout: fixed; width: 100%; }
    table.tbl-cards .c-job   { width: 33%; }
    table.tbl-cards .c-issue { width: 42%; }
    table.tbl-cards .c-needs { width: 25%; }

    /* The go-chevron is the one thing that must not be given a share: it is an icon, and a
       percentage of a 375px screen would be four times the room it needs. */
    /* GONE, NOT NARROWED — see the note on tbl-mywork below. The row opens the card. */
    table.tbl-cards .c-go    { display: none; }
}

/* --- My Work's queue, on a phone -------------------------------------------------------------
   Five columns in 342px made the table 554px wide inside a 342px box: the severity and the hours
   were off the right-hand edge, reachable only by swiping a table most people do not know can be
   swiped. The same fold the Today list uses — the columns that cannot earn their width become a
   line under the description.

   THE STATUS KEEPS ITS COLUMN. It is the one thing on the row addressed to the person holding the
   card — "Rework" and "Yours to do" are instructions, not attributes — and it lines up down the
   list where it can be read at a glance. */
@media (max-width: 700px) {
    table.tbl-mywork .c-sev,
    table.tbl-mywork .c-hrs   { display: none; }
    table.tbl-mywork .fold-sev { display: block; margin-top: 4px; }

    table.tbl-mywork          { table-layout: fixed; width: 100%; }
    /* THE JOB NUMBER SETS THE FLOOR. "JC-2026-0017" is twelve fixed characters in a mono face
       inside a bordered chip — about 110px all in — and a column narrower than that does not wrap
       it, it overflows it. The description gives up the difference because it is the one cell that
       can take another line without hurting. */
    table.tbl-mywork .c-num    { width: 34%; }
    table.tbl-mywork .c-desc   { width: 40%; }
    table.tbl-mywork .c-status { width: 26%; }

    /* GONE, NOT NARROWED. A column set to nought is still a column: the browser keeps its padding
       and its border, and three of those were the last 23px holding this table wider than the
       space it had. The whole row opens the card anyway — the chevron was only ever a hint. */
    table.tbl-mywork .c-go     { display: none; }

    /* Prose wraps on words. The default break-word is right for an asset code in a narrow column
       and wrong for a sentence — it is what turned "Chiller" into "Chill / er". */
    table.tbl-mywork .c-desc { overflow-wrap: normal; word-break: normal; hyphens: none; }

    /* AND THE CHIPS WRAP. "Will miss its target" is four words that refuse to break everywhere
       else — rightly, because a warning split across two lines in a wide table reads as two
       warnings. In a 92px column it is the last thing holding the row open: with the columns fixed
       the chip cannot widen the table any more, so instead it overflowed its own cell and left
       36px of the row unreachable. The same trade tbl-jobs makes with the due badge at 620px. */
    table.tbl-mywork .c-status .chip,
    table.tbl-mywork .c-status .sev { white-space: normal; }
}

/* --- The controls that are icons rather than buttons ----------------------------------------
   A cross on a file card, an eye on a password box, a link-styled button in a table: each is a
   real control and none of them carries the button class, so none was caught by the rule above.
   They came out at 20px, 26px and 30px.

   THE PADDING GROWS, NOT THE ICON. A 44px cross drawn at 44px would shout; what is wanted is a
   small mark with a large area around it, which is how a phone keyboard's keys work. */
@media (pointer: coarse), (max-width: 820px) {
    .link-btn {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        padding: 0 4px;
    }

    .pw-eye,
    .file-card-x {
        min-width: 44px;
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    /* The cross sits in the corner of a file card, so it has to come back inside as it grows or
       it hangs off the edge of the card it belongs to. */
    .file-card-x { top: 2px; right: 2px; }
}

/* --- Wide tables on a tablet ------------------------------------------------------------------
   700px was the floor for the job card table, and a tablet in portrait has 696px of content
   width — four pixels short, which put a scrollbar under every one of them on the Allocate and
   Planned Work screens. The floor exists so the columns do not collapse into unreadable slivers;
   660 keeps that and clears the tablet. */
@media (min-width: 701px) and (max-width: 820px) {
    table.tbl-jobs,
    table.tbl-sched,
    table.tbl-menu { min-width: 660px; }
}

/* --- Tables on a phone: the room is in the gutters ---------------------------------------------
   12px each side of every cell is right on a wide screen and is 24px per column on a narrow one.
   Across the six columns of a settings table that is 144px — half the width of the screen — spent
   on white space, and it was the difference between a table that fits and one that has to be
   swiped.

   SEVEN PIXELS, NOT NONE. Cells still need to be told apart; what they do not need on a handset is
   desk-width gutters. */
@media (max-width: 700px) {
    table.tbl thead th,
    table.tbl tbody td,
    table.tbl tfoot td { padding-left: 7px; padding-right: 7px; }
}

/* --- The two tables that stay wide, deliberately -----------------------------------------------
   The Database Log and the email Outbox are diagnostic tables: a timestamp, a source, a message,
   an outcome. An administrator opens them to read exactly the columns that would have to be hidden
   to make them fit, so hiding them would leave a screen that fits and answers nothing.

   THEY SCROLL SIDEWAYS INSIDE THEIR PANEL, which is what .tbl-wrap is for, and the page itself
   never moves. That is the accepted answer for a genuinely wide table and it is a deliberate
   choice here rather than an oversight — both screens are the administrator's, and the
   administrator is at a desk.

   Scheduled Jobs is the same case for the same reason: a cron expression and a last-run state do
   not fold into anything smaller and still mean what they said. */

/* ============================================================================================
   A TABLE THAT HAS RUN OUT OF ROOM: THE ROW BECOMES A BLOCK
   ============================================================================================

   Applied by the script in _Layout, which measures whether a table actually fits the space it has
   rather than guessing from the width of the screen. See the note there for why.

   THE HEADING COMES WITH THE VALUE. A stacked cell on its own is a number with no name — "3" over
   "High" over "Friday" — so each carries its own column heading, copied onto it by the same
   script. That is the whole difference between a stacked table and a broken one.

   NOTHING IS HIDDEN AND NOTHING IS SWIPED. Every column that was in the table is still on the
   screen; it is read down instead of across. This is what lets the Database Log, the outbox and
   the settings tables keep every column they have and still fit a phone.
   ============================================================================================ */

table.tbl.is-stacked,
table.tbl.is-stacked tbody,
table.tbl.is-stacked tr,
table.tbl.is-stacked td { display: block; width: auto; }

/* AND THE FLOOR COMES OFF WITH IT. Several tables carry a min-width so their columns do not
   collapse into slivers on a narrow desk — the log asks for 980px, the job list for 700. Once the
   row is a block there are no columns to protect, and a floor left standing is a table that
   stacked correctly and still made the panel scroll sideways. */
table.tbl.is-stacked { min-width: 0 !important; table-layout: auto; }

/* THE HEADER ROW GOES, because every value now carries its own. Taken out of the layout rather
   than hidden from assistive technology: a screen reader follows the data-label on each cell. */
table.tbl.is-stacked thead { display: none; }

/* ONE CARD PER RECORD, with the same line between records the table had between rows. */
table.tbl.is-stacked tbody tr {
    padding: 10px 2px;
    border-bottom: 1px solid var(--line);
}

table.tbl.is-stacked tbody tr:last-child { border-bottom: none; }

/* A STACKED ROW IS A BLOCK, AND height MEANS SOMETHING ELSE TO A BLOCK.
   ====================================================================
   Two tables give their rows a height — tbl-sched 76px, tbl-jobs 56px — so that a row holding one
   chip is not visibly shorter than the one below it holding a paragraph. On a REAL table row that
   is a floor: a table row grows past its height whenever its content needs more, which is what
   makes the rule safe there.

   Stacked, the row becomes display: block, and to a block `height` is not a floor — it is the
   height. The scheduled jobs list put 338px of cells into a 76px box on a handset, so every row
   was drawn over the one after it: the owner's screenshot has "Notifications - Send queued mail"
   printed through "Changed by System Administrator".

   IT IS FIXED HERE RATHER THAN ON EITHER TABLE, because it is not a fault in either of them — the
   rule they wrote is correct for the layout they wrote it for. What is wrong is carrying a table's
   height into a layout that is no longer a table. */
table.tbl.is-stacked tbody tr,
table.tbl.is-stacked tbody td { height: auto; }

/* A GROUP HEADING STILL SPANS EVERYTHING — the day on My Work, the section in the user register.
   It never got a label because it is not a record; see the script. */
table.tbl.is-stacked tbody tr.row-group { padding: 12px 2px 4px; border-bottom: none; }

table.tbl.is-stacked td {
    display: flex;
    gap: 10px;
    align-items: baseline;
    border: none;
    padding: 3px 2px;
    text-align: left;
}

/* THE LABEL IS A THIRD OF THE ROW AND NO MORE. Wider and the values line up so far right they
   stop reading as a pair; narrower and "Expected completion" wraps four times. */
table.tbl.is-stacked td::before {
    content: attr(data-label);
    flex: 0 0 34%;
    max-width: 130px;
    color: var(--muted);
    font-size: var(--fs-xs);
    font-weight: 600;
    text-transform: none;
}

/* A cell with no heading — the chevron column, an actions cell — takes the whole width rather
   than sitting in the value column with an empty label beside it. */
table.tbl.is-stacked td:not([data-label])::before { content: none; }
table.tbl.is-stacked td:not([data-label]) { display: block; }

/* A CELL CARRYING MORE THAN ONE THING READS DOWN, NOT ACROSS.
   ==========================================================
   The rule above makes every stacked cell a flex ROW: the label, then the value. That is right
   for a cell holding one thing, which is most of them.

   IT IS WRONG THE MOMENT A CELL HOLDS TWO. The outbox's message cell carries a subject, the kind
   of message, and — where nothing went out — the reason. Laid out as a row those became three
   columns sharing the value's width, so the middle one was about thirty pixels and read

       Assi
       gne
       d

   The owner: "messages under notifications also look a bit funny, these must auto resize too."

   :has(> * + *) IS THE WHOLE POINT OF SCOPING IT. Only a cell with two or more element children
   changes; a cell holding one value, or bare text with no element around it at all, is left
   exactly as it was. Grid rather than a column flex so the label still sits beside the FIRST line
   rather than above the block, which is what the stacked layout has always looked like. */
table.tbl.is-stacked td[data-label]:has(> * + *) {
    display: grid;
    grid-template-columns: minmax(0, 34%) minmax(0, 1fr);
    align-items: baseline;
}

table.tbl.is-stacked td[data-label]:has(> * + *)::before { grid-column: 1; grid-row: 1; }
table.tbl.is-stacked td[data-label]:has(> * + *) > *    { grid-column: 2; min-width: 0; }

/* AND AN EMPTY CELL IS DROPPED ENTIRELY. Down a column, a blank is read as "nothing for this
   one"; stacked, it is a heading with nothing after it, which reads as a fault. */
table.tbl.is-stacked td:empty { display: none; }

/* The first cell of a record is the one that names it — the job number, the person, the setting —
   so it leads rather than sitting in the middle of the block. */
table.tbl.is-stacked tbody tr > td:first-child {
    display: block;
    font-weight: 600;
    padding-bottom: 6px;
}

table.tbl.is-stacked tbody tr > td:first-child::before { content: none; }

/* Numbers stop being right-aligned once there is no column to align within. */
table.tbl.is-stacked td.num { text-align: left; }

/* A stacked row is still one tap target that opens the record. */
table.tbl.is-stacked tbody tr.row-click,
table.tbl.is-stacked tbody tr.is-openable { cursor: pointer; }



/* ---------------------------------------------------------------------------
   The hundred percent bar — Reports, Block 1.

   ONE BAR, FOUR MEANINGS. Worked, booked away, had a card and was not on it,
   and nobody was asked. The last two are the sharpest distinction on the
   screen and must be told apart at a glance, so they are not two shades of
   the same colour.

   Widths come from the model as a percentage of the whole. The segments
   therefore always fill the bar exactly, because the arithmetic behind them
   sums to the rostered hours by construction — see WhereTheTimeWent.
   --------------------------------------------------------------------------- */
/* --- The report's answer: one paragraph, the largest type on the page ------ */
.report-answer {
    font-size: var(--fs-lg, 1.15rem);
    line-height: 1.6;
    margin: 0;
}
.report-answer a { text-decoration: underline; text-decoration-color: var(--line-2); text-underline-offset: 3px; }
.report-answer a:hover { text-decoration-color: var(--navy-500); }
.report-answer .last { display: block; margin-top: 6px; font-size: var(--fs-sm, .9rem); color: var(--ink-2); }

.split-bar {
    display: flex;
    width: 100%;
    height: 22px;
    border-radius: 4px;
    overflow: hidden;
    background: var(--line-2);
    border: 1px solid var(--line);
    margin: 0 0 14px;
}

.split-bar .seg { display: block; height: 100%; min-width: 2px; }

.split-bar .seg.is-worked      { background: var(--ok); }
.split-bar .seg.is-booked      { background: var(--steel-2); }
.split-bar .seg.is-idle        { background: var(--warn); }
.split-bar .seg.is-unallocated { background: var(--busy); }

/* The same four, as a key beside the row they explain. */
.swatch {
    display: inline-block;
    width: 10px;
    height: 10px;
    border-radius: 2px;
    margin-right: 7px;
    vertical-align: baseline;
}

.swatch.is-worked      { background: var(--ok); }
.swatch.is-booked      { background: var(--steel-2); }
.swatch.is-idle        { background: var(--warn); }
.swatch.is-unallocated { background: var(--busy); }

/* --- A summary table with a lot of columns --------------------------------- */
/* HEADERS WRAP HERE, and they have to. Every .tbl header is nowrap — right for
   "Status" and "Raised", wrong the moment a column is called "Awaiting
   verification" and the table is table-layout: fixed. The header could not
   break, so it ran out of its cell and was clipped. The owner: "text wrap the
   headers! they're overflowing and cut off!" */
table.tbl-summary thead th {
    white-space: normal;
    overflow-wrap: anywhere;
}

/* AND THE STAGE BREAKDOWN NEEDS ROOM. It carries the branch, the count, six
   lifecycle columns and Overdue — nine, where this table was written for five.
   The percentages below over-subscribe badly at that count, so a floor is set
   and .tbl-wrap scrolls it sideways rather than crushing every column to a
   width no heading fits in. */
table.tbl-summary.is-wide { min-width: 900px; }

table.tbl-summary.is-wide col.c-name { width: 20%; }
table.tbl-summary.is-wide col.c-num  { width: 9%; }
table.tbl-summary.is-wide col.c-go   { width: 44px; }


/* A SUB-LINE IS A LINE. .tbl .cell-sub sets the size and the colour and leaves
   the display alone, because everywhere else in this system it is a <div>. Used
   as a <span> it ran on after the thing above it — "Phanuel Sibanda Day shift"
   on one line — so these two screens say it explicitly. */
.tbl .cell-sub { display: block; }

/* One card a person could be given: the number, then what it is, then how big.
   Three facts that were running together as one wrapped sentence. */
.offer { margin-bottom: 6px; }
.offer:last-child { margin-bottom: 0; }
.offer .ref { margin-right: 6px; }

/* --- A data table on Insights and Reports ----------------------------------
   NOTHING IS HIDDEN, AND IT SCROLLS INSTEAD.

   tbl-cards FOLDS on a phone: c-who, c-when, c-sev and the rest go display:none
   below 821px and come back as sub-lines inside the description cell. That works
   because those tables emit the fold lines. These do not — so on a phone the
   person's NAME vanished from "Free and able to work", and the waiting time, the
   due date and who it waits on vanished from "Work that is waiting". The columns
   were there; the reader could not see them.

   A table somebody has to push sideways is worse than one that folds. A table
   with the answer missing is worse than either.

   FOUR WIDTHS, NOT A NAME PER COLUMN. These tables hold people, skills, machines
   and hours — naming a class after each would be a vocabulary nobody could keep
   straight. The colgroup says how much room a column needs and the heading says
   what it is. */
table.tbl-data { table-layout: fixed; width: 100%; min-width: 680px; }

/* THE PERCENTAGES ONLY ADD UP WHILE THERE ARE FEW ENOUGH COLUMNS. c-fill takes what is left, and
   with eleven sized columns there was nothing left: the day-by-day table asked for 116% and its
   first column collapsed to a single character, printing "Tuesday 1 September 2026" downwards, one
   letter per line. A table past about eight columns uses c-narrow and this floor. */
table.tbl-data.is-wide { min-width: 1180px; }

table.tbl-data col.c-tight  { width: 10%; }
table.tbl-data col.c-mid    { width: 16%; }
table.tbl-data col.c-narrow { width: 7%; }
table.tbl-data col.c-fill   { width: auto; }
table.tbl-data col.c-wide   { width: 24%; }
table.tbl-data col.c-widest { width: 32%; }

/* A SUB-LINE IS A LINE. .tbl .cell-sub sets the size and the colour and leaves
   the display alone, because everywhere else in this system it is a <div>. Used
   as a <span> it ran on after the thing above it — "Phanuel Sibanda Day shift"
   on one line. */
.tbl .cell-sub { display: block; }

/* One card a person could be given: the number, then what it is, then how big.
   Three facts that were running together as one wrapped sentence. */
.offer { margin-bottom: 6px; }
.offer:last-child { margin-bottom: 0; }
.offer .ref { margin-right: 6px; }


/* The period buttons are one row; a filter bar that wraps them reads as two filters. */
.filter-bar .field.is-period { flex: 0 0 auto; max-width: none; }
.filter-bar .field.is-period .row.tight { flex-wrap: nowrap; }

/* ---------- Hand-drawn charts: SVG, no library ---------- */
.chart { margin: 4px 0 10px; }
.chart svg { width: 100%; height: auto; display: block; overflow: visible; }
.chart-bars svg, .chart-stack svg { max-width: 760px; }
.chart-donut { display: flex; gap: 22px; align-items: center; flex-wrap: wrap; }
.chart-donut svg { flex: 0 0 180px; width: 180px; }
.chart-line { flex: 1 1 260px; min-width: 240px; max-width: 360px; }
.chart-row { display: flex; gap: 20px; flex-wrap: wrap; align-items: flex-start; }
.chart text { font-size: 12px; fill: var(--ink-2); font-family: inherit; }
.chart text.lbl { fill: var(--steel); }
.chart text.lbl.is-link { fill: var(--navy-500); text-decoration: underline; }
.chart text.val { font-weight: 550; font-variant-numeric: tabular-nums; }
.chart text.centre { font-size: 18px; font-weight: 600; fill: var(--ink); }
.chart text.centre.is-small { font-size: 12px; }
.chart .axis { stroke: var(--line); stroke-width: 1; }
.chart .line { fill: none; stroke: var(--navy-400); stroke-width: 2; stroke-linejoin: round; }
.chart .dot { fill: var(--navy-400); stroke: #fff; stroke-width: 2; }
.chart .dot.is-current { fill: var(--gold-500); }
.chart .ring-empty { stroke: var(--line-2); }
.chart a rect:hover, .chart a path:hover { opacity: .8; }
.chart .t-navy { fill: var(--navy-400); }
.chart .t-gold, .chart rect.is-current { fill: var(--gold-500); }
.chart .t-steel { fill: var(--steel-2); }
.chart .t-ok, .chart .t-worked { fill: var(--ok); }
.chart .t-booked { fill: var(--steel-2); }
.chart .t-warn, .chart .t-idle { fill: var(--warn); }
.chart .t-busy, .chart .t-unallocated { fill: var(--busy); }
.chart .t-bad { fill: var(--bad); }
.chart .t-info { fill: var(--info); }
.chart .t-line { fill: var(--line-2); }
/*  THE WHOLE KEY IS ONE GRID. Every row used to be its own, so "auto" sized to that row's
    own name and its own figure — and five rows lined up with nothing at all. The list owns
    the tracks now and the rows hand their cells straight to it, which is what display:
    contents is for. */
.chart-legend {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: 10px auto auto minmax(0, 1fr);
    gap: 9px 12px;
    align-items: center;
    font-size: var(--fs-sm);
}
.chart-legend > li { display: contents; }
.chart-donut > .chart-legend { flex: 1 1 190px; min-width: 175px; max-width: 560px; }
.chart-legend.is-row { display: flex; flex-direction: row; flex-wrap: wrap; gap: 14px; margin-top: 6px; }
.chart-legend.is-row > li { display: flex; gap: 8px; align-items: baseline; }
.chart-legend .name { color: var(--ink-2); min-width: 0; }
.chart-legend .n  { color: var(--ink); font-weight: 650; font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.chart-legend .pc { color: var(--steel); font-variant-numeric: tabular-nums; text-align: right; min-width: 44px; }
.chart-legend .swatch { width: 10px; height: 10px; border-radius: 50%; }
.swatch.is-navy { background: var(--navy-400); }
.swatch.is-gold { background: var(--gold-500); }
.swatch.is-steel { background: var(--steel-2); }
.swatch.is-ok { background: var(--ok); }
.swatch.is-warn { background: var(--warn); }
.swatch.is-bad { background: var(--bad); }
.swatch.is-info { background: var(--info); }
.swatch.is-busy { background: var(--busy); }
.swatch.is-line { background: var(--line-2); }
.chart-line text { font-size: 10px; }
.chart-line text.val { font-size: 10.5px; }

/* ===================================================== the reporting screens ===

   ONE DESIGN FOR REPORTS AND INSIGHTS, from the owner's own mockups. It sits on the application's
   tokens — the same navy, the same type scale, the same line and surface colours — so it reads as
   this system rather than as a screen bolted onto it. Nothing here is a one-off: every piece below
   is used by more than one screen, which is the whole reason it is a stylesheet and not markup.

   NO SCRIPT DRAWS ANY OF IT. Every chart is inline SVG rendered by a Razor partial; the bars and
   rings below are CSS. That is a house rule, not a preference. */

/* ---------- the page's own head: an icon, a title, a sentence saying what it answers ---------- */

.report-head { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 4px; }

.report-head .ico {
    flex: 0 0 auto;
    width: 34px; height: 34px;
    border-radius: 9px;
    display: grid; place-items: center;
    background: var(--navy-500);
    color: #fff;
    font-size: 1rem;
}

.report-head h1 { font-size: var(--fs-xl); font-weight: 650; letter-spacing: -.015em; margin: 0; }
.report-head .sub { margin: 2px 0 0; font-size: var(--fs-sm); color: var(--steel); }

/* ---------- the headline figures: four across, each with what it is measured against ---------- */

.stat-cards {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 14px;
    margin-bottom: 18px;
}

/* THE COUNT DECIDES THE COLUMNS. Five cards in a four-wide grid leaves one on its own line, which
   reads as an afterthought rather than as one of five. */
.stat-cards[data-count="5"] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.stat-cards[data-count="6"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (min-width: 1700px) { .stat-cards[data-count="6"] { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
.stat-cards[data-count="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }

@media (max-width: 1300px) { .stat-cards, .stat-cards[data-count="5"], .stat-cards[data-count="3"], .stat-cards[data-count="6"] { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px)  { .stat-cards, .stat-cards[data-count="5"], .stat-cards[data-count="3"], .stat-cards[data-count="6"] { grid-template-columns: minmax(0, 1fr); } }

.stat-card {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--sh-xs);
    padding: 14px 16px;
    display: flex;
    gap: 13px;
    align-items: flex-start;
    text-decoration: none;
    color: inherit;
    min-width: 0;
    transition: border-color .12s var(--ease), box-shadow .12s var(--ease);
}

a.stat-card:hover { border-color: var(--navy-400); box-shadow: var(--shadow-sm); }

/* THE PASTEL SQUARE IS THE ONLY DECORATION ON THIS SCREEN, and it is doing a job: four figures in
   a row are hard to tell apart at a glance and the colour is what separates them. */
.stat-card .ico {
    flex: 0 0 auto;
    width: 38px; height: 38px;
    border-radius: 10px;
    display: grid; place-items: center;
    font-size: 1.05rem;
}

.stat-card .ico.is-work    { background: var(--ok-bg);   color: var(--ok); }
.stat-card .ico.is-time    { background: var(--info-bg); color: var(--info); }
.stat-card .ico.is-people  { background: var(--busy-bg); color: var(--busy); }
.stat-card .ico.is-machine { background: var(--bad-bg);  color: var(--bad); }
.stat-card .ico.is-plain   { background: var(--line-2);  color: var(--steel); }
.stat-card .ico.is-navy    { background: #eef3fb;        color: var(--navy-400); }
.stat-card .ico.is-warn    { background: var(--warn-bg); color: var(--warn); }

.stat-card .body { min-width: 0; flex: 1 1 auto; }
.stat-card .label { font-size: var(--fs-xs); color: var(--steel); font-weight: 500; }

.stat-card .value {
    display: block;
    font-size: var(--fs-2xl);
    font-weight: 650;
    letter-spacing: -.03em;
    line-height: 1.15;
    margin-top: 3px;
    font-variant-numeric: tabular-nums;
    overflow-wrap: anywhere;
}

.stat-card .figure { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-top: 3px; }
.stat-card .against { display: block; font-size: var(--fs-xs); color: var(--steel); margin-top: 3px; }

/* Which way it moved, and whether that is the way the business wants. Nought is neither. */
.delta { font-size: var(--fs-xs); font-weight: 600; white-space: nowrap; font-variant-numeric: tabular-nums; }
.delta.is-better { color: var(--ok); }
.delta.is-worse  { color: var(--bad); }
.delta.is-level  { color: var(--steel-2); }

/* And what it moved against, beside it and quieter than it. */
.stat-card .versus { font-size: var(--fs-xs); color: var(--steel); margin-left: 5px; }

/* ---------- ranked bars, a hundred per cent in one bar, and what it adds up to ---------- */
/* The owner's rule for Reports: a chart supports the story and never carries it. So every bar here
   prints its own figure and its share on the row, and nothing has to be hovered to be read. */

.rank { display: flex; flex-direction: column; gap: 10px; }

.rank-row {
    display: grid;
    grid-template-columns: minmax(150px, 1.3fr) minmax(0, 2fr) 96px 56px;
    gap: 14px;
    align-items: center;
    font-size: var(--fs-sm);
}
.rank-row .label { color: var(--ink-2); min-width: 0; }
.rank-row .label .sub { display: block; font-size: var(--fs-xs); color: var(--steel); margin-top: 1px; }
.rank-row .n  { text-align: right; color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }
.rank-row .pc { text-align: right; color: var(--steel); font-variant-numeric: tabular-nums; }

.rank-row > .rank-bar { height: 10px; border-radius: 5px; background: var(--line-2); overflow: hidden; }
.rank-row > .rank-bar > i { display: block; height: 100%; border-radius: 5px; background: var(--navy-400); min-width: 2px; }

.rank-row.is-flagged .label { color: var(--warn); font-weight: 600; }
.rank-row.is-flagged .n     { color: var(--warn); }

.whole { height: 16px; border-radius: 8px; overflow: hidden; display: flex; background: var(--line-2); }
.whole > i { display: block; min-width: 2px; }

.whole-key { display: flex; flex-wrap: wrap; gap: 6px 22px; margin-top: 12px; font-size: var(--fs-sm); }
.whole-part { display: inline-flex; align-items: baseline; gap: 8px; }
.whole-part .dot { width: 9px; height: 9px; border-radius: 2px; align-self: center; }
.whole-part .name { color: var(--ink-2); }
.whole-part .n  { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }
.whole-part .pc { color: var(--steel); font-variant-numeric: tabular-nums; }
.whole-part.is-flagged .name, .whole-part.is-flagged .n { color: var(--warn); }

/* What the rows come to, said in a sentence. Each line is worked out from the figures beside it. */
.report-notes { list-style: none; margin: 14px 0 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
.report-notes li {
    font-size: var(--fs-sm);
    color: var(--ink-2);
    padding-left: 20px;
    position: relative;
    line-height: 1.55;
}
.report-notes li::before {
    content: "";
    position: absolute;
    left: 4px;
    top: .55em;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--gold-500);
}

.rank-row > .rank-bar > i.t-navy { background: var(--navy-400); }
.rank-row > .rank-bar > i.t-gold { background: var(--gold-500); }
.rank-row > .rank-bar > i.t-ok { background: var(--ok); }
.rank-row > .rank-bar > i.t-info { background: var(--info); }
.rank-row > .rank-bar > i.t-busy { background: var(--busy); }
.rank-row > .rank-bar > i.t-warn { background: var(--warn); }
.rank-row > .rank-bar > i.t-bad { background: var(--bad); }
.rank-row > .rank-bar > i.t-steel { background: var(--steel-2); }

.whole > i.t-navy { background: var(--navy-400); }
.whole > i.t-gold { background: var(--gold-500); }
.whole > i.t-ok { background: var(--ok); }
.whole > i.t-info { background: var(--info); }
.whole > i.t-busy { background: var(--busy); }
.whole > i.t-warn { background: var(--warn); }
.whole > i.t-bad { background: var(--bad); }
.whole > i.t-steel { background: var(--steel-2); }

.whole-part .dot.t-navy { background: var(--navy-400); }
.whole-part .dot.t-gold { background: var(--gold-500); }
.whole-part .dot.t-ok { background: var(--ok); }
.whole-part .dot.t-info { background: var(--info); }
.whole-part .dot.t-busy { background: var(--busy); }
.whole-part .dot.t-warn { background: var(--warn); }
.whole-part .dot.t-bad { background: var(--bad); }
.whole-part .dot.t-steel { background: var(--steel-2); }


/* ---------- a sentence the period comes to, with the figure that carries it ---------- */

.callouts { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 14px; margin-bottom: 18px; }

.callout {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--sh-xs);
    padding: 15px 17px;
    display: flex;
    gap: 13px;
    align-items: flex-start;
}

.callout .ico { flex: 0 0 auto; width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; font-size: 1rem; }
.callout .ico.is-good  { background: var(--ok-bg);   color: var(--ok); }
.callout .ico.is-watch { background: var(--warn-bg); color: var(--warn); }
.callout .ico.is-bad   { background: var(--bad-bg);  color: var(--bad); }
.callout .ico.is-plain { background: var(--info-bg); color: var(--info); }

.callout strong { display: block; font-size: var(--fs-md); font-weight: 600; color: var(--ink); line-height: 1.35; }
.callout p { margin: 3px 0 0; font-size: var(--fs-sm); color: var(--steel); line-height: 1.45; }

/* ---------- a card's life, left to right, as the stages it goes through ---------- */

.funnel { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 18px; }

.funnel-stage {
    flex: 1 1 140px;
    min-width: 128px;
    background: var(--surface-2);
    border: 1px solid var(--line);
    padding: 12px 16px 12px 22px;
    text-align: center;
    text-decoration: none;
    color: inherit;
    /* The chevron. The first has a flat left edge and the last a flat right one, so the row reads
       as a run rather than a set of arrows pointing nowhere. */
    clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 50%, calc(100% - 12px) 100%, 0 100%, 12px 50%);
    border-radius: 3px;
}

.funnel-stage:first-child { clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 50%, calc(100% - 12px) 100%, 0 100%); padding-left: 16px; }
.funnel-stage:last-child  { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 12px 50%); }

.funnel-stage .label { display: block; font-size: var(--fs-xs); color: var(--steel); font-weight: 500; }
.funnel-stage .value { display: block; font-size: var(--fs-xl); font-weight: 650; line-height: 1.2; margin-top: 2px; font-variant-numeric: tabular-nums; }
.funnel-stage .foot  { display: block; font-size: var(--fs-xs); color: var(--steel); margin-top: 2px; }
.funnel-stage .share { display: block; font-size: var(--fs-2xs); color: var(--steel-2); margin-top: 1px; font-variant-numeric: tabular-nums; }
/* The bottleneck, marked, so it is seen rather than worked out by comparing seven numbers. */
.funnel-stage.is-worst { box-shadow: inset 0 -3px 0 var(--gold-500); }
.funnel-stage.is-worst .value { color: var(--navy-600); }

a.funnel-stage:hover { background: var(--surface); border-color: var(--navy-400); }

.funnel-stage.s-new      { background: #f4f7fb; }
.funnel-stage.s-approved { background: var(--info-bg); }
.funnel-stage.s-assigned { background: #eef4fb; }
.funnel-stage.s-doing    { background: var(--busy-bg); }
.funnel-stage.s-checking { background: var(--warn-bg); }
.funnel-stage.s-done     { background: var(--ok-bg); }

/* ---------- how old the pile is: a label, a bar, a count and its share ---------- */

.aging { display: flex; flex-direction: column; gap: 11px; }

.aging-row { display: grid; grid-template-columns: 88px 1fr 44px 52px; gap: 12px; align-items: center; font-size: var(--fs-sm); }
.aging-row .label { color: var(--ink-2); }
.aging-row .n     { text-align: right; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.aging-row .pc    { text-align: right; color: var(--steel); font-variant-numeric: tabular-nums; }

.aging-bar { height: 9px; border-radius: 5px; background: var(--line-2); overflow: hidden; }
.aging-bar > i { display: block; height: 100%; border-radius: 5px; background: var(--navy-400); }
.aging-bar > i.is-warn { background: var(--warn); }
.aging-bar > i.is-bad  { background: var(--bad); }
.aging-bar > i.is-ok   { background: var(--ok); }

/* ---------- what needs attention, and who it is about ---------- */

.attention { display: flex; flex-direction: column; gap: 12px; }

.attention-card { display: flex; gap: 12px; align-items: flex-start; padding: 13px 0; border-bottom: 1px solid var(--line-2); }
.attention-card:last-child { border-bottom: 0; padding-bottom: 0; }
.attention-card:first-child { padding-top: 0; }

.attention-card .badge {
    flex: 0 0 auto;
    width: 26px; height: 26px;
    border-radius: 50%;
    display: grid; place-items: center;
    font-size: var(--fs-xs);
    font-weight: 650;
}

.attention-card .badge.is-bad   { background: var(--bad-bg);  color: var(--bad); }
.attention-card .badge.is-warn  { background: var(--warn-bg); color: var(--warn); }
.attention-card .badge.is-info  { background: var(--info-bg); color: var(--info); }
.attention-card .badge.is-ok    { background: var(--ok-bg);   color: var(--ok); }

.attention-card .body { min-width: 0; flex: 1 1 auto; }
.attention-card .title { font-size: var(--fs-sm); font-weight: 600; color: var(--ink); }
.attention-card .count { font-size: var(--fs-xs); color: var(--ink-2); margin-top: 1px; }
.attention-card .who   { font-size: var(--fs-xs); color: var(--steel); margin-top: 3px; line-height: 1.5; }
.attention-card .go    { font-size: var(--fs-xs); color: var(--navy-500); text-decoration: none; margin-top: 5px; display: inline-block; }
.attention-card .go:hover { text-decoration: underline; }

/* ---------- why work stopped: numbered, because they are worked through in order ---------- */

.stopped-list { display: flex; flex-direction: column; }

.stopped-card { display: flex; gap: 13px; align-items: flex-start; padding: 15px 0; border-bottom: 1px solid var(--line-2); }
.stopped-card:last-child { border-bottom: 0; padding-bottom: 0; }
.stopped-card:first-child { padding-top: 0; }

/* THE REASON'S OWN TILE. A reader picks the row out by its shape before reading a word of it, which
   is what the owner's design does with a coloured square beside each blocker. */
.stopped-card .tile {
    flex: 0 0 auto;
    width: 34px; height: 34px;
    border-radius: 9px;
    display: grid; place-items: center;
    font-size: 15px;
}
.stopped-card .tile.t-0 { background: var(--info-bg); color: var(--info); }
.stopped-card .tile.t-1 { background: var(--bad-bg);  color: var(--bad); }
.stopped-card .tile.t-2 { background: var(--warn-bg); color: var(--warn); }

/* The way in, on the right, where a list of rows puts it. */
.stopped-card .go {
    flex: 0 0 auto;
    align-self: center;
    color: var(--steel-2);
    text-decoration: none;
    font-size: 14px;
}
.stopped-card .go:hover { color: var(--navy-500); }

.stopped-card .body { min-width: 0; flex: 1 1 auto; }
.stopped-card .top { display: flex; gap: 10px; align-items: baseline; justify-content: space-between; }
.stopped-card .title { font-size: var(--fs-sm); font-weight: 600; color: var(--ink); }
.stopped-card .meta { font-size: var(--fs-xs); color: var(--steel); white-space: nowrap; font-variant-numeric: tabular-nums; }
.stopped-card .affected { font-size: var(--fs-xs); color: var(--steel); margin-top: 3px; line-height: 1.5; overflow-wrap: anywhere; }
.stopped-card .next { font-size: var(--fs-xs); color: var(--navy-500); margin-top: 3px; }

/* ---------- a shape, not a reading: which way a figure has been going ---------- */

.spark { width: 100%; max-width: 92px; height: 26px; display: block; overflow: visible; }
.spark-line { fill: none; stroke: var(--navy-300); stroke-width: 1.6; vector-effect: non-scaling-stroke; }
.spark-fill { fill: var(--navy-300); opacity: .12; }
.spark-dot  { fill: var(--gold-500); }

/* ---------- a person in a table cell, with their initials ---------- */

.cell-person { display: flex; align-items: center; gap: 9px; min-width: 0; }
.cell-person .avatar { flex: 0 0 auto; }
.cell-person .who { min-width: 0; }
.cell-person .who a { text-decoration: none; color: var(--navy-500); font-weight: 550; }
.cell-person .who a:hover { text-decoration: underline; }
.cell-person .who .cell-sub { font-size: var(--fs-xs); color: var(--steel); }

/* ---------- a ring and the lines that explain it ---------- */

.ring-pair { display: flex; gap: 24px; align-items: center; flex-wrap: wrap; }
.ring-pair > .chart-donut { flex: 0 0 auto; margin: 0; }
.ring-pair > .lines { flex: 1 1 260px; min-width: 240px; display: flex; flex-direction: column; gap: 9px; }

.ring-line { display: flex; gap: 9px; align-items: baseline; font-size: var(--fs-sm); }
.ring-line .dot { flex: 0 0 auto; width: 9px; height: 9px; border-radius: 2px; align-self: center; }
.ring-line .name { color: var(--ink-2); font-weight: 550; min-width: 0; }
.ring-line .rest { color: var(--steel); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }

.ring-line .dot.t-navy  { background: var(--navy-400); }
.ring-line .dot.t-gold  { background: var(--gold-500); }
.ring-line .dot.t-ok    { background: var(--ok); }
.ring-line .dot.t-info  { background: var(--info); }
.ring-line .dot.t-busy  { background: var(--busy); }
.ring-line .dot.t-warn  { background: var(--warn); }
.ring-line .dot.t-bad   { background: var(--bad); }
.ring-line .dot.t-steel { background: var(--steel-2); }

/* ---------- two panels across, which is most of these screens ---------- */

.panel-pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 14px; }
.panel-pair > .panel { margin: 0; }

/* A CHART BESIDE A TABLE. An even split leaves 554px a side and a table's floor is 680, so
   an even split is what turned every table on Reports into a stack of label-and-value lines. */
.panel-pair.is-lopsided { grid-template-columns: minmax(0, 360px) minmax(0, 1fr); }
/* The same split with the table first, where the design puts the wide half on the left. */
.panel-pair.is-wide-left { grid-template-columns: minmax(0, 1fr) minmax(0, 400px); }
@media (max-width: 1160px) { .panel-pair.is-wide-left { grid-template-columns: minmax(0, 1fr); } }

/* A ring and the table it summarises, in one panel. */
.chart-beside { display: grid; grid-template-columns: minmax(0, 300px) minmax(0, 1fr); align-items: start; }
.chart-beside > .chart-half { padding: 4px 4px 18px 18px; }
@media (max-width: 1160px) { .chart-beside { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 1160px) { .panel-pair.is-lopsided { grid-template-columns: minmax(0, 1fr); } }

/* A panel that is mostly a chart wants room; one that is a table wants none. */
.panel-body.is-chart { padding-top: 6px; padding-bottom: 16px; }

/* What the open tab answers, said once under the strip. */
.tab-lead { font-size: var(--fs-sm); color: var(--steel); margin: -4px 0 14px; line-height: 1.5; max-width: 88ch; }

/* Three short lists across, each one a thing somebody can do something about today. */
.panel-trio { display: grid; grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); gap: 14px; }
.panel-trio > .panel { margin: 0; }

.act-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.act-list li { padding: 8px 0; border-bottom: 1px solid var(--line-2); display: flex; flex-direction: column; gap: 2px; }
.act-list li:last-child { border-bottom: 0; padding-bottom: 0; }
.act-list li:first-child { padding-top: 0; }
.act-name { font-size: var(--fs-sm); font-weight: 550; color: var(--navy-500); text-decoration: none; }
.act-name:hover { text-decoration: underline; }
.act-name.is-plain { color: var(--ink); font-weight: 550; }
.act-sub { font-size: var(--fs-xs); color: var(--steel); display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.hint.is-more { color: var(--navy-500); text-decoration: none; }
.panel-head .hint.is-more { color: rgba(255, 255, 255, .88); }
.hint.is-more:hover { text-decoration: underline; }

/* ---------- two series over time, soft-filled, on one scale ---------- */

.chart-lines svg { max-width: 100%; }

/* CREATED IS RED AND COMPLETED IS BLUE, so the two lines are told apart at a glance and by
   somebody who cannot tell green from red. */
.chart-lines .line-a { fill: none; stroke: var(--series-in);  stroke-width: 2.2; stroke-linejoin: round; stroke-linecap: round; }
.chart-lines .line-b { fill: none; stroke: var(--series-out); stroke-width: 2.2; stroke-linejoin: round; stroke-linecap: round; }
.chart-lines .fill-a { fill: var(--series-in);  opacity: .10; }
.chart-lines .fill-b { fill: var(--series-out); opacity: .10; }
.chart-lines .dot-a  { fill: var(--series-in);  stroke: #fff; stroke-width: 1.5; }
.chart-lines .dot-b  { fill: var(--series-out); stroke: #fff; stroke-width: 1.5; }

.chart-key { display: flex; gap: 16px; flex-wrap: wrap; font-size: var(--fs-xs); color: var(--steel); margin-top: 4px; justify-content: flex-end; }
.panel-head .chart-key { margin: 0; color: rgba(255, 255, 255, .82); }
.chart-key span { display: inline-flex; align-items: center; gap: 6px; }
.chart-key i.k { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.chart-key i.k.k-a { background: var(--series-in); }
.chart-key i.k.k-b { background: var(--series-out); }

/* ---------- a ring, and the rows that read it ---------- */

.ring-rows { display: flex; flex-direction: column; gap: 10px; }

.ring-row { display: grid; grid-template-columns: 11px 1fr auto auto; gap: 10px; align-items: center; font-size: var(--fs-sm); }
.ring-row .dot { width: 11px; height: 11px; border-radius: 50%; }
.ring-row .name { color: var(--ink-2); min-width: 0; }
.ring-row .n { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }
.ring-row .pc { color: var(--steel); font-variant-numeric: tabular-nums; min-width: 38px; text-align: right; }
.ring-row a.name { color: var(--navy-500); text-decoration: none; }
.ring-row a.name:hover { text-decoration: underline; }

.ring-row .dot.t-navy  { background: var(--navy-400); }
.ring-row .dot.t-gold  { background: var(--gold-500); }
.ring-row .dot.t-ok    { background: var(--ok); }
.ring-row .dot.t-info  { background: var(--info); }
.ring-row .dot.t-busy  { background: var(--busy); }
.ring-row .dot.t-warn  { background: var(--warn); }
.ring-row .dot.t-bad   { background: var(--bad); }
.ring-row .dot.t-steel { background: var(--steel-2); }

/* ---------- the hundred percent as one bar, and the rows that read it ---------- */

.allocation { height: 14px; border-radius: 7px; overflow: hidden; display: flex; background: var(--line-2); }
.allocation > i { display: block; min-width: 2px; }
.allocation > i.t-navy  { background: var(--navy-400); }
.allocation > i.t-gold  { background: var(--gold-500); }
.allocation > i.t-ok    { background: var(--ok); }
.allocation > i.t-info  { background: var(--info); }
.allocation > i.t-busy  { background: var(--busy); }
.allocation > i.t-warn  { background: var(--warn); }
.allocation > i.t-bad   { background: var(--bad); }
.allocation > i.t-steel { background: var(--steel-2); }

/* A tile is a link on these screens rather than a button, and a link brings its own colours. */
.tile { text-decoration: none; color: inherit; }

.panel-line { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
.panel-line > .help { margin: 0; flex: 1 1 320px; }
.panel-line > .help strong { color: var(--ink); font-weight: 600; }

/* Failure history on an asset: eight named columns that come to 100%. Status had no width at all,
   so with the fixed layout it collapsed onto the Raised date. */
table.tbl-cards.tbl-history .c-job    { width: 10%; }
table.tbl-cards.tbl-history .c-issue  { width: 26%; }
table.tbl-cards.tbl-history .c-sev    { width: 9%; }
table.tbl-cards.tbl-history .c-status { width: 11%; }
table.tbl-cards.tbl-history .c-when   { width: 14%; }
table.tbl-cards.tbl-history .c-who    { width: 13%; }
table.tbl-cards.tbl-history .c-go     { width: 3%; }

/* Estimated against actual: five columns that come to 100%, and the over-or-under stands clear of
   the right-aligned hours beside it. */
table.tbl-estimate col.c-ref   { width: 12%; }
table.tbl-estimate col.c-issue { width: 40%; }
table.tbl-estimate col.c-hours { width: 13%; }
table.tbl-estimate col.c-gap   { width: 22%; }
table.tbl-estimate td.c-gap, table.tbl-estimate th.c-gap { text-align: right; padding-left: 12px; }

/* A day's job card opens the card — the whole block, not only the number. The owner: "can we have
   some clicks here? same for workers." */
.day-card.is-openable { cursor: pointer; border-radius: 4px; margin: 0 -4px; padding: 2px 4px; }
.day-card.is-openable:hover { background: var(--surface-2); }
/* A contractor's booked day says which card, and when they arrive, and opens the card. */
a.book-visit { display: block; text-decoration: none; color: inherit; margin-top: 4px; }
a.book-visit:hover .chip { filter: brightness(.96); }
.book-visit-ref { display: block; margin-top: 5px; font-size: var(--fs-xs); font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.book-visit-when { display: block; font-size: var(--fs-2xs); color: var(--steel); }

/* A group row on Today is the header of its group: blue like the panel bar, white text, and a
   click folds the rows under it. The chevron points down while the group is open. */
table.tbl tr.is-group[data-unfold] { cursor: pointer; }
table.tbl tr.is-group td { background: var(--navy-600); color: #fff; }
table.tbl tr.is-group[data-unfold]:hover td { background: var(--navy-500); }
table.tbl tr.is-group td strong { color: #fff; }
table.tbl tr.is-group td .cell-sub { color: rgba(255, 255, 255, .78); }
table.tbl tr.is-group td a { color: #fff; text-decoration: underline; }
table.tbl tr.is-group .group-go { color: rgba(255, 255, 255, .85); transition: transform .12s var(--ease); display: inline-block; }
table.tbl tbody.is-folded tr.is-group .group-go { transform: rotate(-90deg); }
table.tbl tr.is-group-head th {
    font-size: var(--fs-xs);
    font-weight: 600;
    color: var(--navy-600);
    background: var(--surface-2);
    text-align: left;
    padding: 7px 12px;
    border-bottom: 1px solid var(--line);
}

/* Contractor visits on the card: a line under every row, and a heavier one between permits — the
   owner: "let the contractor visits permits have a distinguished line between rows, and permits". */
dl[data-visit] > dt, dl[data-visit] > dd { border-bottom: 1px solid var(--line-2); padding-bottom: 8px; }
dl[data-visit] > dt:last-of-type, dl[data-visit] > dd:last-of-type { border-bottom: 0; padding-bottom: 0; }
dl[data-visit] + dl[data-visit] { border-top: 2px solid var(--line); margin-top: 10px; padding-top: 14px; }

/* While a screen fetches its next state in the background. */
body.is-loading { cursor: progress; }

/* A panel head that folds its panel: the chevron turns, the rows go, the count stays. */
.panel-head.is-foldable { cursor: pointer; user-select: none; }
.panel-head .fold-go { color: rgba(255, 255, 255, .72); transition: transform .12s var(--ease); }
.panel.is-folded > .panel-head .fold-go { transform: rotate(-90deg); }
.panel.is-folded > .tbl-wrap, .panel.is-folded > .panel-body, .panel.is-folded > .panel-foot { display: none; }

/* What happens next, under the card's location. */
.page-head .sub.next-step { margin-top: 4px; color: var(--ink-2); }
/* The report's answer, one short sentence per line. */
.report-answer .answer-line { display: block; margin-top: 4px; }

/* The note under a group header on Today: the approval window, on its own row. */
table.tbl tr.is-group-note td { background: var(--warn-bg); padding: 8px 12px; }
table.tbl tr.is-group-note .cell-sub { display: inline; margin-left: 8px; }
table.tbl tbody.is-folded tr.is-group-note { display: none; }

/* A column heading wraps rather than clips. "Estimated hours" over a narrow hours column is two
   short lines, not one truncated one. */
table.tbl thead th { white-space: normal; overflow-wrap: normal; }

/* The note under a card's header on Today: the approval window. */
.panel-body.is-note { background: var(--warn-bg); padding: 8px 14px; border-bottom: 1px solid var(--line); }
.panel-body.is-note .cell-sub { display: inline; margin-left: 8px; }

/* The completion checklist: two equal columns. The question and its tick on the left, Not
   applicable and its reason on the right. */
.checklist-row.is-two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; align-items: start; }
.checklist-row.is-two .check-left { display: flex; gap: 8px; align-items: flex-start; }
.checklist-row.is-two .check-right { display: flex; flex-direction: column; gap: 6px; }
.checklist-row.is-two .na-reason:not([hidden]) { display: block; }
.checklist-row.is-two .na-reason .inp { width: 100%; }

/* A share of a whole drawn beside its figure, so a column of percentages reads at a glance. Its
   colours are the swatch colours, so the ring, the swatch and the bar say the same thing. */
.share-bar { display: inline-block; width: 72px; height: 6px; border-radius: 3px; background: var(--surface-2); vertical-align: middle; margin-right: 8px; overflow: hidden; }
.share-bar i { display: block; height: 100%; background: var(--navy-400); }
.share-bar.is-worked i      { background: var(--ok); }
.share-bar.is-booked i      { background: var(--steel-2); }
.share-bar.is-idle i        { background: var(--warn); }
.share-bar.is-unallocated i { background: var(--busy); }

/* ---- Every table comes to its whole width, and the chevron is the same everywhere -------------
   One column per table has no width and takes what is left, so the columns always fill the panel
   with nothing over, whatever its width. The chevron column is 44px, last, at the right edge, and
   only on rows that open something. The owner: "put the > at the end of the tables always". */
table.tbl-data col.c-go, table.tbl-cards col.c-go, table.tbl-cards.tbl-history col.c-go { width: 44px; }
table.tbl th.c-go, table.tbl td.c-go { width: 44px; text-align: right; padding-left: 0; color: var(--steel-2); }
table.tbl tbody tr:hover td.c-go { color: var(--navy-500); }

/* A foot figure sits exactly under the column it totals. Where the body cells are bare numbers
   the foot is bare too; where they are chips, the foot keeps the chip's box so the digits line
   up — the dashboard drill, and any list whose counts are pills. */
table.tbl tfoot .count-bare { padding: 0; min-width: 0; justify-content: flex-start; }
table.tbl:has(tbody .count) tfoot .count-bare { padding: 3px 9px; min-width: 30px; justify-content: center; }

/* The period buttons wrap on a phone rather than pushing the whole page wider than the screen. */
@media (max-width: 700px) {
    .filter-bar .field.is-period { flex: 1 1 100%; max-width: 100%; min-width: 0; }
    .filter-bar .field.is-period .row.tight { flex-wrap: wrap; }
    .window-range { flex-wrap: wrap; row-gap: 8px; }
}

/* A figure in a data table may wrap at a space in a narrow column rather than push the table
   two pixels wider than its panel and grow a scrollbar for nothing. */
table.tbl-data td.n { white-space: normal; }

/* A hundred-per-cent bar living inside a table cell: no key, and the row's own height. */
.whole.is-inline { height: 9px; border-radius: 5px; display: flex; min-width: 90px; }

/* The one big number on a panel that is about a single count. */
.big-count { font-size: var(--fs-2xl); font-weight: 650; line-height: 1.1; margin: 0; letter-spacing: -.03em; }
.big-count.is-bad { color: var(--bad); }

/* ---------- two short lists that live in a narrow column ---------- */
/* A table's floor is 680px and these sit in 400. They are three figures a row, so they are lists. */

.who-list, .kind-list { display: flex; flex-direction: column; }

.who-head {
    display: grid;
    grid-template-columns: 26px minmax(0, 1fr) 62px 74px;
    gap: 10px;
    align-items: center;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--line);
    font-size: var(--fs-xs);
    color: var(--steel);
    font-weight: 600;
}
.who-head .n { text-align: right; }

.who-row {
    display: grid;
    grid-template-columns: 26px minmax(0, 1fr) 62px 74px;
    gap: 10px;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid var(--line-2);
    font-size: var(--fs-sm);
    text-decoration: none;
    color: inherit;
}
.who-row:last-child { border-bottom: 0; padding-bottom: 0; }
.who-row:hover { background: var(--surface-2); }
.who-row .who { color: var(--navy-500); font-weight: 550; min-width: 0; overflow-wrap: anywhere; }
.who-row .n   { text-align: right; font-variant-numeric: tabular-nums; color: var(--ink); font-weight: 600; }

.kind-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 40px auto;
    gap: 10px;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid var(--line-2);
    font-size: var(--fs-sm);
}
.kind-row:last-child { border-bottom: 0; padding-bottom: 0; }
.kind-row .name { color: var(--ink-2); min-width: 0; }
.kind-row .n { text-align: right; font-variant-numeric: tabular-nums; color: var(--ink); font-weight: 600; }

/* ---------- room to breathe between panels on the report screens ---------- */
/* The owner: "leave spaces between tables on reports please, its so cramped." */
.app-content .panel + .panel,
.app-content .panel + .panel-pair,
.app-content .panel-pair + .panel,
.app-content .panel-pair + .panel-pair { margin-top: 22px; }
.panel-pair, .panel-trio { gap: 20px; }

/* The machines table takes three fifths and why work stopped the other two. */
.panel-pair.is-wide-left { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); }

/* A ring's key is text, not a field. Nothing in it draws a box. */
.chart-legend .name,
.chart-legend a.ref {
    border: 0;
    background: none;
    padding: 0;
    box-shadow: none;
    font-size: var(--fs-sm);
}

/* What the axes count, and what the two lines came to. */
.chart-lines .axis-name { font-size: 10px; fill: var(--steel-2); }
.chart-total {
    display: flex;
    gap: 20px;
    flex-wrap: wrap;
    justify-content: flex-end;
    font-size: var(--fs-sm);
    color: var(--steel);
    margin-top: 6px;
}
.chart-total span { display: inline-flex; align-items: center; gap: 7px; }
.chart-total strong { color: var(--ink); font-variant-numeric: tabular-nums; }
.chart-total i.k { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.chart-total i.k.k-a { background: var(--series-in); }
.chart-total i.k.k-b { background: var(--series-out); }

/* A PANEL INSIDE A SPLIT IS NOT "THE PANEL AFTER THE LAST ONE". The spacing rule above matched the
   second panel of every pair as well and pushed it down a row, so the two heads in a split stopped
   lining up. */
.panel-pair > .panel + .panel,
.panel-trio > .panel + .panel { margin-top: 0; }

/* The second figure on a key row is a qualifier, not a column of percentages to compare down the
   page — right-aligning it left the shortest row hanging out on its own. */
.chart-legend .pc { text-align: left; }

/* ---------- a key reads beside its ring, not against the far edge ---------- */
/* The name column used to take every spare pixel, which pushed the figures to the right-hand edge
   of the panel and left a hand's width of nothing between a slice's name and its share. The slack
   goes at the END now, so the four columns sit together and line up down the list. */

/* ---------- what is out of service, in columns ---------- */
.kind-head {
    display: grid;
    grid-template-columns: minmax(0, 1.5fr) minmax(0, 0.6fr) minmax(0, 1fr);
    gap: 12px;
    align-items: center;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--line);
    font-size: var(--fs-xs);
    color: var(--steel);
    font-weight: 600;
}
.kind-head .n { text-align: left; }
.kind-row { grid-template-columns: minmax(0, 1.5fr) minmax(0, 0.6fr) minmax(0, 1fr); gap: 12px; }
.kind-row .n { text-align: left; }

/* A panel a figure scrolls to stops below the sticky top bar rather than under it. */
.app-content section.panel[id] { scroll-margin-top: 84px; }

/* ---------- the card status ring fills the panel it shares with the trend ---------- */
/* The panel is as tall as the chart beside it and the ring sat small in the top of it. Same card,
   bigger content: a larger ring, a larger key, and the pair of them centred in the room. */
#card-status { display: flex; flex-direction: column; }
#card-status > .panel-body.is-chart { flex: 1 1 auto; display: flex; align-items: center; }
#card-status .chart-donut { width: 100%; gap: 30px; }
#card-status .chart-donut svg { flex: 0 0 250px; width: 250px; }
#card-status .chart-legend { font-size: var(--fs-base); gap: 14px 16px; max-width: none; }
#card-status .chart-legend .swatch { width: 12px; height: 12px; }
