/* ==========================================================================
   Kitwell — base stylesheet

   Design priorities, in order:
     1. Legible on a phone in a workshop: large type, high contrast, big targets.
     2. Works with gloves: nothing smaller than 44px is tappable.
     3. Light and dark themes driven by [data-theme] on <html>.
   ========================================================================== */

:root {
    --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

    --radius: 10px;
    --radius-sm: 7px;
    --tap: 44px;

    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-5: 1.5rem;
    --space-6: 2rem;

    --shadow: 0 1px 2px rgb(15 23 42 / 8%), 0 2px 8px rgb(15 23 42 / 6%);
}

/* --- Light theme (default) ------------------------------------------------ */
[data-theme="light"] {
    --bg: #f1f5f9;
    --bg-raised: #ffffff;
    --bg-sunken: #e2e8f0;
    --bg-hover: #f8fafc;
    --text: #0f172a;
    --text-muted: #52627a;
    --border: #cbd5e1;
    --border-strong: #94a3b8;

    --accent: #1d4ed8;
    --accent-text: #ffffff;
    --accent-hover: #1e40af;
    --accent-soft: #dbeafe;

    --ok: #15803d;
    --ok-soft: #dcfce7;
    /* Amber-800 rather than amber-700: on the soft amber background the
       lighter shade measured 4.42:1, just under the 4.5:1 needed for normal
       text — and this palette has to work in poor workshop lighting. */
    --warn: #854d0e;
    --warn-soft: #fef3c7;
    --danger: #b91c1c;
    --danger-soft: #fee2e2;
    --info: #0369a1;
    --info-soft: #e0f2fe;

    --focus: #1d4ed8;
    color-scheme: light;
}

/* --- Dark theme ----------------------------------------------------------- */
[data-theme="dark"] {
    --bg: #0b1120;
    --bg-raised: #172033;
    --bg-sunken: #0f172a;
    --bg-hover: #1e293b;
    --text: #f1f5f9;
    --text-muted: #a3b1c6;
    --border: #33415c;
    --border-strong: #64748b;

    --accent: #60a5fa;
    --accent-text: #06152e;
    --accent-hover: #93c5fd;
    --accent-soft: #1e3a8a;

    --ok: #4ade80;
    --ok-soft: #14532d;
    --warn: #fbbf24;
    --warn-soft: #52350a;
    --danger: #f87171;
    --danger-soft: #601616;
    /* Sky-300 rather than sky-400: on the deep soft-blue background the
       darker shade measured 4.42:1, just under 4.5:1 for normal text. */
    --info: #7dd3fc;
    --info-soft: #0c4a6e;

    --focus: #93c5fd;
    color-scheme: dark;
}

/* --- Reset ---------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

/* The browser's own `[hidden] { display: none }` sits in the user-agent
   stylesheet, so *any* author rule setting `display` beats it — and `.btn` sets
   `display: inline-flex`. Every `element.hidden = true` on a button in this
   application was therefore doing nothing at all: the PAT wizard showed Next on
   its last step and Back on its first, the scanner showed Start and Stop
   together, and the photo upload showed its submit before a file was chosen.
   This is the one place `!important` is the right tool — `hidden` means hidden,
   and no component should have to opt in. */
[hidden] { display: none !important; }

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

body {
    margin: 0;
    font-family: var(--font-sans);
    font-size: 17px;              /* deliberately larger than a typical 16px */
    line-height: 1.55;
    background: var(--bg);
    color: var(--text);
    -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 { line-height: 1.25; margin: 0 0 var(--space-3); }
h1 { font-size: 1.6rem; letter-spacing: -0.01em; }
h2 { font-size: 1.25rem; }
h3 { font-size: 1.05rem; }
p  { margin: 0 0 var(--space-3); }

a { color: var(--accent); }
a:hover { color: var(--accent-hover); }

:focus-visible {
    outline: 3px solid var(--focus);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

.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;
}

.skip-link {
    position: absolute; left: -9999px; top: 0; z-index: 100;
    background: var(--accent); color: var(--accent-text);
    padding: var(--space-3) var(--space-4); border-radius: 0 0 var(--radius) 0;
}
.skip-link:focus { left: 0; }

.container {
    width: 100%;
    max-width: 1200px;
    margin-inline: auto;
    padding-inline: var(--space-4);
}

.muted { color: var(--text-muted); }
.mono { font-family: var(--font-mono); font-size: 0.86em; }
.nowrap { white-space: nowrap; }
.break { word-break: break-word; }
.optional { font-weight: 400; color: var(--text-muted); font-size: 0.9em; }

/* --- Header and navigation ------------------------------------------------ */
.site-header {
    background: var(--bg-raised);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 40;
}

.header-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    min-height: 60px;
}

.brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-weight: 700;
    color: var(--text);
    padding-block: var(--space-2);
}

/* The logo is the link home; the wordmark next to it is not. */
.brand-home {
    display: inline-flex;
    align-items: center;
    border-radius: var(--radius-sm);
    color: inherit;
    text-decoration: none;
}
.brand-home:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.brand-mark {
    display: grid;
    place-items: center;
    width: 34px; height: 34px;
    border-radius: 9px;
    background: var(--accent);
    color: var(--accent-text);
    font-size: 0.8rem;
    font-weight: 800;
    letter-spacing: 0.03em;
}

.brand-name { font-size: 1.02rem; }

.nav-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--tap); height: var(--tap);
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text);
    cursor: pointer;
}

.nav-toggle-bars { display: grid; gap: 4px; }
.nav-toggle-bars span {
    display: block; width: 20px; height: 2px;
    background: currentColor; border-radius: 2px;
}

.primary-nav {
    display: none;
    position: absolute;
    left: 0; right: 0;
    top: 100%;
    background: var(--bg-raised);
    border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow);
    padding: var(--space-3) var(--space-4) var(--space-4);

    /* The open menu hangs off a *sticky* header, so it does not scroll away
       with the page: it is pinned under a bar that is itself pinned. Without a
       height of its own, everything past the bottom of the screen was simply
       unreachable — and how much that is depends on the account, since the menu
       is built from the permissions somebody holds.

       So it gets its own scroll. --header-h is measured and written by app.js
       when the menu opens, because the header grows a little on the accounts
       that show a longer name; the 60px fallback is its min-height, which is
       what it is in practice. dvh rather than vh so a phone's collapsing
       address bar does not leave the last item under it. */
    max-height: calc(100vh - var(--header-h, 60px));
    max-height: calc(100dvh - var(--header-h, 60px));
    overflow-y: auto;

    /* Scrolling to the end of the menu must not then start scrolling the page
       underneath it. */
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
}

.primary-nav.is-open { display: block; }

.nav-list {
    list-style: none;
    margin: 0 0 var(--space-3);
    padding: 0;
    display: grid;
    gap: var(--space-1);
}

.nav-link {
    display: flex;
    align-items: center;
    min-height: var(--tap);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    color: var(--text);
    text-decoration: none;
    font-weight: 600;
}

.nav-link:hover { background: var(--bg-hover); color: var(--text); }

.nav-link.is-active {
    background: var(--accent-soft);
    color: var(--text);
    box-shadow: inset 3px 0 0 var(--accent);
}

.nav-link.is-pending {
    color: var(--text-muted);
    cursor: default;
    gap: var(--space-2);
    font-weight: 500;
}
.nav-link.is-pending:hover { background: transparent; }

.nav-divider {
    height: 1px;
    background: var(--border);
    margin: var(--space-2) 0;
}

.nav-account {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding-top: var(--space-3);
    border-top: 1px solid var(--border);
}

.nav-account-group { position: relative; flex: 1 1 auto; }
.nav-user { gap: var(--space-2); flex: 1 1 auto; }
.nav-user-text { display: grid; line-height: 1.2; }
.nav-user-name { font-weight: 600; }
.nav-user-role { font-size: 0.82rem; color: var(--text-muted); font-weight: 500; }

.avatar {
    display: grid; place-items: center;
    width: 34px; height: 34px;
    border-radius: 50%;
    background: var(--bg-sunken);
    color: var(--text);
    font-weight: 700;
}

.nav-logout { margin: 0; display: flex; align-items: center; gap: var(--space-1); flex-wrap: wrap; }

/* "Everywhere" sits beside "Sign out" as the quieter of the two: same row, so
   it is found without hunting, smaller and dimmer, so it is not reached for by
   accident on the way out of the building. */
.nav-logout-all { opacity: 0.75; }
.nav-logout-all:hover,
.nav-logout-all:focus-visible { opacity: 1; }

/* Desktop navigation */
/* The horizontal bar appears at 1150px.
   ---------------------------------------------------------------------------
   An administrator sees every item, which is the widest the bar ever gets: six
   menu items, the account block, Scan and the brand come to about 1059px of
   content, so the bar needs roughly 1091px of viewport before it fits on one
   row. The margin above that covers an APP_NAME longer than "Kitwell"; the
   wordmark is capped in case it is much longer.

   Below 1150px the drawer is used instead: a vertical list cannot wrap, and it
   is the same control phones already use. Widening the window brings the bar
   back. */
@media (min-width: 1150px) {
    .nav-toggle { display: none; }

    /* The block padding is a mobile tap-target concern; on a desktop the row's
       own min-height and centring do that job, and dropping it here is what
       lets the stacked brand fit inside 60px. */
    .brand { padding-block: 0; }

    /* The desktop brand lockup itself is at the end of the branding section,
       next to the mobile rules it has to outrank — a media query adds no
       specificity, so the two have to be read in source order. */

    .primary-nav {
        display: flex;
        position: static;
        align-items: center;
        justify-content: space-between;
        flex: 1 1 auto;
        gap: var(--space-4);
        background: none;
        border: 0;
        box-shadow: none;
        padding: 0;

        /* The drawer's own scroll, undone. Here the nav is part of the header
           row, so a max-height would clip it and an overflow would hide the
           account dropdown that opens out of it. */
        max-height: none;
        overflow: visible;
    }

    .nav-list {
        display: flex;
        align-items: center;
        gap: var(--space-1);
        margin: 0;
        /* Never wrap. A menu that reflows onto a second row doubles the header
           height and pushes the page down; below the width where the bar fits
           on one line the drawer is used instead. */
        flex-wrap: nowrap;
    }

    .nav-item { flex: 0 0 auto; }

    /* Nothing in the bar wraps its own text either. A squeezed "Sign out"
       breaking onto two lines made the header 10px taller than the menu did,
       which is the same bug one level down. */
    .site-header .btn,
    .nav-account .btn,
    .brand-name { white-space: nowrap; }

    /* The account block keeps its natural width rather than being squeezed
       into one: it is the thing that gets compressed first otherwise. */
    .nav-account { flex: 0 0 auto; }

    /* Slightly tighter than the touch-sized padding used on a phone: six items
       at 12px a side is 144px of air, which is most of a menu item. */
    .nav-link { min-height: 40px; padding-inline: var(--space-2); white-space: nowrap; }

    /* The menu text sits on the foot of the logo.
       ---------------------------------------------------------------------
       Everything in the bar bottom-aligns against a shared floor, and the
       brand and the menu text are each lifted off it by the same variable — so
       they cannot drift apart: change it and both move together.

       `align-items: baseline` is the obvious thing to reach for here and is a
       trap. A menu item is `display: flex`, and a flex container's baseline is
       synthesised from its first child — which differs between a plain link
       (an anonymous text item) and a group's `<summary>` (a real `<span>`).
       Measured, the two disagreed by 6.8px while looking entirely plausible.

       Bottom-aligning alone is not enough either: a menu item's box bottom is
       the bottom of its *line box*, which carries half-leading below the
       glyphs — measured, that left the text sitting a couple of pixels high.
       `line-height: 1` collapses the line box onto the text, after which one
       shared floor value puts the logo's foot and the text's on the same line.

       The account block and the quick actions opt out and stay centred: they
       are controls rather than text on a line, and dropped to the baseline
       they read as having slipped. */
    .header-inner { align-items: flex-end; --bar-floor: 14px; }
    .header-actions { align-self: center; }
    .primary-nav { align-items: flex-end; }
    .nav-account { align-self: center; }
    .nav-list { align-items: flex-end; }

    .brand { padding-bottom: var(--bar-floor); }

    .nav-list > .nav-item > .nav-link,
    .nav-list > .nav-item > .nav-group > .nav-group-toggle {
        align-items: flex-end;
        line-height: 1;
        padding-bottom: var(--bar-floor);
    }

    /* The caret is a shape, not a letter: it belongs against the middle of the
       text rather than sitting on the same line as its feet. */
    .nav-list > .nav-item > .nav-group > .nav-group-toggle .caret { align-self: center; }

    /* The underline sits at the foot of the box, which is now the floor of the
       bar rather than the underside of the text — so it reads as an underline
       for the whole row instead of a strike through the descenders. */
    .nav-link.is-active {
        box-shadow: inset 0 -3px 0 var(--accent);
        background: transparent;
    }

    .nav-divider {
        width: 1px; height: 24px; margin-inline: var(--space-2); background: var(--border);
    }

    /* nowrap: the account area is the theme toggle, the account menu and Sign
       out, and they belong on one line. Allowed to wrap it pushed Sign out
       onto a second row and doubled the header's height. */
    .nav-account { border: 0; padding-top: 0; flex-wrap: nowrap; }

    /* On a desktop the bar shows the avatar alone; the name and role are the
       first thing inside the account menu.
       This is arithmetic, not taste: .container caps at 1200px, and brand
       (142) + six nav items (612) + the account chip with a name (321) + the
       scan button (86) is already over that before gaps. The name was
       previously revealed at 1100px, a width the header can never actually
       reach, so "Settings" wrapped onto a second row at every desktop size.
       In the slide-out menu below 900px there is room, and it still shows. */
    .nav-user-text { display: none; }
}

