/* ---------------------------------------------------------------------------
   ISSI Tool App — "Crib Board"

   The design premise: this screen lives on a monitor by the tool crib and gets
   touched with work gloves on. So the job number is the largest thing on a
   row, how long a request has been waiting is a NUMBER rather than a badge,
   controls are square and big enough to hit, and type is the condensed face
   the company website already uses.

   Token NAMES are unchanged from the previous design. Views carry ~80 inline
   var(--text-secondary) style references; renaming would have meant touching
   every one of them to no benefit. The values moved, the names did not.
   ------------------------------------------------------------------------- */
:root {
    /* Steel — the company blue deepened until white text clears 7:1 on it,
       because it now fills large solid controls rather than tinting small
       ones. Same hue family as the header's --site-accent. */
    --brand: #2f5c8a;
    --brand-dark: #22456a;
    --brand-light: #e7eef5;

    --cream: #f7f3e8;
    --cream-border: #e6dcc2;

    /* Warm concrete, not blue-grey. A shop floor is a warm room and the
       ground reads as paper stock rather than as a web page. */
    --bg: #edece8;
    --surface: #ffffff;
    --surface-sunken: #faf9f7;
    --border: #d8d7d1;
    --border-soft: #ebe9e5;
    --text: #191b1a;
    --text-secondary: #4c5559;
    --text-muted: #79837f;

    /* Status. Safety amber for waiting — the colour the shop already uses to
       mean "attention", and distinguishable from green for the ~8% of men
       with red/green colour deficiency because it differs in lightness too. */
    --amber-bg: #fbeeda;
    --amber-text: #a06305;
    --amber-dot: #e0a02a;
    --green-bg: #ddeee4;
    --green-text: #1f6b4a;
    --green-dot: #3f9c72;
    --gray-bg: #e8e9e6;
    --gray-text: #6d7671;
    --gray-dot: #b9bebb;
    --purple-bg: #ece5f2;
    --purple-text: #5d4179;
    --red-bg: #fadedb;
    --red-text: #a4291f;

    /* Type. Barlow throughout — the website's body face, so the app and
       app.issi.us/newsite are set in one voice. Condensed carries anything
       that behaves like a label or a readout: headings, buttons, tabs,
       figures. IBM Plex Mono stays for part numbers and bin codes, where
       character disambiguation (0/O, 1/l) actually matters. */
    --font-body: "Barlow", system-ui, -apple-system, "Segoe UI", sans-serif;
    --font-condensed: "Barlow Condensed", "Arial Narrow", system-ui, sans-serif;
    --font-mono: "IBM Plex Mono", ui-monospace, "Cascadia Mono", monospace;

    /* One radius, and it is nearly square. The previous design put 12px on
       everything, which is what made it read as 2019 SaaS. */
    --radius: 3px;
    --radius-sm: 2px;

    /* How long a request may sit before the board flags it. Four hours is
       half a shift: long enough that a normal fill (minutes) never trips it,
       short enough that nothing quietly rots overnight. */
    --shadow-sm: 0 1px 2px rgba(25, 27, 26, .08);

    /* Header only. Lifted from the company website's stylesheet
       (/newsite/assets/css/site.css) so the two headers are the same object,
       not an approximation of one. Kept under --site-* rather than folded into
       the tokens above: the header must not drift when the app's palette
       moves, and this palette has now moved once. */
    --site-bg: #f2f2f3;
    --site-text: #1d1f20;
    --site-accent: #5980a6;
    --site-accent-700: #416180;
    --site-divider: color-mix(in srgb, #1d1f20 16%, transparent);
    --site-shadow-sm: 0 1px 2px color-mix(in srgb, #2b2b2d 14%, transparent);
    --site-font-heading: "Barlow Condensed", "Arial Narrow", system-ui, sans-serif;
    --site-font-body: "Barlow", system-ui, -apple-system, "Segoe UI", sans-serif;
}

* { box-sizing: border-box; }
[x-cloak] { display: none !important; }
.htmx-indicator { opacity: 0; transition: opacity 150ms ease-in; }
.htmx-request.htmx-indicator, .htmx-request .htmx-indicator { opacity: 1; }
.htmx-indicator { animation: none; }
.htmx-request .htmx-indicator { animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important;
        transition-duration: .01ms !important;
    }
}

body {
    margin: 0;
    font-family: var(--font-body);
    background: var(--bg);
    color: var(--text);
    font-size: 15px;
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
}

.mono { font-family: var(--font-mono); }

a { color: var(--brand); text-decoration: none; }
a:hover { color: var(--brand-dark); text-decoration: underline; }

h1, h2, h3, p { margin: 0; }

input, select, textarea, button { font-family: inherit; font-size: inherit; }

/* Layout */

/* Header — the company website's, at /newsite. Same accent bar, same brand
   lockup, same Barlow Condensed name over a letterspaced strapline, so the
   two do not look like two different companies to somebody who follows a link
   between them. Deliberately NOT restyled with the rest of the app: matching
   the site is the point, and the site did not change. */
.accent-bar { height: 3px; background: var(--site-accent); }

.topbar {
    display: flex;
    align-items: center;
    gap: 14px;
    /* 40px is the site's nav padding. The max() keeps the brand and the
       links lined up with .page (max-width 1120, 24px gutters) once the
       window is wider than the content - otherwise the header drifts out
       to the window edges while the page below stays centred. The site
       avoids this by capping its whole shell at 1200px. */
    padding: 12px max(40px, calc((100% - 1120px) / 2 + 24px));
    background: var(--site-bg);
    border-bottom: 1px solid var(--site-divider);
    box-shadow: var(--site-shadow-sm);
    font-family: var(--site-font-body);
    flex-wrap: wrap;
    row-gap: 14px;
    /* Above the page, so the mobile panel and any shadow sit over content
       rather than under it. */
    position: relative;
    z-index: 20;
}
.topbar-brand {
    display: flex; align-items: center; gap: 14px; min-width: 0;
    text-decoration: none; color: inherit; flex: none; margin-right: auto;
}
.topbar-brand:hover { color: inherit; text-decoration: none; }
.topbar-brand img { height: 46px; width: auto; display: block; }
.topbar-brand-text { display: flex; flex-direction: column; line-height: 1; min-width: 0; }
.topbar-company {
    font-family: var(--site-font-heading); font-weight: 700; font-size: 21px;
    letter-spacing: 0.06em; text-transform: uppercase; color: var(--site-text);
}
.topbar-tagline {
    font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
    margin-top: 4px; font-style: normal;
    color: color-mix(in srgb, var(--site-text) 68%, transparent);
}
.topbar-nav { display: flex; align-items: center; gap: 26px; margin-left: auto; }
/* No font-weight: the site's nav links are body weight, and bolding them
   here is the difference between "the same header" and "nearly". */
.topbar-links { display: flex; align-items: center; gap: 26px; font-size: 15px; }
.topbar-links a { color: var(--site-text); letter-spacing: 0.02em; }
.topbar-links a.active { color: var(--site-accent); }
.topbar-links a:hover { text-decoration: none; color: var(--site-accent); }
.topbar-divider { width: 1px; height: 22px; background: var(--site-divider); }
.topbar-user { display: flex; align-items: center; gap: 9px; font-size: 14px; font-weight: 500; color: var(--site-text); }
/* It is a link now (to the account page) and must not read as one until
   hovered - a permanently underlined name in the header is noise. */
a.topbar-user { text-decoration: none; }
a.topbar-user:hover span { text-decoration: underline; }
/* Scoped: .avatar is also used in the admin user and notification tables,
   which stay on the app's palette. */
.topbar .avatar { background: var(--site-accent); color: #fff; border-radius: 999px; }
.topbar .link-button {
    font-family: var(--site-font-body); font-size: 14px; font-weight: 500;
    color: color-mix(in srgb, var(--site-text) 68%, transparent);
    text-transform: none; letter-spacing: 0;
}
.topbar .link-button:hover { color: var(--site-accent); }
.avatar {
    width: 28px; height: 28px; border-radius: var(--radius-sm);
    background: var(--brand); color: #fff;
    display: flex; align-items: center; justify-content: center;
    font-size: 10.5px; font-weight: 700; letter-spacing: .04em; flex-shrink: 0;
}

/* Hamburger toggle for the mobile nav panel. Hidden on desktop; the site's
   square 44px control rather than the app's rounded one. */
.topbar-toggle {
    display: none;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    flex: none;
    padding: 0;
    cursor: pointer;
    background: transparent;
    border: 1px solid var(--site-divider);
    border-radius: 2px;
    color: var(--site-text);
}
.topbar-toggle:hover { border-color: var(--site-accent); color: var(--site-accent); }

.page { max-width: 1120px; margin: 0 auto; padding: 30px 24px 64px; display: flex; flex-direction: column; gap: 20px; }
.page-narrow { max-width: 800px; }
.page-header { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; flex-wrap: wrap; }
.page-title {
    font-family: var(--font-condensed);
    font-size: 34px;
    font-weight: 700;
    line-height: 1;
    letter-spacing: 0.025em;
    text-transform: uppercase;
    color: var(--text);
}
.page-subtitle { font-size: 14.5px; color: var(--text-secondary); margin-top: 8px; max-width: 68ch; }

/* Summary counts — the strip above the board. Condensed figures, because the
   whole point is that they are read at a glance from a few feet away. */
.counts { display: flex; gap: 30px; flex-wrap: wrap; margin-top: 14px; }
.count { display: flex; flex-direction: column; gap: 3px; }
.count b {
    font-family: var(--font-condensed); font-size: 30px; font-weight: 700;
    line-height: 1; font-variant-numeric: tabular-nums; color: var(--text);
}
.count span {
    font-size: 10.5px; font-weight: 600; letter-spacing: 0.13em;
    text-transform: uppercase; color: var(--text-muted);
}
.count-warn b { color: var(--amber-text); }

/* Card */
.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 26px;
}
.card-divider { height: 1px; background: var(--border); margin: 4px 0; }

/* Form */
.field { display: flex; flex-direction: column; gap: 6px; }
.field-label {
    font-size: 11px; font-weight: 700; color: var(--text-secondary);
    text-transform: uppercase; letter-spacing: 0.1em;
}
.field-row { display: flex; justify-content: space-between; align-items: baseline; }
.input {
    height: 44px; border: 1px solid var(--border); border-radius: var(--radius);
    padding: 0 12px; font-size: 15px; color: var(--text); background: var(--surface); width: 100%;
}
.input:focus, button:focus-visible, a:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.input.input-active { border: 1.5px solid var(--brand); }
.input-sm { height: 38px; font-size: 14px; }

/* Buttons — square, condensed, uppercase. Sized to be hit with a glove on:
   the default is 48px tall, and even the small variant clears 38px. */
.btn {
    height: 48px; padding: 0 24px; border-radius: var(--radius);
    font-family: var(--font-condensed); font-size: 19px; font-weight: 700;
    letter-spacing: 0.07em; text-transform: uppercase;
    cursor: pointer; border: 2px solid transparent; display: inline-flex;
    align-items: center; gap: 9px; justify-content: center; white-space: nowrap;
}
.btn-primary { background: var(--brand); color: #fff; }
.btn-primary:hover { background: var(--brand-dark); color: #fff; text-decoration: none; }
.btn-secondary { background: transparent; color: var(--text-secondary); border-color: var(--border); }
.btn-secondary:hover { text-decoration: none; background: var(--surface); border-color: var(--text-muted); color: var(--text); }
.btn-block { width: 100%; }
.btn-sm { height: 38px; padding: 0 16px; font-size: 16px; }
.btn-icon {
    width: 38px; height: 38px; padding: 0; border: none; background: transparent;
    color: var(--text-muted); border-radius: var(--radius);
}
.btn-icon:hover { color: var(--red-text); background: var(--red-bg); }

.error-text { font-size: 13.5px; color: var(--red-text); }
.help-text { font-size: 12.5px; color: var(--text-muted); }
.link-button {
    background: none; border: none; padding: 0; cursor: pointer; color: var(--brand);
    font-weight: 600; font-size: 13.5px;
}
.link-button:hover { color: var(--brand-dark); text-decoration: underline; }

/* Status chips — square, uppercase, letterspaced. A pill reads as decoration;
   this reads as a stamped label, which is what a status is. */
.badge {
    display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px;
    border-radius: var(--radius-sm); font-size: 11.5px; font-weight: 700;
    letter-spacing: 0.1em; text-transform: uppercase; width: fit-content;
    white-space: nowrap;
}
.badge-dot { width: 8px; height: 8px; border-radius: 1px; flex-shrink: 0; }
.badge-amber { background: var(--amber-bg); color: var(--amber-text); }
.badge-amber .badge-dot { background: var(--amber-dot); }
.badge-green { background: var(--green-bg); color: var(--green-text); }
.badge-green .badge-dot { background: var(--green-dot); }
.badge-gray { background: var(--gray-bg); color: var(--gray-text); }
.badge-gray .badge-dot { background: var(--gray-dot); }
.badge-blue { background: var(--brand-light); color: var(--brand-dark); }
.badge-purple { background: var(--purple-bg); color: var(--purple-text); }
.badge-red { background: var(--red-bg); color: var(--red-text); }
.badge-red .badge-dot { background: var(--red-text); }

/* Tables */
.table-wrap { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.table-row {
    display: grid; align-items: center; padding: 14px 20px; border-bottom: 1px solid var(--border-soft);
    gap: 8px;
}
.table-row:last-child { border-bottom: none; }
.table-row-head { background: var(--gray-bg); border-bottom-color: var(--border); }
.table-head-cell { font-size: 10.5px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.11em; }
.table-row.is-expandable { cursor: pointer; }
.table-row.is-expandable:hover { background: var(--brand-light); }

.cols-requests { grid-template-columns: 130px 145px 1fr 1fr 105px 115px 24px; }
.cols-users { grid-template-columns: 1.3fr 1.6fr 110px 110px 150px 140px; }
.cols-status { grid-template-columns: 1fr auto; }
.cols-notify { grid-template-columns: 1.2fr 1.5fr 150px 60px 60px 60px 60px 88px; }
.cols-backups { grid-template-columns: 190px 110px 120px 100px 100px 1fr; }
.cols-offsite { grid-template-columns: 1fr 190px 100px 90px; }
.cols-parts { grid-template-columns: 170px 1fr 200px 80px 130px; }
.cols-syncstats { grid-template-columns: 1.4fr 70px 70px 100px 90px 90px 160px; }
.cols-synclog { grid-template-columns: 170px 1.2fr 90px 90px 80px 110px; }
.cols-rfq { grid-template-columns: 1.4fr 1.6fr 110px 1.2fr 150px; }
.cols-rfq-customers { grid-template-columns: 1.6fr 1.6fr 100px; }
.cols-deploy { grid-template-columns: 190px 110px 120px 1fr 100px 140px; }
.cols-security { grid-template-columns: 150px 1.1fr 90px 130px 1.6fr 160px; }
/* Three cells. It previously borrowed .cols-notify, which declares SIX -
   the values crammed into the left half and the date clipped off the right. */
.cols-admin-logins { grid-template-columns: 1.2fr 1.8fr 200px; }
.cols-leaderboard { grid-template-columns: 56px 1.5fr 70px 110px 110px 110px 110px; }
.cols-leaderboard-provisional { grid-template-columns: 1.5fr 70px 110px 110px 110px 110px; }
.cols-remote-commits { grid-template-columns: 90px 1fr 160px 160px; }

/* ---------------------------------------------------------------------------
   THE REQUEST BOARD

   A request stopped being a table row and became a card, because a row gives
   a job number the same weight as a timestamp and this screen is scanned for
   job numbers. Each card carries a status stripe down its left edge, so the
   state of the board is legible from across the room before any text is.
   ------------------------------------------------------------------------- */
.req-stack { display: flex; flex-direction: column; gap: 10px; }

.req-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 6px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
}
.req-card-requested { border-left-color: var(--amber-dot); }
.req-card-filled { border-left-color: var(--green-dot); }
.req-card-cancelled { border-left-color: var(--gray-dot); background: var(--surface-sunken); }
/* Waiting longer than the flag threshold: the stripe is not enough on its
   own once several cards are amber, so the whole card gets an outline. */
.req-card-aged { box-shadow: inset 0 0 0 1px var(--amber-dot); }

/* Four columns, and the middle two are fixed on purpose: the age readouts and
   the status chips have to line up down the stack, or the board cannot be
   scanned by column the way a table could. */
.req-head {
    display: grid;
    grid-template-columns: 1fr 100px 132px minmax(0, auto);
    align-items: center;
    gap: 18px;
    padding: 15px 20px;
    cursor: pointer;
}
.req-head:hover { background: var(--surface-sunken); }
.req-card-cancelled .req-head:hover { background: #f4f2ef; }

.req-job {
    font-family: var(--font-condensed);
    font-size: 27px; font-weight: 700; line-height: 1;
    letter-spacing: 0.04em; font-variant-numeric: tabular-nums;
    color: var(--text); overflow-wrap: anywhere;
}
.req-card-cancelled .req-job { color: var(--text-muted); }
.req-sub { font-size: 13.5px; color: var(--text-secondary); margin-top: 7px; }
.req-sub b { font-weight: 600; color: var(--text); }

/* Age as a readout, not a badge. Tabular figures so a column of them lines
   up down the stack. */
.req-age { text-align: right; font-variant-numeric: tabular-nums; }
.req-age b { font-family: var(--font-condensed); font-size: 24px; font-weight: 700; line-height: 1; display: block; }
.req-age span {
    display: block; font-size: 10px; font-weight: 600; letter-spacing: 0.12em;
    text-transform: uppercase; color: var(--text-muted); margin-top: 5px;
}
.req-age-open b { color: var(--text-secondary); }
.req-age-warn b { color: var(--amber-text); }
.req-age-none b { color: var(--text-muted); }

.req-status { justify-self: start; }
/* Fill and Cancel sit ON the row. Acting on a request used to require
   expanding it first, which is one tap too many on a crib screen. */
.req-acts { display: flex; gap: 8px; justify-content: flex-end; }

.req-detail { border-top: 1px solid var(--border-soft); background: var(--surface-sunken); padding: 2px 20px 16px; }
.req-lines { width: 100%; border-collapse: collapse; }
.req-lines th {
    text-align: left; font-size: 10px; font-weight: 700; letter-spacing: 0.12em;
    text-transform: uppercase; color: var(--text-muted);
    padding: 13px 10px 8px; border-bottom: 1px solid var(--border);
}
.req-lines td { padding: 11px 10px; border-bottom: 1px solid var(--border-soft); font-size: 14.5px; vertical-align: top; }
.req-lines tr:last-child td { border-bottom: none; }
.req-lines .req-col-qty { text-align: right; }
.req-part { font-family: var(--font-mono); font-size: 13px; font-weight: 600; color: var(--brand-dark); }
/* Bin and on-hand count, for the person walking the crib. Mono so a bin code
   reads unambiguously - 0/O and 1/l are the whole reason the mono face is
   loaded - and quieter than the part number, which is still the line's
   identity. */
.req-bin { font-size: 12.5px; color: var(--text-muted); white-space: nowrap; }

/* Partial fulfilment. A resolved line is quieter than a pending one, so what
   is left to do reads first - the picker is scanning for the rows that still
   need them, not admiring the ones that do not. */
.req-line-filled { opacity: .55; }
.req-line-unavailable td { background: var(--red-bg); }
.req-note-bad { color: var(--red-text); }
.req-line-acts { display: flex; justify-content: flex-end; }

/* Audit meta: key names quiet, values normal, so a row reads as prose
   rather than as JSON with the braces taken out. */
.audit-key { color: var(--text-muted); font-size: 12px; text-transform: uppercase; letter-spacing: .03em; }
.audit-sep { color: var(--border); margin: 0 6px; }

/* Insights bars. No charting library - there is no build step - so a bar is a
   div whose width IS the number. Nothing to load, nothing to fall back to. */
.insight-bar { height: 8px; border-radius: 4px; background: var(--surface); overflow: hidden; margin-top: 5px; }
.insight-bar-fill { height: 100%; border-radius: 4px; background: var(--brand); }
.insight-hours { display: flex; align-items: flex-end; gap: 3px; height: 140px; }
.insight-hour { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; height: 100%; }
/* min-height so an hour with no requests is still a visible baseline rather
   than a gap the eye reads as missing data. */
.insight-hour-bar { background: var(--brand); border-radius: 3px 3px 0 0; min-height: 2px; }
.insight-hour-label { font-size: 10px; color: var(--text-muted); text-align: center; height: 14px; margin-top: 4px; }
.req-qty {
    font-family: var(--font-condensed); font-size: 20px; font-weight: 700;
    font-variant-numeric: tabular-nums; white-space: nowrap;
}
.req-note { display: block; margin-top: 4px; font-size: 13px; font-weight: 600; color: var(--amber-text); }
.req-empty { padding: 34px 20px; text-align: center; color: var(--text-muted); font-size: 14px; }

/* Deploy / backup panels */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
.stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px 18px; }
.stat-label { font-size: 10.5px; font-weight: 700; letter-spacing: 0.12em; color: var(--text-muted); text-transform: uppercase; }
.stat-value { font-size: 16px; font-weight: 600; color: var(--text); margin-top: 7px; }
.stat-note { font-size: 12.5px; color: var(--text-secondary); margin-top: 3px; }
.console {
    background: var(--gray-bg); border: 1px solid var(--border); border-radius: var(--radius);
    padding: 14px 16px; font-family: var(--font-mono);
    font-size: 12.5px; line-height: 1.6; color: var(--text); overflow-x: auto;
    white-space: pre-wrap; word-break: break-word; max-height: 420px;
}
/* ---------------------------------------------------------------------------
   NOTIFICATION LOG

   A log, not a table. The value is in reading down a fixed-width column of
   timestamps and spotting the gap where three texts should have been — which
   is exactly what a table's wrapping cells destroy the moment a provider
   error runs long. Lines scroll sideways rather than wrap for the same
   reason: one notification is one line.
   ------------------------------------------------------------------------- */
.log-line {
    white-space: pre;
    line-height: 1.75;
}
.log-sub {
    /* Indented under its line, so the eye still reads the timestamps as one
       column even where a failure has pushed detail underneath. */
    white-space: pre-wrap;
    padding: 0 0 6px 22px;
    color: var(--text-secondary);
    line-height: 1.6;
}
.log-when { color: var(--text-secondary); }
.log-chan { color: var(--text-muted); font-weight: 600; }
.log-to { color: var(--text); }
.log-subject { color: var(--text-secondary); }
.log-job { color: var(--brand-dark); font-weight: 600; }
.log-ok { color: var(--green-text); font-weight: 600; }
.log-warn { color: var(--amber-text); font-weight: 600; }
.log-bad { color: var(--red-text); font-weight: 600; }

.diag-row { display: flex; gap: 10px; align-items: flex-start; font-size: 13.5px; margin-bottom: 10px; }
.diag-row:last-child { margin-bottom: 0; }
.notify-checkbox { width: 18px; height: 18px; accent-color: var(--brand); cursor: pointer; }
.notify-checkbox-cell { display: flex; justify-content: center; }

.status-row-name { font-size: 13.5px; font-weight: 600; color: var(--text); }
.status-row-message { font-size: 13px; color: var(--text-secondary); margin-top: 2px; }
.status-updated { font-size: 12.5px; color: var(--text-muted); }

.expand-panel { padding: 4px 20px 20px; background: var(--surface-sunken); border-bottom: 1px solid var(--border-soft); }
.expand-inner { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.cols-lines { grid-template-columns: 140px 1fr 80px; display: grid; padding: 11px 16px; border-top: 1px solid var(--border-soft); align-items: center; }
.cols-lines:first-child { border-top: none; background: var(--gray-bg); }

/* Tabs — an underlined rail rather than a segmented pill. The pill was the
   single most dated control in the old design. */
.tabs { display: flex; gap: 0; border-bottom: 2px solid var(--border); width: 100%; }
.tab {
    padding: 9px 20px 11px; margin-bottom: -2px; background: none; border: none;
    border-bottom: 3px solid transparent; cursor: pointer;
    font-family: var(--font-condensed); font-size: 19px; font-weight: 600;
    letter-spacing: 0.07em; text-transform: uppercase; color: var(--text-muted);
}
.tab:hover { color: var(--text-secondary); text-decoration: none; }
.tab.active { color: var(--text); border-bottom-color: var(--brand); }
.tab.active:hover { color: var(--text); }

/* Autocomplete */
.parts-line { display: flex; flex-direction: column; gap: 10px; padding: 16px; border: 1px solid var(--border); border-radius: var(--radius); }
.parts-line.is-selected { background: var(--brand-light); border-color: #c6d7e6; }
.parts-line-row { display: flex; gap: 12px; align-items: flex-start; }
/* A flex item defaults to min-width:auto, which means it refuses to shrink
   below its content's intrinsic width. The autocomplete list underneath is as
   wide as its longest description, so without this the column grows to fit it
   and drags the whole page into a horizontal scroll. */
.parts-line-row > * { min-width: 0; }
.parts-search-wrap { position: relative; }
.parts-search-icon { position: absolute; left: 11px; top: 50%; transform: translateY(-50%); pointer-events: none; }
/* A part stocked in several bins gets one row per bin, so this list is
   taller than it used to be. Scroll it rather than letting it run off the
   bottom of the form. */
/* Tall enough to scroll through a broad match like "fitting" without the
   list feeling truncated, capped against the viewport so it cannot run off
   the bottom of a short screen. */
/* width:100% + box-sizing pins the list to its column instead of letting it
   size to its widest row. overflow-x:hidden is the backstop, not the fix —
   the rows wrap, so there should be nothing to clip. */
.autocomplete-list { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 8px 24px rgba(25, 27, 26, .16); overflow-y: auto; overflow-x: hidden; max-height: min(60vh, 520px); margin-top: 6px; width: 100%; box-sizing: border-box; }
/* Inline flow, not flex: `part_no : descrip : location : qty` reads as one
   sentence and wraps like one, which is how the old app behaved and the only
   way a 130-character description can be shown in full rather than truncated.
   A flex row cannot wrap mid-item, so it could only ever ellipsis the
   description or force the column wider than the page.
   The padding/text-indent pair is a hanging indent: the first line starts at
   10px and continuation lines at 28px, so a wrapped row is obviously a
   continuation and not a new result. */
.autocomplete-item { display: block; padding: 6px 10px 6px 28px; text-indent: -18px; border-radius: var(--radius-sm); cursor: pointer; line-height: 1.5; white-space: normal; overflow-wrap: break-word; }
.autocomplete-item:hover { background: var(--brand-light); }
.autocomplete-part-no { font-size: 13px; font-weight: 600; color: var(--text-secondary); }
.autocomplete-descrip { font-size: 13px; color: var(--text); }
.autocomplete-sep { font-size: 13px; color: var(--text-muted); }
/* nowrap on these two only: a bin code or a quantity broken across lines is
   unreadable, and they are short enough that keeping them whole costs
   nothing. The description is what absorbs the wrapping. */
.autocomplete-location { font-size: 13px; font-weight: 600; color: var(--text); white-space: nowrap; }
.autocomplete-qty { font-size: 13px; font-weight: 600; color: var(--text); white-space: nowrap; }
/* Nothing in that bin: still worth showing (the part exists there), but it
   should not read as an available quantity. */
.autocomplete-qty.is-zero { color: var(--text-muted); font-weight: 400; }
/* Sits at the bottom of the scroll, so it is read on arriving at what looks
   like the end of the list - which is exactly when "there are more" matters.
   Not clickable, and visibly not a result. */
.autocomplete-more { padding: 8px 10px; font-size: 12px; font-style: italic; color: var(--text-muted); border-top: 1px solid var(--border); background: var(--bg); cursor: default; }

/* Auth */
.auth-shell { min-height: 100vh; display: flex; flex-direction: column; }
/* Signed-out brand band — the same lockup as the header, centred. */
.auth-band {
    background: var(--site-bg); border-bottom: 1px solid var(--site-divider);
    padding: 36px 32px 30px; display: flex; flex-direction: column; align-items: center; gap: 14px;
    font-family: var(--site-font-body);
}
.auth-band img { height: 64px; width: auto; display: block; }
.auth-brand-text { display: flex; flex-direction: column; align-items: center; line-height: 1; }
.auth-brand-name {
    font-family: var(--site-font-heading); font-weight: 700; font-size: 26px;
    letter-spacing: 0.06em; text-transform: uppercase; color: var(--site-text);
}
.auth-brand-sub {
    font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase; margin-top: 6px;
    color: color-mix(in srgb, var(--site-text) 68%, transparent);
}
.auth-body { flex-grow: 1; display: flex; align-items: center; justify-content: center; padding: 32px; }
.auth-box { width: 400px; display: flex; flex-direction: column; gap: 28px; }
.auth-footer { text-align: center; font-size: 12.5px; color: var(--text-muted); }

.push-banner {
    position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
    background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
    box-shadow: 0 8px 24px rgba(25, 27, 26, .18);
    padding: 14px 18px; display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
    max-width: 90vw; font-size: 13.5px; color: var(--text); z-index: 100;
}
.push-banner-actions { display: flex; gap: 8px; flex-shrink: 0; }

.flash {
    padding: 13px 16px; border-radius: var(--radius); font-size: 14px; margin-bottom: 4px;
    border-left: 4px solid transparent;
}
.flash-error { background: var(--red-bg); color: var(--red-text); border-left-color: var(--red-text); }
.flash-success { background: var(--green-bg); color: var(--green-text); border-left-color: var(--green-dot); }
/* Neither good news nor bad: "here is what just happened, carry on". Used by
   the repeat-a-request form to explain why the job number is blank. */
.flash-info { background: var(--amber-bg); color: var(--amber-text); border-left-color: var(--amber-text); }

/* What the server said when it refused an htmx request.
   htmx will not swap a 4xx body, so without somewhere to put the message a
   refused action is indistinguishable from a dead button. Fixed to the
   bottom so it does not move the board underneath it, and above the push
   banner's z-index so the two cannot hide each other. */
.toasts {
    position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
    display: flex; flex-direction: column; gap: 8px; align-items: center;
    z-index: 200; pointer-events: none; width: max-content; max-width: min(90vw, 520px);
}

.toast {
    pointer-events: auto; cursor: pointer;
    background: var(--red-bg); color: var(--red-text);
    border-left: 4px solid var(--red-text); border-radius: var(--radius);
    padding: 13px 16px; font-size: 14px; line-height: 1.45;
    box-shadow: 0 8px 24px rgba(25, 27, 26, .18);
    animation: toast-in .18s ease-out;
}

@keyframes toast-in {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .toast { animation: none; }
}

/* The header collapses at the site's breakpoint, not the app's: at 740px wide
   the website's nav is a hamburger, and the app's would not have been. */
@media (max-width: 767px) {
    /* wrap, NOT the site's nowrap. The site sets flex-wrap: nowrap here and
       its open menu is squeezed into a column beside the logo instead of
       dropping below the bar - reproduced on the live site at 375px. Copying
       the header should not mean copying that. */
    .topbar { padding: 10px 16px; gap: 12px; flex-wrap: wrap; row-gap: 0; }
    .topbar-brand img { height: 38px; }
    .topbar-company { font-size: 15px; letter-spacing: 0.04em; }
    .topbar-tagline { display: none; }
    .topbar-toggle { display: inline-flex; order: 3; }
    .topbar-nav {
        display: none;
        order: 4;
        width: 100%;
        margin: 0;
        flex-direction: column;
        align-items: stretch;
        gap: 0;
        border-top: 1px solid var(--site-divider);
    }
    .topbar-nav.is-open { display: flex; }
    .topbar-links { flex-direction: column; align-items: stretch; gap: 0; }
    .topbar-links a,
    .topbar-user,
    .topbar .link-button {
        /* 44px: a touch target, and the same row height the website uses in
           its collapsed menu. */
        display: flex; align-items: center;
        padding: 14px 4px; font-size: 17px; min-height: 44px;
        border-bottom: 1px solid var(--site-divider);
    }
    .topbar-divider { display: none; }
    .topbar-nav form { width: 100%; }
    .topbar .link-button { width: 100%; text-align: left; border-bottom: 0; }
}

/* The site drops its brand name below 420px, but only because the phone
   number has to stay in the collapsed bar and the two will not both fit. The
   app's bar carries no phone number, so the name keeps its room and the header
   goes on saying where you are. */

@media (max-width: 860px) {
    /* The card's four columns will not hold a job number, an age, a chip and
       two buttons much below this. Job and age keep the top line; status and
       actions drop to a second one, still on their own sides. */
    .req-head {
        grid-template-columns: 1fr auto;
        grid-template-areas:
            "job    age"
            "status acts";
        row-gap: 14px;
    }
    .req-job-cell { grid-area: job; min-width: 0; }
    .req-age { grid-area: age; }
    .req-status { grid-area: status; align-self: center; }
    .req-acts { grid-area: acts; }
}

@media (max-width: 720px) {
    /* --- Tables ---
       Every .cols-* grid collapses to a single stacked column. Each cell
       carries a data-label attribute (added in the view markup); the
       ::before rule below turns that into a small caption above the value,
       so a stacked row stays readable instead of becoming an ambiguous list
       of bare values. Header rows are redundant once every cell labels
       itself, so they're hidden. */
    [class*="cols-"] { grid-template-columns: 1fr !important; }
    .table-row-head { display: none; }
    .expand-inner .cols-lines:first-child { display: none; }
    .table-row, .cols-lines { gap: 10px; }
    [data-label] { text-align: left !important; }
    [data-label]::before {
        content: attr(data-label);
        display: block;
        font-size: 10px;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.05em;
        color: var(--text-muted);
        margin-bottom: 3px;
    }

    /* --- Request cards ---
       The parts table inside a card would otherwise force the page sideways
       on a phone: a part number, a full description and a bin code will not
       share 375px. It scrolls in its own container instead. */
    .page-title { font-size: 28px; }
    .req-head { padding: 14px 16px; gap: 12px; }
    .req-job { font-size: 23px; }
    .req-age b { font-size: 20px; }
    .req-detail { padding: 2px 16px 14px; overflow-x: auto; }
    .req-lines { min-width: 560px; }
    /* Actions get their own full-width row on a phone — side by side with the
       status chip they would each be too narrow to hit reliably. */
    .req-head {
        grid-template-areas:
            "job    age"
            "status status"
            "acts   acts";
    }
    .req-acts { width: 100%; }
    .req-acts form { flex: 1; }
    .req-acts .btn { width: 100%; }
    .counts { gap: 22px; }
    .count b { font-size: 26px; }

    /* --- Auth screens ---
       The card is a fixed 400px on desktop; below the breakpoint let it
       fill the (already padded) auth-body instead of overflowing it. */
    .auth-box { width: 100%; }
    .auth-body { padding: 20px; }

    /* --- Tap targets ---
       Bump anything a thumb needs to hit up to ~40px tall. Desktop keeps
       the smaller/denser sizing (.btn-sm etc.) untouched. */
    .btn-sm { height: 44px; }
    .input-sm { height: 44px; }
    .tab { padding: 11px 16px 13px; }
    .admin-tabs a { display: inline-block; padding: 8px 4px; }
    .btn-icon { width: 44px; height: 44px; }
    .table-row .link-button, .expand-panel .link-button {
        display: inline-block; padding: 10px 4px;
    }
    .notify-checkbox { width: 24px; height: 24px; }
    .notify-checkbox-cell { min-height: 40px; justify-content: flex-start; }

    /* Purely decorative expand/collapse arrow — the whole row is already
       tappable, so it doesn't need its own labeled line when stacked. */
    .row-chevron { display: none; }
}

/* ---------------------------------------------------------------------------
   Settings jump links

   The settings page is seven sections of credentials, and scrolling to the
   one you want meant passing every other one. These are plain anchors, so
   they keyboard-navigate, middle-click into a tab and produce a URL worth
   sending somebody.

   scroll-margin-top on the targets is what stops an anchor landing with its
   heading tucked under the sticky site header - the classic broken-looking
   jump link where the section title is invisible and the fields start
   mid-air.
   --------------------------------------------------------------------------- */

.settings-jump {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    position: sticky;
    top: 0;
    z-index: 5;
    padding: 10px 0;
    background: var(--bg);
    border-bottom: 1px solid var(--border-soft);
}

.settings-jump a {
    font-family: var(--font-condensed);
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-secondary);
    text-decoration: none;
    padding: 5px 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    white-space: nowrap;
}

.settings-jump a:hover,
.settings-jump a:focus-visible {
    color: var(--brand);
    border-color: var(--brand);
}

/* Every jump target, so the heading clears the sticky bar above it. */
[id^="section-"] {
    scroll-margin-top: 64px;
}