/* Who you are, at the top of your own menu. */
.nav-account-identity {
    padding: var(--space-2) var(--space-3) var(--space-3);
    border-bottom: 1px solid var(--border);
    margin-bottom: var(--space-1);
    line-height: 1.3;
}
.nav-account-identity strong { display: block; }
.nav-account-identity span { font-size: 0.85rem; color: var(--text-muted); }

/* Redundant on a phone, where the chip itself already shows both. */
@media (max-width: 899px) {
    .nav-account-identity { display: none; }
}

/* --- Main ----------------------------------------------------------------- */
main.container { padding-block: var(--space-5) var(--space-6); }

.page-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-5);
}
.page-head h1 { margin-bottom: var(--space-1); }
.page-head p { margin: 0; }

.section-title {
    margin: var(--space-6) 0 var(--space-3);
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}

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

.card-grid {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}

.notice-card { border-left: 4px solid var(--accent); }

/* A short explanation inside a form, where a whole card would be too much:
   "this account will be emailed an invitation", and the like. */
.notice-inline {
    padding: var(--space-3);
    border-left: 4px solid var(--accent);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
    font-size: 0.94rem;
    margin-bottom: var(--space-4);
}

/* The invited user's own details, above the password they are about to set. */
.invite-summary { margin-bottom: var(--space-3); }

/* Status cards: a coloured edge rather than a coloured panel, so the text
   inside keeps the page's normal contrast. */
.card-ok     { border-left: 4px solid var(--ok); }
.card-warn   { border-left: 4px solid var(--warn); }
.card-danger { border-left: 4px solid var(--danger); }

.plain-list { margin: 0; padding-left: var(--space-4); }
.plain-list li + li { margin-top: var(--space-1); }

/* --- Sub-navigation ------------------------------------------------------- */
/* Tabs inside a settings area, so four related pages are one menu entry. */
.subnav {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    margin-bottom: var(--space-4);
    border-bottom: 1px solid var(--border);
}

.subnav-link {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
    padding: var(--space-2) var(--space-3);
    color: var(--text-muted);
    text-decoration: none;
    font-weight: 600;
    border-bottom: 3px solid transparent;
    margin-bottom: -1px;
}

.subnav-link:hover { color: var(--text); }

.subnav-link.is-active {
    color: var(--text);
    border-bottom-color: var(--accent);
}

/* A short form that sits on one line where there is room: the test-email box,
   and anything else that is a single field plus its button. */
/* A field or two and the button that acts on them, on one line.
   ---------------------------------------------------------------------------
   flex-end, not flex-start. These fields carry a label above the control, so
   aligning to the top put the button level with the *label* and floating above
   the input it belongs to. Aligned to the bottom the two sit on the same line
   and read as one control, which is what they are.

   The wrapped case is why this is not `align-items: center`: at a narrow width
   the button drops to its own line, and centring would leave it half-height
   against a tall field. */
.form-inline {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-2);
    margin: 0;
}
.form-inline .field { margin: 0; }
.form-inline .field-inline { flex: 1 1 16rem; }

/* --- Stats ----------------------------------------------------------------
   Compact by intent. These are a glanceable summary above the day's work, not
   the point of the page, and at full height a dozen of them pushed everything
   else below the fold.

   auto-fit rather than auto-fill: a row with fewer cards than it has room for
   should stretch them across the width, not leave a column of empty space at
   the right-hand end. */
.stat-grid {
    display: grid;
    gap: var(--space-2);
    grid-template-columns: repeat(auto-fit, minmax(145px, 1fr));
    margin-bottom: var(--space-4);
}

.stat-card {
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-left: 4px solid var(--border-strong);
    border-radius: var(--radius);
    padding: var(--space-3);
    display: grid;
    gap: 2px;
    align-content: start;
}

.stat-value { font-size: 1.5rem; font-weight: 700; line-height: 1.1; }
.stat-label { font-size: 0.85rem; line-height: 1.35; color: var(--text-muted); }

.stat-card.stat-danger { border-left-color: var(--danger); }
.stat-card.stat-danger .stat-value { color: var(--danger); }
.stat-card.stat-warn { border-left-color: var(--warn); }
.stat-card.stat-warn .stat-value { color: var(--warn); }
.stat-card.stat-info { border-left-color: var(--info); }

/* --- Tables --------------------------------------------------------------- */
.table-wrap {
    overflow-x: auto;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    -webkit-overflow-scrolling: touch;
}

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

.table th, .table td {
    padding: var(--space-3);
    text-align: left;
    border-bottom: 1px solid var(--border);
    vertical-align: top;
}

.table thead th {
    background: var(--bg-sunken);
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    position: sticky;
    top: 0;
}

.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr:hover { background: var(--bg-hover); }
.table-compact th, .table-compact td { padding: var(--space-2) var(--space-3); }

.row-muted td { opacity: 0.62; }
.cell-sub { font-size: 0.85rem; color: var(--text-muted); }
.empty { text-align: center; color: var(--text-muted); padding: var(--space-6) !important; }

/* The actions column: one or more buttons in the last cell of a row.
 *
 * The cell stays a table-cell and the buttons lay out inline. Anything that
 * takes the `<td>` out of table layout — `display: flex` in particular — stops
 * it stretching to the row height, and since the row separator is a
 * `border-bottom` on the cell, its line is then drawn short of the others.
 *
 * Inline layout means `align-items` and `gap` do not apply, hence
 * `vertical-align` and a margin. `nowrap` keeps a pair of buttons on one line;
 * the table already scrolls horizontally inside `.table-wrap` when it needs to.
 */
td.actions {
    white-space: nowrap;
    vertical-align: middle;
}

td.actions > * { vertical-align: middle; }

/* Half a gap: inline layout contributes the other half itself, because the
   newline between two buttons in the template collapses to one word space. */
td.actions > * + * { margin-left: var(--space-1); }

.inline-form { display: inline; margin: 0; }

/* --- Badges and chips ----------------------------------------------------- */
.badge {
    display: inline-block;
    padding: 2px 10px;
    border-radius: 999px;
    font-size: 0.78rem;
    font-weight: 600;
    background: var(--bg-sunken);
    color: var(--text);
    white-space: nowrap;
}

.badge-ok { background: var(--ok-soft); color: var(--ok); }
.badge-warn { background: var(--warn-soft); color: var(--warn); }
.badge-danger { background: var(--danger-soft); color: var(--danger); }
.badge-role { background: var(--accent-soft); }
.badge-muted { background: var(--bg-sunken); color: var(--text-muted); }

.permission-chips { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--space-2); }
.chip {
    background: var(--bg-sunken);
    border-radius: 999px;
    padding: 4px 12px;
    font-size: 0.82rem;
}
.chip-warn { background: var(--warn-soft); color: var(--warn); font-weight: 600; }

.meta-list { list-style: none; margin: 0 0 var(--space-3); padding: 0; display: grid; gap: var(--space-1); font-size: 0.92rem; }

/* --- Buttons -------------------------------------------------------------- */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: var(--tap);
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
    color: var(--text);
    font: inherit;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
    transition: background-color 0.12s ease, border-color 0.12s ease;
}

.btn:hover { background: var(--bg-hover); color: var(--text); }
.btn:active { transform: translateY(1px); }

.btn-primary {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-text);
}
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: var(--accent-text); }

.btn-warning { background: var(--warn-soft); border-color: var(--warn); color: var(--warn); }
.btn-danger  { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); }

.btn-ghost { background: transparent; border-color: transparent; }
.btn-ghost:hover { background: var(--bg-hover); }

.btn-sm { min-height: 38px; padding: var(--space-1) var(--space-3); font-size: 0.9rem; }
.btn-lg { min-height: 52px; font-size: 1.05rem; padding-inline: var(--space-5); }
.btn-block { width: 100%; }
.btn-icon .btn-label { display: none; }

@media (max-width: 899px) {
    .btn-icon .btn-label { display: inline; }
}

/* --- Forms ---------------------------------------------------------------- */
.form { max-width: 720px; }
.form.card { max-width: 720px; }

.field { margin-bottom: var(--space-4); }

.field-row {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: 1fr;
}
@media (min-width: 640px) {
    .field-row { grid-template-columns: 1fr 1fr; }
}

.label {
    display: block;
    font-weight: 600;
    margin-bottom: var(--space-1);
}

.input {
    display: block;
    width: 100%;
    min-height: var(--tap);
    padding: var(--space-2) var(--space-3);
    font: inherit;
    font-size: 1rem;                 /* >=16px stops iOS zooming on focus */
    color: var(--text);
    background: var(--bg-raised);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
}

.input:focus { border-color: var(--accent); }
.input.has-error { border-color: var(--danger); background: var(--danger-soft); }

select.input { appearance: auto; }
textarea.input { min-height: 120px; resize: vertical; }

.field-hint { margin: var(--space-1) 0 0; font-size: 0.86rem; color: var(--text-muted); }
.field-error { margin: var(--space-1) 0 0; font-size: 0.88rem; font-weight: 600; color: var(--danger); }

.input-with-button { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: stretch; }
.input-with-button .input { flex: 1 1 12rem; min-width: 0; }
.btn-inline { white-space: nowrap; border: 1px solid var(--border); }

.checkbox {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    min-height: var(--tap);
    padding: var(--space-2);
    border-radius: var(--radius-sm);
    cursor: pointer;
}
.checkbox:hover { background: var(--bg-hover); }
.checkbox input[type="checkbox"] {
    width: 22px; height: 22px;
    margin-top: 2px;
    accent-color: var(--accent);
    flex: 0 0 auto;
}

.fieldset {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--space-4);
    margin: 0 0 var(--space-4);
}
.fieldset legend { padding-inline: var(--space-2); font-weight: 700; }

.form-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    align-items: center;
}

/* A form inside .form-actions is just a wrapper around one button (each POST
   needs its own form and CSRF token), so it must not add layout of its own. */
.form-actions > form { margin: 0; }

/* Several checkboxes that each carry a name and some detail — the reminder
   notify list. Columns where there is room, one per row on a phone. */
.check-grid {
    display: grid;
    gap: var(--space-2);
    grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
}
.check-grid .checkbox { align-items: flex-start; }
.check-grid .muted { display: block; font-size: 0.88rem; }

.sticky-actions {
    position: sticky;
    bottom: 0;
    background: var(--bg);
    padding: var(--space-3) 0;
    border-top: 1px solid var(--border);
}

/*
 * Filter bars come in two shapes: unlabelled boxes (`.field-inline` with an
 * `.sr-only` label) and ones with visible labels above each control. Both have
 * to put every control on the same bottom edge, which is why this aligns to
 * flex-end rather than centring. Centring lines up the middles of columns of
 * different heights, so a submit button with no label above it floats above
 * the boxes it sits beside — which is exactly what it did on /loler.
 */
.filter-bar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    align-items: flex-end;
    margin-bottom: var(--space-4);
}
.field-inline { margin: 0; flex: 1 1 180px; }

/* A field's usual bottom margin is spacing between rows of a form. Inside a
   filter bar the gap does that job, and the margin only pushes the controls off
   the bar's own baseline. */
.filter-bar .field { margin-bottom: 0; flex: 1 1 180px; }
.filter-bar .field-aligned { flex: 0 0 auto; }
.filter-bar .btn { flex: 0 0 auto; }

/* --- Flash messages ------------------------------------------------------- */
.flash-stack { display: grid; gap: var(--space-2); margin-bottom: var(--space-4); }

.flash {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-sm);
    border-left: 4px solid var(--border-strong);
    background: var(--bg-raised);
    font-weight: 500;
}

.text-danger { color: var(--danger); }

/* --- Two-factor ----------------------------------------------------------- */

/* A code is read off a screen and typed in a hurry: wide, spaced and monospaced
   beats an ordinary input that turns 0 and O into the same shape. */
.input-code {
    font-family: var(--font-mono);
    font-size: 1.4rem;
    letter-spacing: 0.35em;
    text-align: center;
}

.totp-enrol { display: grid; gap: var(--space-4); align-items: start; }
@media (min-width: 700px) {
    .totp-enrol { grid-template-columns: auto 1fr; }
}

/* White behind the symbol whatever the theme: a QR inverted by a dark mode is
   one a lot of readers will not see at all. */
.totp-qr {
    background: #fff;
    padding: var(--space-3);
    border-radius: var(--radius);
    line-height: 0;
}

.backup-codes {
    list-style: none;
    margin: var(--space-3) 0 0;
    padding: 0;
    display: grid;
    gap: var(--space-2);
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
}
.backup-codes li {
    padding: var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
    text-align: center;
    letter-spacing: 0.06em;
}

/* The outcome of the last reminder run, kept on the page rather than left in a
   banner that now times out. */
.last-run {
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
}
.last-run h3 { margin-bottom: var(--space-1); }
.last-run p { margin-bottom: var(--space-1); }

/* Fading out rather than snapping away: a banner that simply disappears looks
   like the page reloaded. Nothing depends on the transition finishing — the
   script removes the element on a timer of its own. */
.flash.is-leaving {
    opacity: 0;
    transform: translateY(-4px);
    transition: opacity 240ms ease, transform 240ms ease;
}

@media (prefers-reduced-motion: reduce) {
    .flash.is-leaving { transition: none; transform: none; }
}

.flash-success { background: var(--ok-soft); border-left-color: var(--ok); color: var(--ok); }
.flash-error   { background: var(--danger-soft); border-left-color: var(--danger); color: var(--danger); }
.flash-warning { background: var(--warn-soft); border-left-color: var(--warn); color: var(--warn); }
.flash-info    { background: var(--info-soft); border-left-color: var(--info); color: var(--info); }

/* A field that decides more than the ones around it — "Asset no longer faulty"
   on the maintenance form, which is the one control on that page that changes
   what the register says about the machine. Lifted off the card so it is not
   read as one more optional tick on the way to the Save button. */
.field-emphasis {
    padding: var(--space-3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
}

/* A flash that ends in a button rather than a dismiss cross — the failed-test
   prompt that offers to mark an asset faulty. The text takes the room it needs
   and the button stays whole beside it; below 640px they stack, because a
   button squeezed to three characters wide is not a button anybody presses. */
.flash-action { align-items: center; flex-wrap: wrap; }
.flash-action .flash-text { flex: 1 1 22rem; }
.flash-action .btn { flex: 0 0 auto; }

@media (max-width: 640px) {
    .flash-action { align-items: stretch; }
    .flash-action .btn { width: 100%; }
}

.flash-close {
    background: none; border: 0; cursor: pointer;
    font-size: 1.4rem; line-height: 1; color: inherit; opacity: 0.7;
    min-width: 32px; min-height: 32px;
}
.flash-close:hover { opacity: 1; }

/* --- Roles and permissions ------------------------------------------------ */
.role-card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
.group-title { margin-top: var(--space-4); color: var(--text-muted); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; }

.permission-group-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); margin-bottom: var(--space-2); }
.permission-group-head h2 { margin: 0; }

.permission-list {
    display: grid;
    gap: var(--space-1);
    grid-template-columns: 1fr;
}
@media (min-width: 760px) {
    .permission-list { grid-template-columns: 1fr 1fr; }
}

.permission-item { align-items: flex-start; border: 1px solid transparent; }
.permission-item span { display: block; }
.permission-name { font-weight: 600; }
.permission-desc { font-size: 0.86rem; }
.permission-slug { font-size: 0.78rem; }

/* --- Per-account permission overrides -------------------------------------
 *
 * A role's picker is a checkbox: on or off. An account's is three-way — follow
 * the role, allow anyway, withhold anyway — so it cannot be a checkbox, and a
 * <select> per permission across forty rows is a page of grey boxes nobody can
 * scan. Three small radio pills read at a glance and say which of the three is
 * in force without being read.
 */
.permission-group-head h3 { margin: 0; font-size: 1rem; }

/* One row per permission rather than two columns. A role's picker is a
   checkbox and two fit a line; this row carries a name, a description, a slug
   *and* three buttons, and squeezing that into half a card leaves 18px for the
   buttons. */
.permission-list-override { grid-template-columns: 1fr; }
@media (min-width: 760px) {
    .permission-list-override { grid-template-columns: 1fr; }
}

.permission-override {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
        "name   choice"
        "desc   choice"
        "slug   choice";
    align-items: center;
    gap: 0 var(--space-3);
    padding: var(--space-2);
    border-radius: var(--radius-sm);
    border: 1px solid transparent;
}
.permission-override .permission-name { grid-area: name; }
.permission-override .permission-desc { grid-area: desc; }
.permission-override .permission-slug { grid-area: slug; }
.permission-override .permission-choice { grid-area: choice; }

/* An account that differs from its role is the thing somebody is looking for
   on this page, so it is marked on the row rather than only in the control. */
.permission-override.is-overridden { border-color: var(--accent); background: var(--bg-sunken); }

.permission-override .permission-choice { display: flex; gap: 2px; flex: 0 0 auto; }

.permission-override .permission-choice .choice {
    display: block;
    position: relative;
    margin: 0;
}
.permission-choice .choice input {
    position: absolute;
    opacity: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    cursor: pointer;
}
.permission-choice .choice > span {
    display: block;
    min-width: 62px;
    padding: var(--space-1) var(--space-2);
    border: 1px solid var(--border);
    background: var(--bg-raised);
    font-size: 0.8rem;
    line-height: 1.2;
    text-align: center;
    white-space: nowrap;
}
.permission-choice .choice:first-child > span { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
.permission-choice .choice:last-child > span  { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }

.choice-sub { display: block; font-size: 0.7rem; opacity: 0.7; }

.permission-choice .choice input:checked + span {
    border-color: var(--border-strong);
    background: var(--bg-hover);
    font-weight: 600;
}
.permission-choice .choice-allow input:checked + span {
    border-color: var(--ok);
    background: var(--ok-soft);
    color: var(--ok);
}
.permission-choice .choice-deny input:checked + span {
    border-color: var(--danger);
    background: var(--danger-soft);
    color: var(--danger);
}
.permission-choice .choice input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 1px; }
.permission-choice .choice input:disabled + span { opacity: 0.5; cursor: not-allowed; }

.changes pre {
    margin: var(--space-2) 0 0;
    padding: var(--space-2);
    background: var(--bg-sunken);
    border-radius: var(--radius-sm);
    max-width: 420px;
    overflow-x: auto;
}

/* --- Auth screens --------------------------------------------------------- */
.body-centred {
    min-height: 100vh;
    min-height: 100dvh;
    display: grid;
    place-items: center;
    background: var(--bg);
    padding: var(--space-4);
}

.auth-shell { width: 100%; max-width: 430px; display: grid; gap: var(--space-4); justify-items: center; }

.auth-card {
    width: 100%;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-5);
    box-shadow: var(--shadow);
}

.auth-brand {
    display: flex; align-items: center; gap: var(--space-2);
    margin-bottom: var(--space-5);
    font-weight: 700; font-size: 1.1rem;
}

.auth-title { margin-bottom: var(--space-1); }
.auth-subtitle { color: var(--text-muted); margin-bottom: var(--space-5); font-size: 0.95rem; }
.auth-help { margin: var(--space-4) 0 0; font-size: 0.88rem; text-align: center; }

.theme-toggle-standalone { border: 1px solid var(--border); background: var(--bg-raised); }

.theme-icon {
    width: 18px; height: 18px;
    border-radius: 50%;
    background: currentColor;
    box-shadow: inset -6px -3px 0 0 var(--bg-raised);
}
[data-theme="dark"] .theme-icon { box-shadow: none; }

/* --- Error pages ---------------------------------------------------------- */
.error-page { text-align: center; padding-block: var(--space-6); }
.error-status { font-size: 3rem; font-weight: 800; color: var(--text-muted); margin: 0; line-height: 1; }

/* --- Footer --------------------------------------------------------------- */
.site-footer {
    border-top: 1px solid var(--border);
    background: var(--bg-raised);
    padding-block: var(--space-4);
    font-size: 0.85rem;
}
.footer-inner { display: flex; flex-wrap: wrap; gap: var(--space-2); justify-content: space-between; }

/* --- Asset register ------------------------------------------------------- */
.filter-card { padding: var(--space-3) var(--space-4); }

.search-row {
    display: flex;
    gap: var(--space-2);
    align-items: stretch;
}
.input-search { flex: 1 1 auto; font-size: 1.05rem; }

.filter-details { margin-top: var(--space-3); }
.filter-details summary {
    cursor: pointer;
    padding: var(--space-2);
    margin: calc(var(--space-2) * -1);
    border-radius: var(--radius-sm);
    font-weight: 600;
    min-height: var(--tap);
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

/* `display: flex` above already stops the browser drawing its own marker (a
   summary only renders one while it is a list-item), and this says so on
   purpose rather than by accident — the .caret after the label is the
   indicator. The label is a span so the triangle sits directly after the word
   rather than being pushed to the far side of a full-width row. */
.filter-details summary { list-style: none; }
.filter-details summary::-webkit-details-marker { display: none; }
.filter-summary-label { flex: 0 0 auto; }

.filter-details summary:hover { background: var(--bg-hover); }
.filter-details[open] summary { margin-bottom: var(--space-3); }

.filter-grid {
    display: grid;
    gap: var(--space-3);
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    margin-bottom: var(--space-3);
}
.filter-grid .field { margin-bottom: 0; }

.check-row { display: flex; flex-wrap: wrap; gap: var(--space-1); }
.checkbox-compact {
    min-height: 38px;
    padding: var(--space-1) var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-size: 0.92rem;
    gap: var(--space-2);
}
.checkbox-compact input[type="checkbox"] { width: 20px; height: 20px; margin-top: 0; }
/* Bare checkboxes (row selection, copy screens) show no label text, so the
   box itself would otherwise be the whole target. Keep the 22px box but give
   it a full-size hit area around it. */
.checkbox-bare {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: var(--tap);
    min-width: var(--tap);
    padding: 0;
    gap: 0;
    margin: calc(var(--space-2) * -1) 0;
}
.checkbox-bare:hover { background: none; }
.checkbox-bare input[type="checkbox"] { margin-top: 0; }

.bulk-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
}
.bulk-bar-left { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.bulk-bar .table-wrap { flex: 1 0 100%; order: 3; }
.bulk-bar .is-disabled { opacity: 0.5; cursor: not-allowed; }

.col-check { width: 2.5rem; }
.table-assets td { vertical-align: top; }

.asset-link { display: block; text-decoration: none; color: var(--text); }
.asset-link:hover .asset-name { text-decoration: underline; }
.asset-tag {
    display: inline-block;
    font-weight: 700;
    color: var(--accent);
    margin-right: var(--space-2);
}
.asset-name { font-weight: 600; }

/* Make and model, sitting under the tag and the name they identify. Muted and
   a size down so the row still reads name first — but attached to the name,
   rather than adrift in the sub-line beneath the photo where it read as a
   separate fact about the row. */
.asset-make {
    display: block;
    font-size: 0.85rem;
    font-weight: 400;
    color: var(--text-muted);
}

/* Status and condition colouring: the badge text always says what it means,
   so colour is reinforcement rather than the only signal. */
.status-active         { background: var(--ok-soft); color: var(--ok); }
.status-on-hire        { background: var(--info-soft); color: var(--info); }
.status-in-maintenance { background: var(--warn-soft); color: var(--warn); }
.status-faulty         { background: var(--danger-soft); color: var(--danger); font-weight: 700; }
.status-retired        { background: var(--bg-sunken); color: var(--text-muted); }

/* Urgency, on a fault report. Deliberately not the same palette as condition:
   a Critical fault on an item in Good condition is an ordinary thing to see,
   and two badges the same colour would read as one fact stated twice. */
.urgency-critical { background: var(--danger-soft); color: var(--danger); font-weight: 700; }
.urgency-high     { background: var(--danger-soft); color: var(--danger); }
.urgency-medium   { background: var(--warn-soft); color: var(--warn); }
.urgency-low      { background: var(--bg-sunken); color: var(--text-muted); }

.condition-excellent      { background: var(--ok-soft); color: var(--ok); }
.condition-good           { background: var(--ok-soft); color: var(--ok); }
.condition-fair           { background: var(--warn-soft); color: var(--warn); }
.condition-poor           { background: var(--danger-soft); color: var(--danger); }
.condition-out-of-service { background: var(--danger-soft); color: var(--danger); font-weight: 700; }

.pagination {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    align-items: center;
    margin-top: var(--space-4);
}
.pagination-info { margin-left: auto; font-size: 0.9rem; }

.empty-state { text-align: center; padding: var(--space-6) var(--space-4); }
.empty-state h2 { margin-bottom: var(--space-2); }

/* --- Asset detail --------------------------------------------------------- */
.eyebrow {
    margin: 0 0 var(--space-1);
    font-weight: 700;
    color: var(--accent);
    letter-spacing: 0.04em;
}

.badge-row { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0; }
.head-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/* Several equally-weighted things you might do next, inside a card — as on a
   client item, where a PAT test, a LOLER examination and a routine are three
   choices rather than three steps. */
.action-row { display: flex; flex-wrap: wrap; gap: var(--space-3); }
.action-row .btn { flex: 1 1 12rem; justify-content: center; }

.detail-grid {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: 1fr;
    align-items: start;
}
@media (min-width: 900px) {
    .detail-grid { grid-template-columns: minmax(0, 1fr) 320px; }
    .detail-side { position: sticky; top: 76px; }

    /* A sticky element taller than the viewport can never be scrolled to its
       end — the overhang is simply unreachable. The asset sidebar runs from the
       barcode down to Manage, so on a short window the delete and archive
       buttons could not be reached at all. It scrolls with the page instead.
       The other detail pages have short sidebars, where sticky still helps. */
    .detail-side-flow { position: static; }

    /* The template editor's side column carries the merge-field reference and
       a preview, both of which need more room than a 320px rail. */
    .detail-grid-wide { grid-template-columns: minmax(0, 1fr) minmax(0, 26rem); }
}

/* --- Email templates ------------------------------------------------------ */
textarea.textarea-tall { min-height: 22rem; font-family: var(--font-mono, monospace); font-size: 0.92rem; }

.merge-fields { margin: 0; }
.merge-fields dt { font-weight: 600; margin-top: var(--space-2); }
.merge-fields dd { margin: 0 0 var(--space-1); color: var(--text-muted); font-size: 0.9rem; }

.email-preview {
    margin: 0;
    padding: var(--space-3);
    background: var(--bg-sunken);
    border-radius: var(--radius-sm);
    white-space: pre-wrap;
    word-break: break-word;
    font-size: 0.9rem;
}

.card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
}
.card-head h2 { margin: 0; }

.count-pill {
    display: inline-block;
    min-width: 1.6em;
    padding: 0 0.5em;
    border-radius: 999px;
    background: var(--bg-sunken);
    color: var(--text-muted);
    font-size: 0.8rem;
    font-weight: 700;
    text-align: center;
}

.detail-list { margin: 0; display: grid; gap: var(--space-2); }
.detail-list > div {
    display: grid;
    grid-template-columns: 1fr;
    gap: 0;
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--border);
}
.detail-list > div:last-child { border-bottom: 0; padding-bottom: 0; }
.detail-list dt { font-size: 0.85rem; color: var(--text-muted); }
.detail-list dd { margin: 0; font-weight: 500; }

@media (min-width: 560px) {
    .detail-list > div { grid-template-columns: 200px 1fr; align-items: baseline; gap: var(--space-3); }
}

.detail-list-tight > div { padding-bottom: var(--space-1); }

/* Label above, value full width beneath. The two-column form above works for a
   short value in a wide column; in the 320px sidebar a date, a time and a name
   get squeezed into half of that and wrap into a ragged block. */
.detail-list-stacked > div { padding-bottom: var(--space-1); }
@media (min-width: 560px) {
    .detail-list-stacked > div { grid-template-columns: 1fr; gap: 0; }
}
.prewrap { white-space: pre-wrap; }
.external-hint { font-size: 0.85em; }

.child-list, .file-list, .timeline { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }

.child-item, .file-item {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
}
.child-item.is-retired { opacity: 0.6; }
.child-link { text-decoration: none; color: var(--text); }
.child-link:hover .child-name { text-decoration: underline; }
.child-name { font-weight: 600; }
.child-meta { display: flex; gap: var(--space-2); flex-wrap: wrap; }

.file-icon {
    display: grid;
    place-items: center;
    width: 40px; height: 40px;
    border-radius: var(--radius-sm);
    background: var(--danger-soft);
    color: var(--danger);
    font-size: 0.68rem;
    font-weight: 800;
    flex: 0 0 auto;
}
.file-body { display: grid; gap: 2px; flex: 1 1 220px; min-width: 0; }
.file-title { font-weight: 600; }
.file-meta { font-size: 0.84rem; }
.file-actions { display: flex; gap: var(--space-2); align-items: center; }

.upload-form {
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
    max-width: none;
}
input[type="file"].input { padding: var(--space-2); line-height: 1.6; }

.barcode-card { text-align: center; }
.barcode-holder {
    background: #ffffff;
    border-radius: var(--radius-sm);
    padding: var(--space-3);
    margin-bottom: var(--space-3);
    overflow-x: auto;
}
.barcode-holder svg { display: block; margin: 0 auto; max-width: 100%; height: auto; }

.danger-card { border-left: 4px solid var(--warn); }
.delete-form { margin-top: var(--space-3); padding-top: var(--space-3); border-top: 1px solid var(--border); }

/* The Manage card in the asset rail: ordinary actions at the top, then a ruled
   line, then the ones you cannot take back. The rule keeps "Copy" and "Delete
   permanently" from reading as one undifferentiated stack in a 320px column. */
.manage-actions { display: grid; gap: var(--space-2); }
.manage-divider {
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--border);
}
.manage-divider .muted { margin-top: 0; }

.timeline li {
    display: grid;
    gap: 2px;
    padding: var(--space-2) 0 var(--space-2) var(--space-4);
    border-left: 2px solid var(--border);
    position: relative;
}
.timeline li::before {
    content: "";
    position: absolute;
    left: -5px; top: 1.1rem;
    width: 8px; height: 8px;
    border-radius: 50%;
    background: var(--accent);
}
.timeline-when, .timeline-who { font-size: 0.84rem; }

/* --- Maintenance ---------------------------------------------------------- */
.due-overdue     { background: var(--danger-soft); color: var(--danger); font-weight: 700; }
.due-due-soon    { background: var(--warn-soft); color: var(--warn); }
.due-scheduled   { background: var(--ok-soft); color: var(--ok); }
.due-unscheduled { background: var(--bg-sunken); color: var(--text-muted); }
.due-inactive    { background: var(--bg-sunken); color: var(--text-muted); }

.result-completed { background: var(--ok-soft); color: var(--ok); }
.result-partial   { background: var(--warn-soft); color: var(--warn); }
.result-failed    { background: var(--danger-soft); color: var(--danger); }
.result-deferred  { background: var(--bg-sunken); color: var(--text-muted); }

a.stat-card { text-decoration: none; color: inherit; }
a.stat-link:hover { background: var(--bg-hover); border-color: var(--accent); }

.schedule-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
.schedule-item {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
}
.schedule-item.is-closed { opacity: 0.6; }
.schedule-main { display: grid; gap: 2px; min-width: 0; }
.schedule-meta { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

.log-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); }
.log-item {
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
}
.log-list-compact .log-item { padding: var(--space-2) var(--space-3); }
.log-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); }
.log-date { font-weight: 700; }
.log-work { margin: 0 0 var(--space-2); }

.log-meta { display: grid; gap: var(--space-1); margin: 0 0 var(--space-2); font-size: 0.88rem; }
.log-meta > div { display: flex; gap: var(--space-2); }
.log-meta dt { color: var(--text-muted); min-width: 8rem; }
.log-meta dd { margin: 0; }

.photo-grid-compact { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: var(--space-2); margin-bottom: 0; }

/* Evidence inside a history entry, where photos may already sit above it. */
.file-list-compact { margin-top: var(--space-2); }
.photo-grid-compact .photo-link { aspect-ratio: 1 / 1; }

.radio-cards { display: grid; gap: var(--space-2); }
.radio-card {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    min-height: var(--tap);
}
.radio-card:hover { background: var(--bg-hover); }
.radio-card:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.radio-card input[type="radio"] { width: 22px; height: 22px; margin-top: 2px; accent-color: var(--accent); flex: 0 0 auto; }
.radio-card > span { display: grid; gap: 2px; }
.radio-card .muted { font-size: 0.88rem; }

[data-when-type][hidden] { display: none; }

/* --- CSV import ----------------------------------------------------------- */
.import-steps, .import-notes { margin: 0; padding-left: 1.4em; display: grid; gap: var(--space-2); }

.preview-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}
.preview-actions h2 { margin: 0; }
.preview-actions p { margin: 0; }

.table-preview td { vertical-align: top; }
.table-preview .preview-notes { min-width: 16rem; white-space: normal; }
.preview-notes span { display: block; font-size: 0.85rem; }
.preview-error { color: var(--danger); font-weight: 600; }
.preview-warning { color: var(--warn); }

tr.preview-error td { background: var(--danger-soft); }
tr.preview-warning td { background: var(--warn-soft); }

/* --- Reports -------------------------------------------------------------- */
.report-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    text-decoration: none;
    color: inherit;
    margin-bottom: 0;
}
.report-card:hover { border-color: var(--accent); background: var(--bg-hover); }
.report-card h3 { margin: 0; color: var(--accent); }
.report-card p { margin: 0; font-size: 0.92rem; flex: 1 1 auto; }
.report-card-go { font-weight: 600; font-size: 0.9rem; }

.table-report { font-size: 0.92rem; }
.table-report td, .table-report th { white-space: nowrap; }
.table-report td .cell-sub { white-space: normal; }
.align-right { text-align: right; }
.report-footnote { margin-top: var(--space-3); font-size: 0.86rem; }

.report-print { padding: var(--space-4); }
.report-print-head { margin-bottom: var(--space-4); }
.report-print-org { font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; margin: 0 0 var(--space-1); }
.report-print-meta { font-size: 0.85rem; }
.report-print-summary {
    list-style: none;
    margin: 0 0 var(--space-4);
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-4);
}
.report-print-summary strong { font-size: 1.3rem; }

@media print {
    .report-print { padding: 0; }
    .table-report { font-size: 9pt; }
    .table-report thead { display: table-header-group; }
    .table-report tr { break-inside: avoid; }
}

/* --- Hires ---------------------------------------------------------------- */
.hire-out      { background: var(--info-soft); color: var(--info); }
.hire-overdue  { background: var(--danger-soft); color: var(--danger); font-weight: 700; }
.hire-returned { background: var(--ok-soft); color: var(--ok); }

.hire-banner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    margin-bottom: var(--space-4);
    border: 1px solid var(--border);
    border-left: 5px solid var(--info);
    border-radius: var(--radius);
    background: var(--info-soft);
}
.hire-banner.is-overdue { border-left-color: var(--danger); background: var(--danger-soft); }
.hire-banner-main { display: grid; gap: 2px; min-width: 0; }
.hire-banner-label {
    font-size: 0.72rem; font-weight: 800; letter-spacing: 0.1em;
    text-transform: uppercase; opacity: 0.75;
}
.hire-banner-headline { font-size: 1.1rem; font-weight: 700; }
.hire-banner-detail { font-size: 0.9rem; }
.hire-banner-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/* --- Faults --------------------------------------------------------------- */

/* Radio buttons that each need a line of explanation — the urgency scale.
   A <select> would fit in less room, but the whole difficulty of urgency is
   that people disagree about what the words mean, so the descriptions have to
   be visible at the moment of choosing rather than hidden behind a click. */
.radio-stack { display: grid; gap: var(--space-2); }
.radio-stack .checkbox { align-items: flex-start; }
.radio-stack .field-hint { display: block; margin: 2px 0 0; }

/* The current fault, across the top of the asset page. Shaped like the hire
   banner deliberately: both answer "what is going on with this thing right
   now?", and a reader should not have to learn two layouts for that. */
.fault-banner {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    margin-bottom: var(--space-4);
    border: 1px solid var(--border);
    border-left: 5px solid var(--danger);
    border-radius: var(--radius);
    background: var(--danger-soft);
}
.fault-banner-main { display: grid; gap: var(--space-2); min-width: 0; flex: 1 1 22rem; }
.fault-banner-label {
    font-size: 0.72rem; font-weight: 800; letter-spacing: 0.1em;
    text-transform: uppercase; opacity: 0.75;
}
.fault-banner-description { font-size: 1.05rem; font-weight: 600; white-space: pre-wrap; }
.fault-banner-detail { font-size: 0.9rem; }
.fault-banner-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/* A few small thumbnails beside the description, not a gallery. The evidence
   is worth a glance from here; the full-size versions are one tap away. */
.fault-thumbs { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0; padding: 0; list-style: none; }
.fault-thumbs img {
    width: 72px; height: 72px;
    object-fit: cover;
    display: block;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: var(--bg-sunken);
}

/* --- API documentation and keys -------------------------------------------- */

/* The one time a secret is on screen. Big, monospaced and selectable in one
   gesture, because the instruction is "copy this now" and the page means it. */
.key-reveal {
    display: block;
    padding: var(--space-3);
    background: var(--bg-sunken);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-sm);
    font-size: 1.05rem;
    word-break: break-all;
    user-select: all;
}

.code-block {
    padding: var(--space-3);
    background: var(--bg-sunken);
    border-radius: var(--radius-sm);
    overflow-x: auto;
    white-space: pre-wrap;
    word-break: break-word;
    font-size: 0.88rem;
}

.api-op { border-top: 1px solid var(--border); padding: var(--space-2) 0; }
.api-op:first-of-type { border-top: 0; }
.api-op-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    cursor: pointer;
    padding: var(--space-1) 0;
}
.api-op-body { display: grid; gap: var(--space-3); padding: var(--space-3) 0 var(--space-2); }

.api-method {
    flex: 0 0 auto;
    min-width: 4.5rem;
    text-align: center;
    padding: 2px 8px;
    border-radius: var(--radius-sm);
    font-size: 0.72rem;
    font-weight: 800;
    letter-spacing: 0.06em;
    background: var(--bg-sunken);
    color: var(--text-muted);
}
.api-method-get    { background: var(--info-soft); color: var(--info); }
.api-method-post   { background: var(--ok-soft); color: var(--ok); }
.api-method-patch,
.api-method-put    { background: var(--warn-soft); color: var(--warn); }
.api-method-delete { background: var(--danger-soft); color: var(--danger); }

.api-path { font-weight: 600; }
.api-summary { color: var(--text-muted); font-size: 0.9rem; }
.api-url { word-break: break-all; font-size: 0.82rem; }
.api-response { max-height: 26rem; overflow: auto; }
.api-params .input-sm { min-width: 8rem; }
.input-sm { padding: var(--space-1) var(--space-2); font-size: 0.9rem; }

/* One fault report in the history list. */
.fault-entry { display: grid; gap: var(--space-2); padding: var(--space-3) 0; border-top: 1px solid var(--border); }
.fault-entry:first-of-type { border-top: 0; padding-top: 0; }
.fault-entry-text { white-space: pre-wrap; margin: 0; }

/* What closed a fault, set apart from what the fault was — the two are
   different statements and reading them as one paragraph made the repair look
   like part of the complaint. */
.fault-entry-resolution {
    padding: var(--space-2) var(--space-3);
    border-left: 3px solid var(--ok);
    background: var(--ok-soft);
    border-radius: var(--radius-sm);
    font-size: 0.92rem;
}
.fault-entry-resolution .prewrap { display: block; margin-top: var(--space-1); }

.hire-summary {
    display: grid;
    gap: var(--space-3);
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.hire-summary.is-overdue { border-left: 5px solid var(--danger); }
.hire-summary > div { display: grid; gap: 2px; }
.hire-summary .label { font-size: 0.78rem; color: var(--text-muted); margin: 0; }
.hire-summary strong { font-size: 1.1rem; }

.quick-actions .quick-action-row { display: flex; flex-wrap: wrap; gap: var(--space-3); }

/* --- Scanning ------------------------------------------------------------- */
.scan-icon {
    display: block;
    width: 18px; height: 14px;
    border-left: 3px solid currentColor;
    border-right: 3px solid currentColor;
    box-shadow: inset 0 0 0 1px transparent, 6px 0 0 -5px currentColor, -6px 0 0 -5px currentColor;
}

.scan-page { display: grid; gap: var(--space-4); }
.input-scan { font-size: 1.2rem; letter-spacing: 0.04em; }

/* The viewport, the reticle and the result card belonged to the full-page
   camera scanner on /scan, which was removed once that page's field grew the
   same Scan button every other barcode field has. What is left is the status
   line, which the modal scanner still writes to. */
.scan-status { margin: var(--space-3) 0 0; font-weight: 600; }
.scan-status-ok { color: var(--ok); }
.scan-status-error { color: var(--danger); }

/* --- Client portal ----------------------------------------------------- */
.my-hires { list-style: none; margin: 0 0 var(--space-4); padding: 0; display: grid; gap: var(--space-3); }

.my-hire {
    border: 1px solid var(--border);
    border-left: 5px solid var(--info);
    border-radius: var(--radius);
    background: var(--bg-raised);
    overflow: hidden;
}
.my-hire.is-overdue { border-left-color: var(--danger); }

.my-hire-link {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3);
    text-decoration: none;
    color: var(--text);
    min-height: 84px;
}
.my-hire-link:hover { background: var(--bg-hover); }

.my-hire-media { flex: 0 0 auto; }
.my-hire-media img,
.my-hire-media-empty {
    display: block;
    width: 72px; height: 72px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
}
.my-hire-media-empty {
    background: var(--bg-sunken) linear-gradient(135deg, transparent 45%, var(--border) 45%, var(--border) 55%, transparent 55%);
}

.my-hire-body { display: grid; gap: 2px; min-width: 0; flex: 1 1 auto; }
.my-hire-tag { font-weight: 700; color: var(--accent); font-size: 0.85rem; }
.my-hire-name { font-weight: 600; font-size: 1.05rem; }
.my-hire-dates { display: grid; gap: 1px; font-size: 0.88rem; color: var(--text-muted); }
.my-hire-due { font-weight: 600; color: var(--text); }
.my-hire-due.is-overdue { color: var(--danger); }
.my-hire-chevron { font-size: 1.6rem; color: var(--text-muted); flex: 0 0 auto; }

.my-hire-photo { display: block; }
.my-hire-photo img { width: 100%; height: auto; border-radius: var(--radius-sm); display: block; }

.pat-simple.is-pass { border-left: 4px solid var(--ok); }
.pat-simple.is-fail { border-left: 4px solid var(--danger); }
.pat-simple-result { margin-bottom: var(--space-3); }

/* --- PAT testing ---------------------------------------------------------- */
.pat-status-current        { background: var(--ok-soft); color: var(--ok); }
.pat-status-due-soon       { background: var(--warn-soft); color: var(--warn); }
.pat-status-overdue        { background: var(--danger-soft); color: var(--danger); font-weight: 700; }
.pat-status-failed         { background: var(--danger-soft); color: var(--danger); font-weight: 700; }
.pat-status-never-tested   { background: var(--warn-soft); color: var(--warn); font-weight: 700; }
.pat-status-no-retest-date { background: var(--bg-sunken); color: var(--text-muted); }
.pat-status-not-required,
.pat-status-retired        { background: var(--bg-sunken); color: var(--text-muted); }

.pat-pass { background: var(--ok-soft); color: var(--ok); font-weight: 700; }
.pat-fail { background: var(--danger-soft); color: var(--danger); font-weight: 700; }

/* The at-a-glance banner on the asset page. */
.pat-status {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    margin-bottom: var(--space-4);
    border: 1px solid var(--border);
    border-left: 5px solid var(--border-strong);
    border-radius: var(--radius);
    background: var(--bg-raised);
}
.pat-status-ok     { border-left-color: var(--ok); background: var(--ok-soft); }
.pat-status-warn   { border-left-color: var(--warn); background: var(--warn-soft); }
.pat-status-danger { border-left-color: var(--danger); background: var(--danger-soft); }

/* Secondary text sitting on a tinted banner takes the banner's own text
   colour: the standard muted grey does not hold 4.5:1 against these
   backgrounds. Hierarchy comes from size instead. */
.pat-status .muted,
.hire-banner .muted,
.hire-summary .muted { color: inherit; }

.pat-status-main { display: grid; gap: 2px; min-width: 0; }
.pat-status-label {
    font-size: 0.72rem;
    font-weight: 800;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    opacity: 0.75;
}
.pat-status-headline { font-size: 1.15rem; font-weight: 700; }
.pat-status-detail { font-size: 0.9rem; }
.pat-status-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }

/* Chronological test history. */
.pat-history { list-style: none; margin: 0 0 var(--space-4); padding: 0; display: grid; gap: var(--space-3); }

.pat-record {
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-left: 4px solid var(--ok);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
}
.pat-record.is-fail { border-left-color: var(--danger); }
.pat-history-compact .pat-record { padding: var(--space-2) var(--space-3); }

.pat-record-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
}
.pat-date { font-weight: 700; }
.pat-retest { font-size: 0.86rem; margin-left: auto; }

.pat-readings {
    display: grid;
    gap: var(--space-2);
    grid-template-columns: 1fr;
    margin: 0 0 var(--space-2);
    font-size: 0.9rem;
}
@media (min-width: 560px) {
    .pat-readings { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 900px) {
    .pat-readings { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.pat-readings > div {
    display: grid;
    gap: 1px;
    padding: var(--space-2);
    background: var(--bg-raised);
    border-radius: var(--radius-sm);
}
.pat-readings dt { font-size: 0.78rem; color: var(--text-muted); }
.pat-readings dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; }

.reading-pass { color: var(--ok); }
.reading-fail { color: var(--danger); }

.pat-remedial {
    padding: var(--space-2) var(--space-3);
    margin-bottom: var(--space-2);
    border-radius: var(--radius-sm);
    background: var(--danger-soft);
    color: var(--danger);
}
.pat-remedial p { margin: 0; }

.pat-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }

.radio-cards-inline { grid-template-columns: 1fr; }
@media (min-width: 560px) {
    .radio-cards-inline { grid-template-columns: 1fr 1fr; }
}

[data-pat-when-class][hidden],
[data-pat-when-result][hidden] { display: none; }

/* --- Condition photos ----------------------------------------------------- */
.photo-grid {
    list-style: none;
    margin: 0 0 var(--space-4);
    padding: 0;
    display: grid;
    gap: var(--space-3);
    /* 140px keeps two tiles across a 375px phone, which is what makes a
       condition history scannable rather than a single tall column. */
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}

@media (min-width: 700px) {
    .photo-grid { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
}

.photo-tile {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
    overflow: hidden;
}
.photo-tile.is-primary { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

/* What a photograph shows, under it. Evidence on a LOLER report, where the
   description is part of the record rather than a nicety. */
.photo-tile-caption {
    margin: 0;
    padding: var(--space-2);
    font-size: 0.82rem;
    line-height: 1.35;
    color: var(--text-muted);
    border-top: 1px solid var(--border);
}

.photo-link {
    position: relative;
    display: block;
    aspect-ratio: 4 / 3;
    background: var(--bg-sunken);
    overflow: hidden;
}
.photo-link img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform 0.15s ease;
}
.photo-link:hover img { transform: scale(1.03); }

.photo-flag {
    position: absolute;
    top: var(--space-2);
    left: var(--space-2);
    padding: 2px 8px;
    border-radius: 999px;
    background: var(--accent);
    color: var(--accent-text);
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.photo-caption {
    display: grid;
    gap: 2px;
    padding: var(--space-2) var(--space-3);
    font-size: 0.85rem;
}
.photo-date { font-weight: 600; }
.photo-text { color: var(--text); }
.photo-by { font-size: 0.78rem; }

.photo-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    padding: 0 var(--space-2) var(--space-2);
    margin-top: auto;
}

/* Upload controls */
.photo-upload { border-top: 1px solid var(--border); }
.photo-inputs { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-2); }

.btn-file { cursor: pointer; }
.btn-file input[type="file"] { position: absolute; width: 1px; height: 1px; opacity: 0; }

.photo-preview {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin: var(--space-3) 0;
}

.photo-preview-item {
    position: relative;
    width: 96px;
    border-radius: var(--radius-sm);
    overflow: hidden;
    border: 1px solid var(--border);
    background: var(--bg-sunken);
}
.photo-preview-item img { display: block; width: 100%; height: 72px; object-fit: cover; }
.photo-preview-item.is-too-big { border-color: var(--danger); }
.photo-preview-size {
    display: block;
    padding: 2px 4px;
    font-size: 0.7rem;
    text-align: center;
    color: var(--text-muted);
}
.photo-preview-item.is-too-big .photo-preview-size { color: var(--danger); font-weight: 700; }

.photo-meta { margin-top: var(--space-3); }

.photo-edit-list { display: grid; gap: var(--space-3); }
.photo-edit-row {
    display: grid;
    gap: var(--space-3);
    grid-template-columns: 64px 1fr;
    align-items: end;
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--border);
}
.photo-edit-row .field { margin-bottom: 0; }
.photo-edit-row .btn { grid-column: 2; justify-self: start; }
.photo-edit-thumb { width: 64px; height: 64px; object-fit: cover; border-radius: var(--radius-sm); grid-row: span 3; }

@media (min-width: 760px) {
    .photo-edit-row { grid-template-columns: 64px 2fr 1fr auto; align-items: end; }
    .photo-edit-thumb { grid-row: auto; }
    .photo-edit-row .btn { grid-column: auto; }
}

/* Thumbnails in the register */
.asset-link-media { display: flex; align-items: flex-start; gap: var(--space-3); }
.asset-link-text { display: block; min-width: 0; }
.asset-thumb {
    width: 48px;
    height: 48px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    flex: 0 0 auto;
    background: var(--bg-sunken);
}
.asset-thumb-empty {
    display: block;
    background: var(--bg-sunken) linear-gradient(135deg, transparent 45%, var(--border) 45%, var(--border) 55%, transparent 55%);
}

/* Lightbox */
.lightbox {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    padding: var(--space-4);
    background: rgb(2 6 23 / 92%);
    border: 0;
    width: 100%;
    height: 100%;
    max-width: 100%;
    max-height: 100%;
}
/* The UA stylesheet hides a closed dialog with `dialog:not([open])
   { display: none }`, but ANY author rule beats the UA origin — so the
   `display: flex` above kept the element painted after close(). The result was
   a full-viewport, 92%-opaque overlay that swallowed every click: the Close
   button looked broken and the page looked "slightly translucent", because you
   were seeing it through the dialog rather than through the backdrop.
   Restore the UA behaviour explicitly. Do not remove. */
.lightbox:not([open]) { display: none; }

.lightbox::backdrop { background: rgb(2 6 23 / 92%); }

.lightbox img {
    max-width: 100%;
    max-height: 78vh;
    object-fit: contain;
    border-radius: var(--radius-sm);
    background: #000;
}

.lightbox-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    width: 100%;
    max-width: 900px;
    color: #f1f5f9;
}
.lightbox-info { display: grid; gap: 2px; font-size: 0.9rem; }
.lightbox-meta { color: #a3b1c6; font-size: 0.82rem; }
.lightbox-controls { display: flex; gap: var(--space-2); }
.lightbox .btn { background: rgb(255 255 255 / 12%); border-color: rgb(255 255 255 / 25%); color: #f1f5f9; }
.lightbox .btn:hover { background: rgb(255 255 255 / 22%); }

/* --- Copy screens --------------------------------------------------------- */
.copy-table th[scope="row"] { font-weight: 600; white-space: nowrap; }
.copy-table td, .copy-table th { vertical-align: middle; }
.target-search { margin-bottom: var(--space-3); }
.hidden-form { display: none; }
.conditional-block[hidden] { display: none; }
.form-wide { max-width: 900px; }

/* --- Reference data (categories, locations) -------------------------------
   A tree of compact rows. One row is a single line: caret, name, what uses it,
   and the three actions. Everything else about an entry lives on its own page,
   which is what keeps this readable when there are fifty of them.

   The indent is on the nested list rather than on each row, so a branch and the
   rule down its left edge move together. */
.tree-card { padding: var(--space-2) var(--space-3); }

.tree-list { list-style: none; margin: 0; padding: 0; }

.tree-list .tree-list {
    margin-left: 10px;
    padding-left: var(--space-4);
    border-left: 1px solid var(--border);
}

.tree-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: 5px var(--space-2);
    border-radius: var(--radius-sm);
    min-height: 34px;
    cursor: pointer;
    list-style: none;
}
.tree-row::-webkit-details-marker { display: none; }
.tree-row:hover { background: var(--bg-hover); }

/* A leaf discloses nothing, so it gets no control — but it keeps the caret's
   width, or the names would not line up down the column. */
.tree-row.is-leaf { cursor: default; }
.caret-placeholder { visibility: hidden; }

.tree-row.is-inactive .tree-name { color: var(--text-muted); text-decoration: line-through; }

.tree-name { font-weight: 600; }

/* Pushed to the right and allowed to collapse first: on a narrow screen the
   name and the actions are what matter. */
.tree-meta {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: 0.86rem;
}
.tree-count { color: var(--text-muted); white-space: nowrap; }

.tree-actions { display: flex; align-items: center; gap: var(--space-1); flex: 0 0 auto; }

/* Only on hover or keyboard focus, so a long list is a list of names rather
   than a wall of buttons — but always visible on a touch screen, where there
   is no hover to reveal them with. */
@media (hover: hover) and (pointer: fine) {
    .tree-actions { opacity: 0; transition: opacity 120ms ease; }
    .tree-row:hover .tree-actions,
    .tree-row:focus-within .tree-actions { opacity: 1; }
}

.btn-xs {
    min-height: 26px;
    padding: 0 var(--space-2);
    font-size: 0.8rem;
    border-radius: var(--radius-sm);
}

@media (max-width: 599px) {
    /* The count is the first thing to go: the name and the actions are why you
       are on this page. */
    .tree-count { display: none; }
    .tree-list .tree-list { padding-left: var(--space-3); }
}

/* --- Print ---------------------------------------------------------------- */
@media print {
    .site-header, .site-footer, .filter-bar, .form-actions, .btn { display: none !important; }
    body { background: #fff; color: #000; font-size: 12pt; }
    .card, .table-wrap { border: 1px solid #999; box-shadow: none; }
}

@media (prefers-reduced-motion: reduce) {
    * { transition: none !important; animation: none !important; }
}

/* ==========================================================================
   Guided PAT test, scan buttons and pass/fail toggles
   ========================================================================== */

/* --- A scan button welded to the end of a barcode field ------------------ */
.input-with-scan {
    display: flex;
    gap: var(--space-2);
    align-items: stretch;
}
.input-with-scan .input { flex: 1 1 auto; min-width: 0; }

.btn-scan {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
}
.btn-scan-icon { flex: 0 0 auto; }

/* On a phone the label is noise next to the icon; the aria-label still reads. */
@media (max-width: 30rem) {
    .btn-scan span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
}

/* --- The camera modal the scan button opens ------------------------------ */
.scan-modal {
    width: min(560px, 94vw);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised);
    color: var(--text);
    padding: var(--space-4);
}
.scan-modal:not([open]) { display: none; }   /* see the lightbox note above */
.scan-modal::backdrop { background: rgb(2 6 23 / 78%); }

.scan-modal-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}
.scan-modal-title { margin: 0; font-size: 1.1rem; }
.scan-modal-frame {
    background: #000;
    border-radius: var(--radius-sm);
    overflow: hidden;
    aspect-ratio: 4 / 3;
}
.scan-modal-frame video { width: 100%; height: 100%; object-fit: cover; display: block; }
.scan-modal-hint { color: var(--text-muted); font-size: 0.85rem; margin: var(--space-2) 0 0; }

/* --- Pass / fail toggle -------------------------------------------------- */
.verdict { display: flex; gap: var(--space-2); flex: 0 0 auto; }
.verdict-input {
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
}
.verdict-label {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 84px;
    min-height: 44px;            /* the workshop tap target */
    padding: 0 var(--space-3);
    border: 2px solid var(--border);
    border-radius: var(--radius-sm);
    font-weight: 600;
    cursor: pointer;
    background: var(--bg-raised);
    color: var(--text);
}
.verdict-label:hover { border-color: var(--text-muted); }
.verdict-input:focus-visible + .verdict-label {
    outline: 3px solid var(--focus);
    outline-offset: 2px;
}
.verdict-input:checked + .verdict-pass {
    background: var(--ok);
    border-color: var(--ok);
    color: #fff;
}
.verdict-input:checked + .verdict-fail {
    background: var(--danger);
    border-color: var(--danger);
    color: #fff;
}

/* --- One check, one row -------------------------------------------------- */
.check-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    align-items: flex-start;
    justify-content: space-between;
    padding: var(--space-3) 0;
    border-top: 1px solid var(--border);
}
.check-row:first-of-type { border-top: 0; }
.check-row.has-error {
    background: var(--danger-soft);
    border-radius: var(--radius-sm);
    padding-inline: var(--space-3);
}
.check-text { flex: 1 1 16rem; min-width: 0; }
.check-text h3 { margin: 0 0 4px; font-size: 1rem; }

.input-with-unit { display: flex; align-items: center; gap: var(--space-2); max-width: 16rem; }
.input-with-unit .input { flex: 1 1 auto; min-width: 0; }
.input-unit { color: var(--text-muted); font-weight: 600; }

.guideline { margin-top: var(--space-2); }
.guideline-tag {
    display: inline-block;
    background: var(--warn-soft);
    color: var(--warn);
    border-radius: 999px;
    padding: 1px 8px;
    margin-right: 6px;
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

/* --- Wizard shell -------------------------------------------------------- */
.pat-wizard-layout {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 62rem) {
    .pat-wizard-layout { grid-template-columns: 20rem minmax(0, 1fr); align-items: start; }
    .pat-context { position: sticky; top: var(--space-4); }
}

.pat-context-list { display: grid; gap: var(--space-2); margin: 0 0 var(--space-3); }
.pat-context-list > div { display: flex; justify-content: space-between; gap: var(--space-2); }
.pat-context-list dt { color: var(--text-muted); font-size: 0.85rem; margin: 0; }
.pat-context-list dd { margin: 0; text-align: right; }

.wizard-progress {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    list-style: none;
    margin: 0 0 var(--space-3);
    padding: 0;
    counter-reset: wizard;
}
.wizard-progress-step {
    counter-increment: wizard;
    font-size: 0.85rem;
    color: var(--text-muted);
    padding: 4px 10px;
    border-radius: 999px;
    border: 1px solid var(--border);
    cursor: pointer;
}
.wizard-progress-step::before { content: counter(wizard) ". "; font-weight: 700; }
.wizard-progress-step.is-current {
    color: var(--text);
    border-color: var(--accent);
    background: var(--accent-soft);
    font-weight: 600;
}
/* Colour reports the step's own result, not how far through you are: green
   only when everything in it passed, red as soon as anything in it failed,
   and otherwise the default grey. */
.wizard-progress-step.is-pass { color: var(--ok); border-color: var(--ok); background: var(--ok-soft); }
.wizard-progress-step.is-fail { color: var(--danger); border-color: var(--danger); background: var(--danger-soft); font-weight: 700; }

/* The current step keeps its accent ring on top of either. */
.wizard-progress-step.is-pass.is-current,
.wizard-progress-step.is-fail.is-current { border-color: var(--accent); }

.wizard-nav {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-top: var(--space-3);
}
.wizard-count { margin-inline: auto; }

.result-banner {
    padding: var(--space-3);
    border-radius: var(--radius-sm);
    border-left: 4px solid var(--border);
    margin-bottom: var(--space-3);
}
.result-banner p { margin: 0; font-weight: 600; }
.result-pending { background: var(--bg-sunken); border-left-color: var(--text-muted); }
.result-pass    { background: var(--ok-soft);     border-left-color: var(--ok); }
.result-fail    { background: var(--danger-soft); border-left-color: var(--danger); }

[data-wizard-save][disabled] { opacity: 0.55; cursor: not-allowed; }

/* A plain list of links — used to pick an asset to test. */
.link-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-1); }
.link-list a {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--tap);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    text-decoration: none;
    color: var(--text);
}
.link-list a:hover { background: var(--bg-hover); }

/* ==========================================================================
   Navigation: grouped menu and the scan quick-action

   One set of markup drives both layouts. A group is a <details>; below 900px
   it is an accordion inside the slide-out menu, above it the same element
   becomes a drop-down. Nothing here duplicates a mobile-only structure.
   ========================================================================== */

.header-actions {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-left: auto;          /* pins the scan button to the right */
    flex: 0 0 auto;
}

/* Scanning is a persistent quick-action, deliberately not a menu item. */
.scan-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-width: var(--tap);
    min-height: var(--tap);
    padding-inline: var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--accent);
    color: var(--accent-text);
    text-decoration: none;
    font-weight: 600;
}
.scan-action:hover { background: var(--accent-hover); color: var(--accent-text); }
.scan-action-label { display: none; }

/* --- Groups -------------------------------------------------------------- */
.nav-group summary { list-style: none; cursor: pointer; }
.nav-group summary::-webkit-details-marker { display: none; }

.nav-group-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    width: 100%;
}

/* The expand/collapse triangle, wherever a <details> is used as a disclosure:
   the nav drop-downs, the account menu, the filter panel on the register. One
   shape and one rotation rule, so a triangle means the same thing everywhere.
   `.nav-caret` stays as the nav's own hook for the reduced-motion rule. */
.caret {
    width: 0; height: 0;
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-top: 5px solid currentColor;
    flex: 0 0 auto;
    transition: transform 120ms ease;
}
details[open] > summary .caret { transform: rotate(180deg); }

.nav-sublist {
    list-style: none;
    margin: var(--space-1) 0 var(--space-2);
    padding: 0 0 0 var(--space-4);
    display: grid;
    gap: 2px;
    border-left: 2px solid var(--border);
}
.nav-sublink { font-weight: 500; }

/* Matches the bar's own breakpoint above: these rules describe the horizontal
   bar's drop-downs, so they have to appear and disappear with it. */
@media (min-width: 1150px) {
    .scan-action-label { display: inline; }

    .nav-item-group { position: relative; }

    /* The panel floats over the page rather than pushing the bar taller. */
    .nav-group > .nav-sublist {
        position: absolute;
        top: calc(100% + 4px);
        left: 0;
        z-index: 50;
        min-width: 15rem;
        margin: 0;
        padding: var(--space-2);
        border: 1px solid var(--border);
        border-left: 1px solid var(--border);
        border-radius: var(--radius);
        background: var(--bg-raised);
        box-shadow: var(--shadow);
    }

    .nav-group-toggle { width: auto; }

    /* A group is rendered `open` when the current page is inside it, so the
       menu shows where you are. On a phone that is an accordion and it simply
       expands. Here the panel floats over the page, so an auto-opened one
       would land on top of the page you just navigated to and stay there.
       Keep it shut until the group is actually reached for.

       :focus-within rather than a class, so this still works with JavaScript
       off: clicking the summary focuses it and the panel appears. The script
       drops the attribute on first interaction, after which the group behaves
       like any other. */
    .nav-group[data-nav-autoopen]:not(:focus-within) > .nav-sublist { display: none; }

    /* The account menu sits at the right-hand end of the bar, so its panel
       hangs from the right edge — anchored left it would run off the page. */
    .nav-account-group > .nav-sublist { left: auto; right: 0; }

    /* On a phone the chip fills its row; on a desktop it must not grow, or it
       pushes Sign out onto a second line and doubles the header's height. */
    .nav-account-group { flex: 0 0 auto; }
}

@media (prefers-reduced-motion: reduce) {
    .caret { transition: none; }
}

/* --- Audit trail on a maintenance record ---------------------------------- */

.audit-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-4); }

.audit-entry + .audit-entry { border-top: 1px solid var(--border); padding-top: var(--space-4); }

.audit-meta { margin-bottom: var(--space-2); font-size: 0.92rem; color: var(--text-muted); }
.audit-reason { font-style: italic; }

/* The old value is the thing being corrected, so it reads as struck through
   rather than merely quieter — at a glance you want to see what it *was*. */
.audit-table { font-size: 0.92rem; }
.audit-table .audit-was { text-decoration: line-through; color: var(--text-muted); }

/* --- Branding -------------------------------------------------------------
   The logo is constrained by height and never by width, so it keeps its
   proportions whatever shape it is; max-width stops a very wide wordmark from
   pushing the navigation off the bar. object-fit: contain letterboxes rather
   than stretches, which is the whole point. */

.brand-logo-wrap { display: inline-flex; align-items: flex-end; }

/* Mobile: logo and wordmark side by side, sitting on the same baseline — the
   text aligns with the *bottom* of the logo rather than its middle, which is
   what makes the pair read as one lockup instead of two loose objects. */
.brand-stack {
    display: flex;
    align-items: flex-end;
    gap: var(--space-2);
    min-width: 0;
}

.brand-logo {
    display: block;
    height: 34px;
    width: auto;
    max-width: 190px;
    object-fit: contain;
}

/* The wordmark's own line box carries space under the baseline; trimming it is
   what actually lines the text bottom up with the foot of the logo. */
.brand-stack .brand-name { line-height: 1; padding-bottom: 2px; }

/* Both variants ship in the markup and CSS picks: the theme can change without
   a page load, so the server cannot know which one to send. */
[data-theme="light"] .brand-logo-dark,
[data-theme="dark"]  .brand-logo-light { display: none; }

/* Where only one variant was uploaded it is used for both themes — one logo is
   better than none. It is the only .brand-logo present, so nothing hides it. */

/* Desktop: logo and wordmark side by side, both centred on the bar's own line
   so the name sits level with the menu items rather than above or below them.
   ---------------------------------------------------------------------------
   They were stacked, which was the cheapest way to buy width back when the menu
   had one more item in it. Dropping Dashboard paid for the wider lockup; the
   arithmetic is in the breakpoint comment further up.

   This block has to come *after* the mobile rules above, not with the rest of
   the desktop navigation: both are single-class selectors, so source order is
   the only thing deciding which wins. */
@media (min-width: 1150px) {
    /* The wordmark stands on the logo's foot, which is the same line the menu
       items stand on — so the whole bar reads as one row of type rather than a
       lockup floating above a menu. It was centred against the logo until the
       menu was brought down to that line, and then it was the odd one out. */
    .brand-stack { align-items: flex-end; }

    /* Large enough to balance a menu-sized wordmark, small enough to leave the
       header at 60px. max-width caps a wide banner logo — object-fit
       letterboxes it rather than squashing it. */
    .brand-stack-logo .brand-logo { height: 36px; max-width: 128px; }

    /* Item for item with the menu: same size (inherited, because the body is
       17px and a rem is 16px), same weight, and standing on the same line —
       `line-height: 1` plus the descent correction, exactly as the menu items
       do it, so all three feet coincide.

       max-width is the safety net the logo already has: APP_NAME is whatever
       the operator typed, and an unbounded wordmark is the one thing left in
       this bar that could put it back on two rows. A flex item is blockified,
       so overflow and the ellipsis both apply here. */
    .brand-stack .brand-name {
        font-size: inherit;
        font-weight: 600;
        line-height: 1;
        padding-bottom: 0;
        max-width: 12rem;
        overflow: hidden;
        text-overflow: ellipsis;
    }
}

.brand-mark-inline {
    display: inline-grid;
    width: 22px; height: 22px;
    font-size: 0.62rem;
    border-radius: 6px;
    vertical-align: text-bottom;
}

.footer-brand { display: grid; gap: 2px; }
.footer-brand a { color: inherit; }

.logo-grid {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: 1fr;
}
@media (min-width: 700px) {
    .logo-grid { grid-template-columns: 1fr 1fr; }
}

.logo-slot { display: grid; gap: var(--space-3); align-content: start; }

/* Each preview sits on the background it will actually be seen against, so a
   white-on-transparent logo is not judged against a white card. */
.logo-preview {
    display: grid;
    place-items: center;
    min-height: 96px;
    padding: var(--space-3);
    border: 1px dashed var(--border);
    border-radius: var(--radius-sm);
}
.logo-preview img { max-height: 64px; max-width: 100%; width: auto; object-fit: contain; }
.logo-preview-light { background: #ffffff; }
.logo-preview-dark  { background: #0f172a; }
.logo-preview-dark .muted { color: #94a3b8; }

/* --- Spacing between blocks that are not cards ----------------------------
   `.card` carries only a bottom margin, so a card that *follows* something
   else — a form, a grid of cards — gets no separation from it and sits flush.
   That is what put the "Running the reminders" panel against the save button
   and the "How importing works" panel against the importer tiles. */

form + .card,
form + .section-title,
.card-grid + .card,
.card-grid + .section-title { margin-top: var(--space-5); }

/* --- Roles and permissions ------------------------------------------------ */

.role-table td { vertical-align: middle; }
.role-name { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.role-table .num { text-align: right; }
.role-table thead .num { text-align: right; }

/* A fixed layout is what makes the columns line up: without it the table sizes
   each column to its own widest cell, and the slug column drifts wider or
   narrower depending on which group you are looking at. */
.permission-table { table-layout: fixed; }
.permission-table td, .permission-table th { vertical-align: middle; overflow-wrap: anywhere; }
.permission-col-slug { width: 15rem; }
.permission-col-name { width: 13rem; }

.permission-group-row th {
    background: var(--bg-sunken);
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}

@media (max-width: 720px) {
    .permission-col-slug { width: 9rem; }
    .permission-col-name { width: 8rem; }
}

/* --- Help (the documentation, rendered in the application) ----------------- */

.help-layout {
    display: grid;
    gap: var(--space-5);
    grid-template-columns: 1fr;
    align-items: start;
}

@media (min-width: 900px) {
    .help-layout { grid-template-columns: 260px minmax(0, 1fr); }
    .help-contents { position: sticky; top: var(--space-4); }
}

.help-contents h2 { font-size: 1rem; margin-bottom: var(--space-2); }
.help-contents ul { list-style: none; margin: 0; padding: 0; }
.help-contents li { margin: 0; }

.help-contents a {
    display: flex;
    align-items: center;
    /* Tap targets stay at 44px even though the list is dense. */
    min-height: 44px;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    color: var(--text);
    text-decoration: none;
}

.help-contents a:hover { background: var(--bg-hover); }

.help-contents li.current a {
    background: var(--accent-soft);
    color: var(--accent-hover);
    font-weight: 600;
}

/* Long-form text, as opposed to the forms and tables everywhere else. */
.prose { max-width: 78ch; }
.prose > :first-child { margin-top: 0; }
.prose > :last-child { margin-bottom: 0; }

.prose h1 { font-size: 1.6rem; margin: var(--space-6) 0 var(--space-3); }
.prose h2 { font-size: 1.3rem; margin: var(--space-6) 0 var(--space-3); }
.prose h3 { font-size: 1.1rem; margin: var(--space-5) 0 var(--space-2); }
.prose h1:first-child, .prose h2:first-child { margin-top: 0; }

.prose p, .prose ul, .prose ol { margin: 0 0 var(--space-4); }
.prose li { margin: 0 0 var(--space-2); }
.prose li > ul, .prose li > ol { margin: var(--space-2) 0 0; }

.prose hr {
    border: 0;
    border-top: 1px solid var(--border);
    margin: var(--space-5) 0;
}

.prose code {
    padding: 0.1em 0.35em;
    background: var(--bg-sunken);
    border-radius: var(--radius-sm);
    font-size: 0.92em;
    word-break: break-word;
}

.prose pre {
    margin: 0 0 var(--space-4);
    padding: var(--space-3);
    background: var(--bg-sunken);
    border-radius: var(--radius-sm);
    overflow-x: auto;
}

.prose pre code { padding: 0; background: none; }

.prose blockquote {
    margin: 0 0 var(--space-4);
    padding: var(--space-2) var(--space-4);
    border-left: 3px solid var(--border-strong);
    color: var(--text-muted);
}

/* Reference tables in the documentation.
 *
 * Ruled on both axes, unlike the register's own lists: these are dense
 * lookup tables — a permission against the roles that hold it, a CSV column
 * against its heading — where the eye has to track along a row and down a
 * column, and a row rule alone is not enough to keep a cell with its heading.
 * `.table-wrap` draws the outer edge, so the cells only draw the inside. */
.prose .table-wrap { margin: 0 0 var(--space-4); }

.prose table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.95rem;
}

.prose th,
.prose td {
    padding: var(--space-2) var(--space-3);
    text-align: left;
    vertical-align: top;
    border: 1px solid var(--border);
}

.prose tr > *:first-child { border-left: 0; }
.prose tr > *:last-child { border-right: 0; }
.prose thead tr:first-child > * { border-top: 0; }
.prose tbody tr:last-child > * { border-bottom: 0; }

.prose thead th {
    background: var(--bg-sunken);
    color: var(--text);
    font-weight: 700;
    border-bottom: 2px solid var(--border-strong);
}

/* In the dark palette `--bg-sunken` sits very close to the card it is drawn on,
   so the header takes the page background instead and separates by the same
   amount the light palette manages. */
[data-theme="dark"] .prose thead th { background: var(--bg); }

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

/* A code span in a cell should not be the thing that forces a column wide. */
.prose td code, .prose th code { white-space: normal; }

/* --- Shared media library -------------------------------------------------- */

/* The library, and the picker, as a **list**.

   These were a grid of 240px cards, which is a fine shape for a dozen files and
   the wrong one for four hundred: a library that has to be scrolled through is
   scanned down a single column of names, not swept across a wall of tiles. So
   one row per file, full width, with the name first and everything else after
   it in a quieter line.

   The whole row is the label, so the target is the row rather than the 16px
   box — which is what the cards got right and is worth keeping. */
.media-grid {
    display: flex;
    flex-direction: column;
    margin: 0 0 var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    overflow: hidden;
}

/* Kept as a hook so callers do not have to change; the list is already full
   width, so there is nothing left for it to widen. */
.media-grid-wide { }

.media-card {
    display: flex;
    gap: var(--space-3);
    align-items: center;
    padding: var(--space-2) var(--space-3);
    background: var(--bg-raised);
    border-top: 1px solid var(--border);
    cursor: pointer;
}

.media-card:first-child { border-top: 0; }
.media-card:hover { background: var(--bg-hover); }
.media-card-static { cursor: default; }

.media-card.is-selected,
.media-card:has(input:checked) {
    background: var(--accent-soft);
    box-shadow: inset 3px 0 0 var(--accent);
}

.media-card:focus-within { outline: 2px solid var(--focus); outline-offset: -2px; }

.media-card input[type="checkbox"] { flex: none; }

/* A row's height is set by this, so it is the one number that decides how many
   files fit on a screen. 32px keeps a photo recognisable while leaving the row
   about as tall as a line of text with its padding. */
.media-thumb {
    flex: none;
    width: 32px;
    height: 32px;
    object-fit: cover;
    border-radius: 3px;
    background: var(--bg-sunken);
}

/* A document has no picture, so it gets a labelled block of the same size —
   the grid stays even whether a row is photos, documents or both. */
.media-thumb-doc {
    display: grid;
    place-items: center;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    color: var(--text-muted);
    border: 1px solid var(--border);
}

/* Name on the first line, everything else on the second. Two lines rather than
   one, because a filename and a title on the same line means one of them gets
   truncated to nothing on a phone. */
.media-meta {
    display: flex;
    flex-direction: column;
    min-width: 0;
    flex: 1 1 auto;
    line-height: 1.3;
}

.media-title { font-weight: 600; overflow-wrap: anywhere; }

/* The quiet line. `nowrap` with an ellipsis on wide screens keeps every row the
   same height, which is what makes a long list scannable; below 700px it wraps
   instead, because a truncated filename on a phone is no use at all. */
.media-sub {
    font-size: 0.82rem;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (max-width: 700px) {
    .media-sub { white-space: normal; overflow-wrap: anywhere; }
}

/* Actions pinned to the right of a row — the delete button on the library. */
.media-actions { flex: none; margin-left: auto; }
.media-actions form { margin: 0; }

.media-empty { margin: 0; padding: var(--space-4); }

.media-picker { margin: 0 0 var(--space-5); }
.media-picker-head { margin-bottom: var(--space-2); }
.media-picker-status { margin: 0; font-size: 0.9rem; }

/* The picker's list is scrolled rather than grown: it sits inside a form with
   a Save button under it, and a picker that pushes the button off the screen is
   a picker people stop using. The height is about eight rows. */
.media-picker .media-grid {
    max-height: 22rem;
    overflow-y: auto;
    overscroll-behavior: contain;
}

.template-picker .field-row { align-items: end; }

/* --- Maintenance routines -------------------------------------------------- */

/* A badge that is also a link. The badge rules already fix the shape; this
   only stops the anchor colour and underline fighting them. */
.badge-link { color: inherit; text-decoration: none; }
.badge-link:hover { text-decoration: underline; }

/* Picking a routine to run. Rows rather than a table: this is met on a phone
   at arm's length, and the whole row has to be the target. */
.pick-list { list-style: none; margin: 0; padding: 0; }

.pick-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    border-bottom: 1px solid var(--border);
}
.pick-item:last-child { border-bottom: 0; }

.pick-link {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1;
    min-width: 0;
    padding: var(--space-3) 0;
    color: inherit;
    text-decoration: none;
}
.pick-link:hover .pick-title { text-decoration: underline; }
.pick-title { font-weight: 600; }
.pick-sub { font-size: 0.9rem; }

/* --- One step, as it is filled in ------------------------------------------ */

.routine-step {
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
}
.routine-step:last-child { border-bottom: 0; }
.routine-step.has-error { background: var(--danger-soft); border-radius: var(--radius-sm); }

.routine-step-label { margin-bottom: 2px; }
.routine-step-help { margin-top: 0; margin-bottom: var(--space-2); }
.req { color: var(--danger); font-weight: 700; }

/* Choices as pills. Big enough for a gloved hand, and the whole pill is the
   label, so the tap target is the visible shape rather than the radio inside
   it. */
.routine-choices { display: flex; flex-wrap: wrap; gap: var(--space-2); }

.choice-pill {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 44px;
    padding: 0 var(--space-3);
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg-raised, transparent);
    cursor: pointer;
}
.choice-pill:has(input:checked) {
    border-color: var(--accent);
    background: var(--accent-soft);
    font-weight: 600;
}
.choice-pill:has(input:disabled) { opacity: 0.6; cursor: default; }

/* --- The editor ------------------------------------------------------------ */

.card-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }

/* A field whose value is shown rather than edited — the asset status while the
   item is out on hire, where the hire decides it and a button changes it. */
.static-value { margin: 0; }

/* A multi-select tall enough to work with. The browser default is four rows,
   which makes choosing twelve assets a scrolling exercise. */
/* The bar above a table whose rows can be selected for one action. */
.bulk-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) 0 var(--space-3);
}
.bulk-bar .field-hint { margin: 0; flex: 1 1 220px; }

.col-check { width: 34px; }
.checkbox-bare { min-height: 0; padding: 0; border: 0; background: none; }

.input-multi { min-height: 260px; padding: var(--space-1); }
.input-multi option { padding: 2px var(--space-1); }

.form-actions-inline {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-top: var(--space-3);
    align-items: center;
}

/* A button sitting on the same line as the input beside it, with the label
   above accounted for. */
.field-aligned { display: flex; align-items: flex-end; }

.routine-step-editor { list-style: none; margin: var(--space-3) 0 0; padding: 0; }

.routine-step-row {
    display: flex;
    gap: var(--space-3);
    padding: var(--space-3);
    margin-bottom: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-sunken);
}

.routine-step-main { flex: 1; min-width: 0; }

.routine-step-tools {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    flex-shrink: 0;
}

@media (max-width: 620px) {
    .routine-step-row { flex-direction: column; }
    .routine-step-tools { flex-direction: row; }
}

/* --- What a routine asks, summarised --------------------------------------- */

.routine-summary-page + .routine-summary-page { margin-top: var(--space-4); }
.routine-summary-steps { list-style: none; margin: var(--space-2) 0 0; padding: 0; }

.routine-summary-steps li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) 0;
    border-bottom: 1px solid var(--border);
}
.routine-summary-steps li:last-child { border-bottom: 0; }
.routine-summary-label { flex: 1; min-width: 12rem; }

/* --- A completed routine, read back ---------------------------------------- */

.answer-list { margin: 0; }

.answer-row {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-1);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
}
.answer-row:last-child { border-bottom: 0; }

/* Two columns once there is room, so the answers line up down the page and can
   be scanned without reading every question again. */
@media (min-width: 700px) {
    .answer-row { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); gap: var(--space-4); }
}

.answer-question { font-weight: 600; margin: 0; }
.answer-help { display: block; font-weight: 400; font-size: 0.88rem; margin-top: 2px; }
.answer-value { margin: 0; min-width: 0; }
.answer-choices { margin: 0; padding-left: 1.1rem; }

/* A file control that is only there to be looked at. */
.btn-file.is-disabled { opacity: 0.6; cursor: default; }

/* --- Checkbox help text ---------------------------------------------------- */

/* A `.field-hint` inside a checkbox label is a span, and a span is inline —
   so its top margin does nothing and the sentence runs on after the label,
   squeezed into whatever width is left. Both rules are needed: the block makes
   it a line of its own, and the flexed wrapper gives it the row's full width
   to use. Same stacking as the label/value lists elsewhere. */
.checkbox > span { flex: 1 1 auto; min-width: 0; }
.checkbox .field-hint { display: block; margin-top: var(--space-1); }

/* --- An open routine run --------------------------------------------------- */

.run-step-list { list-style: none; margin: 0; padding: 0; }

.run-step {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
}
.run-step:last-child { border-bottom: 0; }

/* A tick that is present or absent, in a column that is always there — so the
   labels line up whether or not a step has been answered. */
.run-step-mark {
    flex: 0 0 1.5rem;
    width: 1.5rem;
    text-align: center;
    font-weight: 700;
    color: var(--ok);
    line-height: 1.6;
}

.run-step-body { flex: 1 1 auto; min-width: 0; }

.run-step-label {
    display: block;
    font-weight: 600;
    color: inherit;
    text-decoration: none;
}
.run-step-label:hover { text-decoration: underline; }

.run-step-meta { display: block; font-size: 0.88rem; overflow-wrap: anywhere; }

.run-step-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    flex-shrink: 0;
}

.run-step.is-done .run-step-label { font-weight: 500; }

@media (max-width: 620px) {
    .run-step { flex-wrap: wrap; }
    .run-step-actions { width: 100%; padding-left: calc(1.5rem + var(--space-3)); }
}

.plain-list { list-style: none; margin: 0; padding: 0; }
.plain-list li + li { margin-top: var(--space-2); }

/* --- LOLER thorough examination -------------------------------------------- */

/* A field with its own confirmation beside it. The tick sits to the right and
   stays on the same line as the top of the field, so a column of them reads as
   a checklist rather than as five separate controls. */
.confirm-row {
    display: flex;
    align-items: flex-start;
    gap: var(--space-4);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
}
.confirm-row:last-of-type { border-bottom: 0; }
.confirm-row > .field { flex: 1 1 auto; min-width: 0; margin: 0; }
.confirm-row.has-error { background: var(--danger-soft); border-radius: var(--radius-sm); }

.confirm-tick {
    flex: 0 0 auto;
    align-self: flex-start;
    margin-top: 1.6rem;
    white-space: nowrap;
}

.confirm-all-wrap {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}
.confirm-all-wrap .field-hint { margin: 0; }

@media (max-width: 620px) {
    .confirm-row { flex-direction: column; gap: var(--space-2); }
    .confirm-tick { margin-top: 0; }
}

/* One recorded defect while it is being entered. */
.defect-row {
    margin: 0 0 var(--space-4);
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-sunken);
}
.defect-row legend { padding: 0 var(--space-2); font-weight: 600; }
.defect-row.has-error { border-color: var(--danger); }

/* The consequence of a tick, said at the moment it is ticked. */
.defect-notice {
    margin: var(--space-3) 0;
    padding: var(--space-3);
    border-left: 4px solid var(--danger);
    border-radius: var(--radius-sm);
    background: var(--danger-soft);
    font-size: 0.92rem;
}

/* A recorded defect on the finished report. */
.defect-list { list-style: none; margin: 0; padding: 0; }

.defect-item {
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
}
.defect-item:last-child { border-bottom: 0; }

.defect-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-2);
}

/* --- The routine editor: collapsible cards, and failing a run -------------- */

/* `<details>` does the collapsing itself, so a routine of forty questions is
   navigable with JavaScript switched off exactly as it is with it. Everything
   here is presentation on top of that. */

.routine-collapse { min-width: 0; }

.routine-collapse-head {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: var(--space-2) 0 var(--space-2) 18px;
    cursor: pointer;
    list-style: none;
}

.routine-collapse-head::-webkit-details-marker { display: none; }

/* The twisty, drawn rather than borrowed from the user agent, so it sits on
   the same line as the title in every browser. */
.routine-collapse-head::before {
    content: "\25B8";
    position: absolute;
    left: 0;
    top: var(--space-2);
    color: var(--text-muted);
    transition: transform 0.12s ease;
}

.routine-collapse[open] > .routine-collapse-head::before { transform: rotate(90deg); }

.routine-collapse-title { font-weight: 700; }

.routine-collapse-sub {
    font-size: 0.86rem;
    overflow-wrap: anywhere;
}

.routine-collapse-body { padding: var(--space-2) 0 0 18px; }

.routine-page-card > .routine-collapse > .routine-collapse-head .routine-collapse-title {
    font-size: 1.05rem;
}

.routine-page-actions { justify-content: flex-end; margin-bottom: var(--space-2); }

/* The Failure Response page, wherever it appears: the editor, the preview and
   a failed record. It is not part of the sequence, and should not read as one
   more page of it. */
.routine-failure-card { border-left: 4px solid var(--danger); }

.editor-status { margin: 0 0 var(--space-2); min-height: 1.2em; font-size: 0.88rem; }
.editor-status.is-error { color: var(--danger); font-weight: 600; }

[data-routine-pages][aria-busy="true"] { opacity: 0.6; }

.field-block {
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px dashed var(--border);
}

.form-aside { margin-top: var(--space-3); }

/* --- Failing a routine while it is being carried out ---------------------- */

.wizard-fail-action { margin-top: var(--space-4); text-align: right; }

.wizard-fail-panel { border-left: 4px solid var(--danger); }

/* A question the conditions ruled out. Shown rather than removed, because
   somebody looking for it needs to be told it was not asked. */
.run-step.is-skipped { opacity: 0.62; }
.run-step.is-skipped .run-step-label { font-style: italic; }

/* Client contacts: a one-line "pick one and add it" row, and a boxed list of
   what a change will do. */
.form-inline-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    align-items: flex-end;
    margin-top: var(--space-4);
}
.form-inline-row .field { flex: 1 1 16rem; margin: 0; }
.callout {
    border: 1px solid var(--border);
    border-left: 4px solid var(--accent);
    border-radius: var(--radius);
    padding: var(--space-3) var(--space-4);
    margin: var(--space-4) 0;
}
.callout ul { margin-top: var(--space-2); }
.callout-warning { border-left-color: var(--warn); background: var(--warn-soft); }
.callout[hidden] { display: none; }

.job-list .job-row { padding: var(--space-2) 0; border-bottom: 1px solid var(--border); }
.job-list .job-row:last-child { border-bottom: 0; }
.job-list .job-row.is-overdue { border-left: 3px solid var(--danger); padding-left: var(--space-3); }

/* Reopened LOLER reports and PAT tests: the reason each was reopened, newest first. */
.reopen-history { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-3); }
.reopen-history li { border-left: 3px solid var(--border); padding-left: var(--space-3); }
.reopen-history p { margin: 0 0 var(--space-1); }

/* Photographs already on a report being corrected: thumbnail, description, and a Remove tick. */
.reopen-photos { list-style: none; margin: 0 0 var(--space-3); padding: 0; display: grid; gap: var(--space-3); }
.reopen-photos li { display: flex; gap: var(--space-3); align-items: center; }
.reopen-photos img { display: block; border-radius: var(--radius, 4px); border: 1px solid var(--border); }
.reopen-photo-fields { flex: 1; display: grid; gap: var(--space-1); }
