* { box-sizing: border-box; }

/* Paired with the color-scheme meta tag in index.html — see the reasoning there. Declared in both
   places deliberately: the meta tag applies before the stylesheet has loaded, and this one keeps
   the declaration with the palette it protects, so deleting one does not silently lose it. */
:root { color-scheme: light; }

body {
  margin: 0;
  font-family: -apple-system, Segoe UI, Roboto, Arial, sans-serif;
  background: #f4f6f8;
  color: #1f2933;
  font-size: 16px;
}

h1 { font-size: 22px; margin: 0 0 12px; }
h2 { font-size: 19px; margin: 0 0 8px; }
h3 { font-size: 16px; margin: 20px 0 8px; color: #1e3a5f; }
.muted { color: #67748a; font-size: 14px; }

/* ---------- Boot / auth ---------- */
.boot-screen { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 16px; }
.auth-card { background: #fff; border-radius: 14px; padding: 24px; width: 100%; max-width: 360px; box-shadow: 0 1px 4px rgba(0,0,0,0.1); }
.auth-card h1 { text-align: center; }
.auth-error { color: #c0392b; font-size: 14px; margin: 8px 0; }
.auth-toggle { text-align: center; font-size: 14px; margin: 8px 0 0; }
.auth-toggle a { color: #1e3a5f; }

/* ---------- Top bar ---------- */
.topbar {
  position: fixed; top: 0; left: 0; right: 0; height: 56px;
  background: #1e3a5f; color: #fff;
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 16px; z-index: 50;
}
.topbar h1 {
  color: #fff; font-size: 18px; margin: 0; display: flex; align-items: center; gap: 8px;
  /* Three header buttons instead of two, so a long organisation name now has to give way rather
     than shove them off the edge of a phone. */
  min-width: 0; overflow: hidden;
}
.topbar h1 #app-brand { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.demo-badge { background: #d4a017; color: #1f2933; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; flex: 0 0 auto; }
.icon-btn {
  background: none; border: none; color: #fff; font-size: 22px;
  min-width: 44px; min-height: 44px; cursor: pointer;
}
/* Emergency, moved up here from the bottom bar. Deliberately UNDERSTATED rather than a red button:
   this screen is shut-off locations and after-hours numbers, not a panic button — the silent alarm
   is a separate thing — and a header that shouts at somebody all day stops being read. The tinted
   pill and the gap on its right make it findable at a glance and hard to hit by accident when
   reaching for the gear, which is the button next to it that gets tapped constantly. */
.icon-btn-alert {
  background: rgba(255,255,255,0.14); border-radius: 999px;
  margin-right: 6px; font-size: 20px;
}
.topbar-actions { display: flex; align-items: center; gap: 2px; flex: 0 0 auto; }

/* ---------- Main content ---------- */
/* ⚠️ The bottom figure clears the fixed nav AND whatever the phone takes below it — see the
   note on .bottom-nav. Without the inset the last row of any list sits behind the nav on a phone
   with system buttons. */
#app-content { padding: 68px 16px calc(84px + env(safe-area-inset-bottom, 0px)); min-height: 100vh; }
#app-content.has-guest-banner { padding-top: 108px; }
.screen-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }

/* ---------- Guest pass / paywall (24hr trial -> Google -> Lemon Squeezy) ---------- */
#guest-pass-banner { font-size: 13px; padding: 8px 14px; }
#guest-pass-banner .btn { margin: 0; flex: 0 0 auto; }

/* ---------- Bottom nav ---------- */
/* Scrolls sideways rather than squeezing. With eight sections a flex:1 split gives each button
   about 48px on a phone, which is narrower than the words "Maintenance" and "Emergency" —
   they clip or wrap into an unreadable stack. A fixed min-width keeps every label legible and
   keeps working as more modules are added, instead of degrading a little further each time.
   Momentum scrolling and snapping make it feel deliberate rather than broken. */
/* ⚠️ NO HORIZONTAL SCROLL ANY MORE, and that is the whole fix rather than a tidy-up.
   This used to be `overflow-x: auto` with the scrollbar deliberately hidden, holding eleven
   hard-coded buttons at a 72px minimum. On a 390px phone that showed about five of them with
   NOTHING on screen indicating the other six existed — the app's own Emergency screen among them.
   Five slots now (four by role plus More), so they divide the width instead of overflowing it.
   Alan: "with less buttons at the bottom they could probably be bigger" — they are: 68px tall,
   24px icons, 12px labels, and each one is a fifth of the screen rather than 72px of it. */
/* ROOM FOR THE PHONE'S OWN BUTTONS — allowed for here, and ⚠️ INERT IN THIS APP TODAY.
 *
 * Added 2026-10-12 after Alan: "on several of my apps the bottom navigation bar is white and it
 * obscures the three browser buttons at the bottom."
 *
 * ⚠️⚠️ READ THIS BEFORE CITING IT AS THAT FIX. The commit that added it (`8c4e54d`) claims
 * index.html asks for `viewport-fit=cover`. IT DOES NOT — see the deliberate comment at
 * index.html:5, there since 2026-08-26, explaining why this app opts out. And
 * `env(safe-area-inset-*)` reports **0 unless the viewport is `cover`**, so both `calc()`s here
 * resolve to exactly 68px and 84px: this renders identically to the version before it, on every
 * device. It is defensive and becomes correct the moment the viewport ever goes `cover`. It is NOT
 * a diagnosis of what Alan reported, which is still open — see BACKLOG.md, "START HERE".
 *
 * ⚠️ height + padding, NOT min-height, and that difference WAS measured. box-sizing is border-box
 * here, so `min-height: 68px` let the 68px buttons inside push the bar to 69 and move its top edge
 * by a pixel on every phone, including the ones with nothing wrong with them.
 *
 * ⚠️ Everything measured from the bottom carries the same inset or it would land on the nav
 * instead: #app-content's bottom padding and the update bar, both below. */
.bottom-nav {
  position: fixed; bottom: 0; left: 0; right: 0;
  height: calc(68px + env(safe-area-inset-bottom, 0px));
  padding-bottom: env(safe-area-inset-bottom, 0px);
  display: flex; background: #fff; border-top: 1px solid #ddd; z-index: 50;
}
.bottom-nav button {
  flex: 1 1 0; min-width: 0;
  border: none; background: none; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 3px; min-height: 68px;
  font-size: 12px; color: #67748a; cursor: pointer;
  /* A label that does not fit is truncated rather than wrapped — a two-line label makes one
     button taller than its neighbours and the whole row looks broken. */
  padding: 0 2px;
}
.bottom-nav button .nav-icon { font-size: 24px; line-height: 1; }
.bottom-nav button .nav-label {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bottom-nav button.active { color: #1e3a5f; font-weight: 600; }

/* ---------- Recurring bills ----------
   A late bill is a different thing from one merely due — it is the one that costs money — so it
   gets a colour rather than a slightly different word. */
.bills-due .list-row { border-left: 3px solid #c47f00; padding-left: 10px; }
.bills-due .list-row.late { border-left-color: #b42318; background: #fdf3f2; }

.util-row {
  display: flex; flex-wrap: wrap; gap: 10px; align-items: flex-end;
  border: 1px solid #e2e6ea; border-radius: 8px; padding: 10px 12px; margin: 8px 0;
}
.util-row label { flex: 1 1 140px; min-width: 0; margin: 0; }
.util-row .btn { flex: 0 0 auto; }
.util-add { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }

/* The comparison, at the moment the amount is typed. Understated when normal — most bills are —
   and only coloured when something actually moved. */
.bill-compare { margin: 4px 0 12px; min-height: 1.2em; }
.bill-normal { color: #1e4620; background: #e7f4ea; border-radius: 8px; padding: 8px 12px; margin: 0; font-size: 14px; }
.bill-flag { border-radius: 8px; padding: 10px 12px; font-size: 14px; }
.bill-flag.high { background: #fdf3f2; color: #5c1a14; border: 1px solid #f0c7c2; }
.bill-flag.low  { background: #fdf2d0; color: #5c4a00; border: 1px solid #e6d08a; }
.bill-flag div { margin-top: 4px; }
.bill-flag .muted { font-size: 12px; }

/* A bar per row rather than a chart: the shape of a year is the question, and this answers it
   without a charting library, a canvas, or anything to load. */
.hist-list { margin-top: 8px; }
.hist-row {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 2px; border-bottom: 1px solid #e2e6ea; font-size: 14px;
}
.hist-when { flex: 0 0 96px; color: #4a5568; }
.hist-bar { flex: 1 1 auto; background: #eef1f4; border-radius: 3px; height: 10px; min-width: 40px; }
.hist-bar span { display: block; height: 100%; background: #1e3a5f; border-radius: 3px; }
.hist-amt { flex: 0 0 80px; text-align: right; font-variant-numeric: tabular-nums; }
.hist-delta { flex: 0 0 56px; text-align: right; font-size: 13px; font-variant-numeric: tabular-nums; }
.hist-delta.high { color: #b42318; font-weight: 600; }
.hist-delta.low { color: #8a6d00; }

/* Which bill you are looking at. Sits at the very top of the history, because after the first
   switch it is the thing that says what is on the screen — the modal title cannot, it is set once
   and the contents change underneath it. Borrows .assignee-select's 44px target from Duties: same
   job, same one-handed use, no reason for it to look like a different kind of control. */
.hist-pick { margin: 0 0 12px; font-size: 13px; color: #67748a; }

/* A bill's history, a year at a time.
   Built on <details> so the years keep keyboard and screen-reader behaviour for free and — the part
   that matters most here — so opening one needs no JavaScript binding at all. A handler attached at
   render time inside a modal is exactly the Android trap in CLAUDE.md; the browser's own disclosure
   has no such failure mode. */
.hist-year { border: 1px solid #e2e8f0; border-radius: 10px; margin-bottom: 8px; overflow: hidden; }
.hist-year > summary {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 12px 14px; cursor: pointer; background: #f7fafc; list-style: none;
}
.hist-year > summary::-webkit-details-marker { display: none; }
.hist-year > summary::after { content: '▾'; color: #67748a; }
.hist-year[open] > summary::after { content: '▴'; }
.hist-year-name { font-size: 16px; font-weight: 600; }
/* Right-aligned tabular figures, so the year totals stack into a column that can be read down. */
.hist-year-sum { color: #4a5568; font-size: 14px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.hist-year-vs {
  padding: 9px 14px 0; color: #67748a; font-size: 13px; line-height: 1.45;
  font-variant-numeric: tabular-nums;
}
.hist-year > .hist-list { padding: 0 14px 6px; }
.hist-year > .hist-list .hist-row:last-child { border-bottom: none; }

/* ---------- Which printer ----------
   Two large targets rather than a chip row. Getting this wrong wastes physical stock — a sheet
   layout sent to a thermal printer arrives as the whole page shrunk onto one sticker — so the
   choice is worth the vertical space and worth reading like a question. */
.printer-choice { display: flex; gap: 10px; margin: 4px 0 14px; }
.printer-opt {
  flex: 1 1 0; min-width: 0; cursor: pointer; text-align: left;
  display: flex; flex-direction: column; gap: 3px;
  padding: 12px; border: 2px solid #e2e6ea; border-radius: 10px; background: #fff;
}
.printer-opt.on { border-color: #1e3a5f; background: #eef3f9; }
.printer-icon { font-size: 22px; line-height: 1; }
.printer-name { font-weight: 600; font-size: 14px; color: #1f2933; }
.printer-sub { font-size: 12px; color: #67748a; }
@media (max-width: 420px) { .printer-choice { flex-direction: column; } }

/* A label drawn at its real size, so "is the QR too small?" is answered before anything is
   printed. CSS inches are ~96px, which is close enough on a normal monitor to be honest about —
   and the exact printed size of the code is stated in words underneath either way. */
.label-preview-wrap { margin: 12px 0; }
.label-preview {
  border: 1px dashed #9aa5b1; border-radius: 3px; box-sizing: border-box;
  padding: 1.5mm; background: #fff; overflow: hidden;
  /* Mirrors Labels.labelCSS so the preview cannot drift from what gets printed. */
  font-family: -apple-system, "Segoe UI", Roboto, Arial, sans-serif; color: #000;
}
.label-preview .lbl-in { display: flex; align-items: center; gap: 1.5mm; height: 100%; }
.label-preview .lbl-qr { flex: 0 0 auto; width: 45%; max-width: 100%; aspect-ratio: 1 / 1; }
.label-preview .lbl-qr svg { width: 100%; height: 100%; display: block; }
.label-preview .lbl-txt { flex: 1 1 auto; min-width: 0; line-height: 1.15; }
.label-preview .lbl-name { font-weight: 700; font-size: 8pt; overflow: hidden; word-break: break-word; }
.label-preview .lbl-sub { font-size: 6pt; color: #222; overflow: hidden; }

/* ---------- "More" ---------- */
.more-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 4px;
}
.more-tile {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
  padding: 16px 6px; min-height: 84px;
  border: 1px solid #e2e6ea; border-radius: 10px; background: #fff;
  font-size: 13px; color: #1f2933; cursor: pointer; text-align: center;
}
.more-tile:hover { background: #f4f7fa; }
.more-icon { font-size: 26px; line-height: 1; }
.more-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
@media (min-width: 600px) { .more-grid { grid-template-columns: repeat(4, 1fr); } }

/* ---------- Duties / Maintenance / Equipment are one job ---------- */
.group-tabs {
  display: flex; gap: 4px; margin: 0 0 12px; border-bottom: 1px solid #e2e6ea;
  overflow-x: auto; scrollbar-width: none;
}
.group-tabs::-webkit-scrollbar { display: none; }
.group-tab {
  flex: 0 0 auto; border: none; background: none; cursor: pointer;
  padding: 10px 14px; min-height: 44px; font-size: 15px; color: #67748a;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.group-tab.active { color: #1e3a5f; font-weight: 600; border-bottom-color: #1e3a5f; }

/* The date and Whose pickers sit side by side where there is room and stack on a phone. */
.duties-controls { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; }
.duties-controls label { flex: 1 1 200px; min-width: 0; }

/* ---------- Buttons ---------- */
.btn {
  display: inline-block; padding: 12px 18px; border-radius: 8px; border: none;
  font-size: 16px; min-height: 44px; cursor: pointer; text-align: center; text-decoration: none;
}
.btn-block { display: block; width: 100%; margin: 8px 0; }
.btn-primary { background: #1e3a5f; color: #fff; }
.btn-secondary { background: #e7ebf0; color: #1f2933; }
.btn-danger { background: #c0392b; color: #fff; }
.btn-ghost { background: none; color: #1e3a5f; border: 1px solid #1e3a5f; }
.btn-ghost:disabled { opacity: 0.4; cursor: default; }
.btn-icon { background: none; border: none; font-size: 18px; min-width: 36px; min-height: 36px; cursor: pointer; color: #67748a; }
.btn-call { background: #1f7a3d; color: #fff; }
.btn-sm { padding: 8px 12px; font-size: 14px; min-height: 36px; }

/* ---------- Lists ---------- */
.plain-list { list-style: none; margin: 0; padding: 0; }
.list-row {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 12px 4px; border-bottom: 1px solid #e2e6ea;
}
.list-row.tappable { cursor: pointer; }
.list-row-main { flex: 1 1 auto; min-width: 0; }
.list-row-main .title { font-weight: 600; }
.list-row-actions { display: flex; gap: 4px; flex: 0 0 auto; }
.history-entry { padding: 10px 4px; border-bottom: 1px solid #e2e6ea; }
.history-entry p { margin: 4px 0; font-size: 14px; }
.location-group-header { margin: 18px 0 4px; font-weight: 700; color: #1e3a5f; font-size: 15px; }
.location-group-header:first-child { margin-top: 0; }

/* ---------- Badges ---------- */
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 12px; background: #e7ebf0; color: #1e3a5f; }
.badge-ok { background: #e1f3e6; color: #1f7a3d; }
.badge-warn { background: #fdf2d0; color: #8a6d00; }
/* Added 2026-08-20 with the compliance screen, which had been asking for it: an overdue
   inspection badge was rendering with no colour at all, which on the one screen that exists to
   show what is NOT in order is the worst possible place to fail quietly. */
.badge-bad { background: #fdeceb; color: #b42318; }
.badge-danger { background: #fbe1de; color: #c0392b; }

/* ---------- Forms ---------- */
label { display: block; margin: 10px 0; font-size: 14px; color: #3a4654; }
input, select, textarea {
  display: block; width: 100%; margin-top: 4px; padding: 10px; font-size: 16px;
  border: 1px solid #c9d0d9; border-radius: 6px; background: #fff; font-family: inherit;
}
textarea { min-height: 70px; resize: vertical; }
.field-row { display: flex; gap: 10px; }
.field-row label { flex: 1; }
.checkbox-row { display: flex; align-items: center; gap: 8px; }
.checkbox-row input { width: auto; margin: 0; }
.inline-form { display: flex; flex-direction: column; gap: 8px; margin: 10px 0; }

/* ---------- Photo upload (Android rule: full-size transparent input over a styled button,
   never a hidden input triggered via .click()) ---------- */
.photo-upload-wrap { position: relative; display: inline-block; width: 100%; margin: 10px 0; }
.photo-upload-btn {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; min-height: 44px; border-radius: 8px; border: 1px dashed #1e3a5f;
  background: #eef2f6; color: #1e3a5f; font-size: 15px; pointer-events: none;
}
.photo-upload-wrap input[type=file] {
  position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; margin: 0; padding: 0; cursor: pointer;
}
.equip-photo-preview { width: 100%; max-height: 220px; object-fit: cover; border-radius: 8px; margin: 8px 0; display: block; }
.equip-photo-thumb { width: 48px; height: 48px; object-fit: cover; border-radius: 6px; flex: 0 0 auto; background: #e7ebf0; }

/* ---------- Dashboard ---------- */
/* ---------- Stay Awake + dim curtain ---------- */
/* Above every other layer in the app (modals are 110) — the point is that the screen goes dark
   whatever is open, while the app keeps running underneath. */
#dimCurtain {
  display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(0, 0, 0, 0.95); z-index: 9999; cursor: pointer;
}
/* Which app went dark. Without it a 95%-black rectangle is indistinguishable from a crashed app
   or a phone that did not wake — reported by Alan on 2026-08-20 after returning from another app.

   THE NAME AND NOTHING ELSE. No "tap anywhere to open", no icon: Alan's call, and the right one —
   the gesture is learned once, but the pixels would be lit for as long as the curtain is up, on a
   screen whose entire purpose is to have as few lit pixels as possible.

   ⚠️ IT DRIFTS, AND THAT IS NOT DECORATION. Static text held for hours is exactly the OLED
   burn-in this curtain exists to prevent, so it walks a few percent around the centre over two
   minutes — far too slow to notice, fast enough that no pixel holds the same glyph for long.
   pointer-events: none so a tap on the words still reaches the curtain underneath. */
#dimBrand {
  position: absolute; left: 50%; top: 50%;
  color: #3a3a3a; font-size: 15px; font-weight: 600; letter-spacing: 0.08em;
  text-align: center; max-width: 70%; pointer-events: none;
  animation: dim-drift 120s ease-in-out infinite alternate;
  transform: translate(-50%, -50%);
}
@keyframes dim-drift {
  0%   { transform: translate(-50%, -50%) translate(-7vw, -9vh); }
  50%  { transform: translate(-50%, -50%) translate(6vw,  8vh); }
  100% { transform: translate(-50%, -50%) translate(-5vw, 10vh); }
}
/* Asked to hold still, it holds still. #3a3a3a on near-black is dim enough that the burn-in
   trade is acceptable for the people who set that preference. */
@media (prefers-reduced-motion: reduce) {
  #dimBrand { animation: none; }
}
/* On-state has to read at a glance in daylight, so it is a filled pill rather than a tint. */
.icon-btn.active { background: #ffd97a; border-radius: 999px; box-shadow: inset 0 0 0 1px #e0a800; }

.stat-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin: 12px 0; }
/* The tile count changes with which modules an organisation has, so it is sometimes odd. A lone
   final tile spans the full width rather than sitting half-width beside a gap. */
.stat-grid > .stat-card:last-child:nth-child(odd) { grid-column: 1 / -1; }
.stat-card { background: #fff; border-radius: 10px; padding: 14px 8px; text-align: center; cursor: pointer; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
.stat-num { font-size: 26px; font-weight: 700; color: #1e3a5f; }
.stat-num.danger { color: #c0392b; }
.stat-num.warn { color: #b8860b; }
.stat-label { font-size: 12px; color: #67748a; margin-top: 4px; }
.quick-actions { margin-top: 16px; }

/* ---------- Viewing as somebody else ----------
   Its own row under the building selector, and it renders on every screen — including Settings,
   where "what can a sub actually reach" is one of the real questions it answers. Sticks below the
   selector so both survive a scroll; 104px is 56 (header) + 48 (the selector row). */
.role-bar {
  position: sticky; top: 104px; z-index: 29;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin: -12px -16px 12px; padding: 7px 16px;
  background: #f2f6fa; border-bottom: 1px solid #e2e6ea;
  font-size: 13px; color: #4a5568;
}
/* No selector above it (a single-building organisation has none), so it takes that slot instead. */
.location-bar + .role-bar { top: 104px; }
#app-content > .role-bar:first-of-type { top: 56px; }
#app-content.has-guest-banner .role-bar { top: 144px; }
/* Amber while a preview is running — the same signal as the banner at the top, on the control
   that started it, so the way out is never off-screen. */
.role-bar.previewing { background: #fdf6e3; border-bottom-color: #f0e3b8; color: #6b5400; }
/* ⚠️ THE LABEL WRAPS TO ITS OWN LINE RATHER THAN BEING CRUSHED, and the old behaviour was exactly
   backwards. Three things share this row: the label (the STATE — what you are looking at), the
   switch button and, for the app owner only, the plan picker. The button and the picker were both
   `flex: 0 0 auto`, so the label was the ONLY thing that could give — and it gave everything.

   Measured on a 390px phone with the plan picker present: the label needed 130px and was rendered
   at 36. About six characters. "Viewing as a Manager" showed as "Viewi…", so the one control that
   says which role you are previewing never said it — while the amber banner at the top of the
   screen showed it perfectly.

   Alan, 2026-08-25: "it never actually shows me what I'm viewing as because that section doesn't
   have enough characters to put the rest in." Present since the plan picker was added; the trial
   accounts everything else is tested on never render that third control, which is why it survived.

   min-width in ch rather than a media query: the row wraps exactly when its own contents stop
   fitting, whatever the button happens to say or how long a role is called, instead of at a
   breakpoint somebody guessed. The ellipsis stays as a genuine last resort. */
.role-bar { flex-wrap: wrap; row-gap: 6px; }
/* ⚠️ flex-grow 0, NOT 1. Written as `1 1 auto` first, which made the label greedy: it swallowed all
   the free space and pushed the button onto a second line at EVERY width, including 1440px. It
   needs to refuse to shrink, not to expand — `0 1 auto` takes its natural width, sits beside the
   controls while there is room, and wraps them below only when there genuinely is not. */
.role-bar-label {
  flex: 0 1 auto;
  min-width: 26ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.role-bar .btn { flex: 0 0 auto; }

/* ---------- The home screen: the chips ----------
   Read-only, deliberately. The buttons a few rows down already go everywhere, and making these tap
   targets too would put a third route to Budget on one screen — which is the duplication we just
   spent an evening removing.
   Equal widths so three chips never jostle as their contents change length: "All done" and "3" have
   to occupy the same box, or the strip reflows every time somebody finishes a job. */
/* ⚠️ EVERY RULE BELOW IS SCOPED `.chip-strip …` AND MUST STAY THAT WAY.
   `.chip` is used by three different things in this stylesheet: these home-screen cards, and TWO
   generations of filter pill (around line 470 and line 1060). All three were written as a bare
   `.chip`, which is the same specificity — so the last one in the file won, and these cards were
   silently taking their background, border, radius, padding, min-height and white-space from a
   pill rule 700 lines further down.

   What that actually cost, before it was found on 2026-08-20:
     • the green/amber/red TINTS never rendered at all — `.chip-good`'s background lost to the
       later bare `.chip`, while `.chip-good .chip-value` (two classes) won, so the text was
       coloured on a white card and the card itself never was;
     • the 66px equal-height box was really 36px;
     • `white-space: nowrap` was inherited, which is the real reason the budget chip read "On sch…"
       on 2026-08-19. The `overflow-wrap: break-word` added that day could not have worked —
       nowrap suppresses line breaking outright. Shortening the words is what fixed the symptom.
   Scoping to `.chip-strip .chip` makes these two classes deep, so they outrank both pill rules
   wherever they sit in the file, and a fourth `.chip` added later cannot reach in here either. */
.chip-strip { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 12px 0 4px; }
.chip-strip .chip {
  background: #fff; border: 1px solid #e2e6ea; border-radius: 10px;
  padding: 9px 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.06);
  min-width: 0;                       /* or a long word widens the grid instead of wrapping */
  /* Centred in a fixed-height box so a two-line value and a one-line one still line up. */
  min-height: 66px; display: flex; flex-direction: column;
  align-items: center; justify-content: center; text-align: center;
  white-space: normal;                /* explicit: see the nowrap note above */
  position: relative; font-family: inherit;
}
/* ⚠️ WRAPS, NEVER ELLIPSIZES. This read "On sch…" at 390px, which defeats the whole reason the
   budget chip carries a word at all — the word is what means something to somebody who cannot
   rely on the colour, and half of it means nothing to anybody. */
.chip-strip .chip-value {
  font-size: 17px; font-weight: 700; color: #1e3a5f; line-height: 1.2;
  font-variant-numeric: tabular-nums;
  overflow-wrap: break-word; hyphens: auto;
}
.chip-strip .chip-label { font-size: 11px; color: #67748a; margin-top: 3px; line-height: 1.25; }
/* A colour AND a word, never a colour alone — see the note on chipBudgetPace. The tint is a hint
   for people who read it fast; the word is what actually carries the meaning. */
.chip-strip .chip-good .chip-value { color: #1e7a3c; }
.chip-strip .chip-good { background: #f2fbf5; border-color: #cbead6; }
.chip-strip .chip-warn .chip-value { color: #8a6d00; }
.chip-strip .chip-warn { background: #fdfaf0; border-color: #f0e3b8; }
.chip-strip .chip-bad  .chip-value { color: #b42318; }
.chip-strip .chip-bad  { background: #fdf4f3; border-color: #f3ccc8; }
.chip-strip .chip-quiet .chip-value { color: #67748a; font-weight: 600; }

/* The tap affordance. Small and grey on purpose: it has to say "there is more here" without
   competing with the number, which is the only thing most people ever want from this strip. */
.chip-strip .chip-tappable { cursor: pointer; -webkit-tap-highlight-color: transparent; }
.chip-strip .chip-q {
  position: absolute; top: 3px; right: 6px;
  font-size: 11px; font-weight: 700; color: #a8b2c0; line-height: 1;
}
.chip-strip .chip-tappable:active { transform: scale(0.97); }
.chip-strip .chip-tappable:focus-visible { outline: 2px solid #1e3a5f; outline-offset: 2px; }

/* ---------- Help sheets ----------
   Reached by tapping a chip. Set wider and looser than the app's dense list screens because this
   is the one place somebody is reading sentences rather than scanning rows. */
.help-what { font-size: 16px; line-height: 1.5; color: #1e3a5f; font-weight: 600; margin: 0 0 14px; }
.help-sub { font-size: 12px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
  color: #67748a; margin: 16px 0 6px; }
.help-list { margin: 0; padding-left: 20px; }
.help-list li { font-size: 14px; line-height: 1.55; color: #333; margin-bottom: 7px; }
.help-tones { list-style: none; margin: 0; padding: 0; }
.help-tones li { display: flex; align-items: baseline; gap: 8px; font-size: 14px;
  line-height: 1.5; margin-bottom: 6px; }
.help-dot { flex: 0 0 auto; width: 11px; height: 11px; border-radius: 50%;
  border: 1px solid rgba(0,0,0,0.12); transform: translateY(1px); }
.help-dot-good { background: #1e7a3c; }
.help-dot-warn { background: #d9a400; }
.help-dot-bad  { background: #b42318; }
.help-dot-quiet { background: #98a2b3; }
.help-tone-name { font-weight: 700; color: #1e3a5f; flex: 0 0 auto; }
.help-tone-text { color: #4a5666; }
/* The caveat block. Tinted rather than plain because it is nearly always the answer to "why is
   this chip missing / why is that number not what I expected" — the reason somebody opened this. */
.help-note { margin-top: 16px; padding: 11px 13px; background: #f4f7fb;
  border-left: 3px solid #1e3a5f; border-radius: 0 8px 8px 0;
  font-size: 13px; line-height: 1.55; color: #333; }
.help-note b { color: #1e3a5f; }
/* Through to everything, from every sheet. Sits below the "others on your screen" row because it
   is the wider question — you ask it after the specific one has been answered. */
.help-guide-btn { margin-top: 18px; }

/* The guide. Collapsed by default: fourteen screens and eight numbers open at once is a wall, and
   the summary line alone is enough to find the one you came for. */
.guide-topic { border-bottom: 1px solid #f0f3f6; padding: 10px 0; }
.guide-topic:last-child { border-bottom: none; }
.guide-topic > summary { cursor: pointer; font-size: 14px; line-height: 1.5; color: #333;
  list-style: none; padding-right: 4px; }
.guide-topic > summary::-webkit-details-marker { display: none; }
.guide-topic > summary::before { content: '▸ '; color: #8a94a0; font-weight: 700; }
.guide-topic[open] > summary::before { content: '▾ '; }
.guide-topic > summary b { color: #1e3a5f; }
.guide-topic > *:not(summary) { margin-left: 16px; }

.help-also { margin-top: 20px; padding-top: 14px; border-top: 1px solid #e8ecf1; }
.help-also-head { font-size: 12px; font-weight: 700; letter-spacing: 0.05em;
  text-transform: uppercase; color: #67748a; margin-bottom: 8px; }
.help-also-row { display: flex; flex-wrap: wrap; gap: 8px; }
.help-also-btn {
  min-height: 40px; padding: 8px 14px; font-family: inherit; font-size: 14px; font-weight: 600;
  color: #1e3a5f; background: #fff; border: 1px solid #cbd5e1; border-radius: 18px;
  cursor: pointer;
}
.help-also-btn:active { background: #eef2f6; }

/* ---------- The home screen: needs your attention ----------
   One tappable row per thing that might need doing, most serious first. Deliberately a plain
   list rather than the stack of coloured alert banners this replaced: three banners in three
   different colours read as three unrelated emergencies, and pushed every button off a phone. */
.attn-block { margin: 12px 0 4px; background: #fff; border-radius: 10px; overflow: hidden;
  box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
.attn-head { font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: #67748a; padding: 12px 14px 6px; }
.attn-row {
  display: flex; align-items: center; gap: 10px;
  /* 48px, not the row's natural height — this is the most-tapped thing on the screen and it is
     tapped one-handed, often in a plant room with gloves on. */
  padding: 12px 14px; min-height: 48px; cursor: pointer;
  border-top: 1px solid #eef1f4; font-size: 15px; color: #1f2933;
}
.attn-row:first-of-type { border-top: none; }
.attn-row:active { background: #f2f6fa; }
.attn-icon { flex: 0 0 auto; font-size: 17px; }
.attn-text { flex: 1 1 auto; min-width: 0; line-height: 1.35; }
/* Urgent is a left stripe, not a whole red row. A row that is entirely red is read as broken;
   a stripe puts it top of the list visually without shouting over the ones underneath. */
.attn-row.urgent { box-shadow: inset 3px 0 0 #b42318; font-weight: 600; }
.attn-chev { flex: 0 0 auto; color: #97a3b4; font-size: 18px; }
.attn-more { display: block; width: 100%; text-align: left; padding: 11px 14px;
  border-top: 1px solid #eef1f4; font-size: 14px; }

/* ---------- The home screen: the buttons ----------
   Big targets with the icon on top, because this is a menu read at a glance rather than a form.
   A count rides on the button it belongs to instead of getting its own tile — "3 overdue" used
   to render as a banner AND a stat tile, which is how half the old screen was one number. */
.home-section-label {
  font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  color: #67748a; margin: 18px 0 8px;
}
.home-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
/* An odd last button spans the row rather than leaving a hole — same trick .stat-grid uses. */
.home-grid > .home-btn:last-child:nth-child(odd) { grid-column: 1 / -1; }
.home-btn {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 6px;
  min-height: 84px; padding: 14px 10px;
  background: #fff; border: 1px solid #e2e6ea; border-radius: 10px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.08); cursor: pointer;
  font-family: inherit; color: #1f2933; text-align: center;
}
.home-btn:active { background: #f2f6fa; }
.home-icon { font-size: 26px; line-height: 1; }
.home-label { font-size: 14px; font-weight: 600; line-height: 1.25; }
/* Reserved space top-right so a badge appearing never nudges the label. */
.home-badge {
  position: absolute; top: 8px; right: 8px; min-width: 22px; padding: 1px 6px;
  background: #1e3a5f; color: #fff; border-radius: 11px;
  font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums;
}
.home-badge.danger { background: #b42318; }

/* ---------- Owner Tools ----------
   The version stamp doubles as the way in (seven taps). It must keep looking like plain text —
   the point is that nobody notices it — so no cursor change and no highlight. user-select is the
   part that actually matters: without it, tapping the same words seven times on Android pops the
   text-selection toolbar over the screen instead of counting taps. */
#app-build-stamp {
  -webkit-user-select: none; user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.owner-block { border: 1px solid #e2e6ea; border-radius: 10px; padding: 14px; margin: 12px 0; }
.owner-block h4 { margin: 0 0 8px; font-size: 15px; }
.owner-block .muted { font-size: 13px; line-height: 1.45; }
.alert-banner {
  background: #fbe1de; color: #8a241a; border: 1px solid #f3b6ae;
  border-radius: 8px; padding: 12px 14px; margin: 10px 0; font-size: 14px; cursor: pointer;
}
.alert-banner.warn { background: #fdf2d0; color: #6b5400; border-color: #ffe69c; }

/* ---------- "Where are you working today" — above the panic button ----------
   Quiet by default, because it is a standing fact rather than a warning, and it sits on the one screen
   where nothing should compete with the button. The .warn variant is for a yesterday's answer that needs
   confirming: a stale area attached to an emergency would send somebody to the wrong room, so it has to
   read as a question rather than a statement.
   Wraps on a narrow phone rather than squashing the buttons — the whole row has to stay tappable at
   360px, per the mobile-first rule. */
.area-today {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  background: #eef3f8; color: #2c3e50; border: 1px solid #d7e2ec;
  border-radius: 8px; padding: 10px 12px; margin: 0 0 14px; font-size: 14px; line-height: 1.4;
}
.area-today > span { flex: 1 1 180px; min-width: 0; }
.area-today.warn { background: #fdf2d0; color: #6b5400; border-color: #ffe69c; }

/* The distance-and-direction line in the alarm detail. Empty until this device has a fix of its own, so
   it must collapse to nothing rather than leaving a gap. */
.alarm-detail-bearing:empty { display: none; }
.alarm-detail-bearing {
  margin: 6px 0 0; font-size: 15px; font-weight: 600; color: #1f5c8b;
}

/* ---------- Budget status bar ---------- */
.budget-card { background: #fff; border-radius: 10px; padding: 14px 16px; margin: 10px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
.budget-card-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }
.budget-card-header .loc-name { font-weight: 600; }
.budget-amounts { font-size: 14px; color: #3a4654; margin-bottom: 8px; }
.budget-bar-track { height: 14px; border-radius: 7px; background: #e7ebf0; overflow: hidden; }
.budget-bar-fill { height: 100%; border-radius: 7px; background: #1f7a3d; transition: width 0.2s; }
.budget-bar-fill.warn { background: #d4a017; }
.budget-bar-fill.danger { background: #c0392b; }
.budget-bar-pct { font-size: 12px; color: #67748a; margin-top: 4px; }

/* ---------- Tabs / chips ---------- */
.tab-row { display: flex; border-bottom: 1px solid #ddd; margin: 8px 0; overflow-x: auto; }
.tab { flex: 0 0 auto; padding: 10px 14px; background: none; border: none; border-bottom: 3px solid transparent; font-size: 14px; cursor: pointer; color: #67748a; white-space: nowrap; }
.tab.active { border-color: #1e3a5f; color: #1e3a5f; font-weight: 600; }
.chip-row { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0; }
.chip { padding: 8px 14px; border-radius: 20px; border: 1px solid #1e3a5f; background: #fff; color: #1e3a5f; font-size: 14px; cursor: pointer; }
.chip:disabled { opacity: 0.5; border-color: #aaa; color: #888; cursor: default; }
.action-row { display: flex; gap: 8px; margin: 10px 0; }
.action-row > * { flex: 1; }

/* ---------- Route planner ---------- */
.route-loc-card { background: #fff; border-radius: 10px; padding: 12px 14px; margin: 10px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
.route-loc-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.route-loc-header .loc-name { font-weight: 700; }
.route-reorder-btns { display: flex; gap: 2px; flex: 0 0 auto; }
.route-task-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 0; border-top: 1px solid #eef0f3; }

/* ---------- Modal ---------- */
.modal-root { position: fixed; inset: 0; z-index: 100; }
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,0.5);
  display: flex; align-items: flex-end; justify-content: center; padding: 0;
}
/* Set while any modal is open. The other half of the fix lives on .modal-card below — see the
   note on UI.lockPageScroll for why one without the other leaves the symptom in a different
   gesture. */
body.modal-open { overflow: hidden; }
.modal-card {
  background: #fff; border-radius: 14px 14px 0 0; width: 100%; max-width: 480px;
  max-height: 88vh; overflow-y: auto; padding: 16px 18px 24px;
  /* ⚠️ SCROLL CHAINING. Without this, scrolling a long form to its end hands the remaining
     movement straight to the page underneath — which is what Alan saw behind Edit Location on
     2026-08-21, and what locking the body alone does NOT stop. */
  overscroll-behavior: contain;
}
/* ⚠️ STICKY, because .modal-card is the thing that scrolls and the header lives INSIDE it.
   Measured on a phone-sized viewport with a long form: the ✕ scrolled 2192px off the top and
   there was no visible way to close the modal. The backdrop above the card still closed it, but
   only about 100px of it was showing and nothing told anyone it was tappable.
   The background is opaque so the form does not show through as it slides underneath, and the
   -16px inset cancels .modal-card's own top padding so the sticking edge sits flush. */
.modal-header {
  display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;
  position: sticky; top: -16px; z-index: 1;
  background: #fff; padding: 16px 0 8px; margin-top: -16px;
}
/* Deliberately IDENTICAL to .modal-card on a phone — the widening happens only in the desktop
   media query further down. A wide class that widened everywhere would break the two screens that
   use it on the device most people carry. */
.modal-card-wide { max-width: 480px; }

/* ---------- Setup wizard ---------- */
.wizard-overlay {
  position: fixed; inset: 0; z-index: 110; background: rgba(0,0,0,0.55);
  display: flex; align-items: center; justify-content: center; padding: 16px;
}
.wizard-card { background: #fff; border-radius: 14px; width: 100%; max-width: 480px; max-height: 88vh; overflow-y: auto; padding: 20px; }
.wizard-hint { color: #67748a; font-size: 14px; margin-bottom: 10px; }
.wizard-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 18px; gap: 8px; }
.wizard-step-indicator { font-size: 13px; color: #67748a; }
.wizard-skip { text-align: center; margin-top: 10px; font-size: 14px; }
.wizard-skip a { color: #67748a; text-decoration: underline; }
/* The "just show me an example" offer on the first wizard step. Set off from the question above
   it, because it is an alternative to the whole setup rather than part of it. */
.wizard-sample-offer {
  margin-top: 16px; padding: 12px 14px;
  background: #f6f8fb; border: 1px solid #e3e9f0; border-radius: 10px;
}

/* ---------- Settings ---------- */
.settings-section { background: #fff; border-radius: 10px; padding: 4px 16px; margin: 14px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
.settings-note { font-size: 13px; color: #67748a; background: #eef2f6; border-radius: 8px; padding: 10px 12px; margin: 10px 0; }

/* ---------- Central Supplies ---------- */
.queue-actions { display: flex; gap: 8px; margin: 12px 0 4px; }
.queue-actions > * { flex: 1; }
/* Sits alongside the row's text rather than inside it, and stays a comfortable tap target
   on a phone — the whole queue is worked through by thumb while placing an order. */
.queue-check { width: 22px; height: 22px; margin: 2px 10px 0 0; flex-shrink: 0; }
.queue-branch { font-size: 13px; color: #4a5568; padding: 1px 0 1px 10px; border-left: 2px solid #dbe2ea; margin-top: 3px; }

/* The bulk-vs-local comparison. Set apart from the branch list above it because it answers a
   different question — the rows are what needs buying, this is how to buy it. */
.queue-spread { font-size: 12.5px; color: #8a5a00; margin-top: 5px; }
.queue-bulk { margin-top: 8px; padding: 8px 10px; background: #f4f7fa; border-radius: 6px; font-size: 13px; }
.queue-bulk-head { font-weight: 600; color: #2d3748; }
.queue-bulk-good { color: #1f7a4d; margin-top: 3px; }
.queue-bulk-warn { color: #8a5a00; margin-top: 3px; }
.queue-bulk-add { margin-top: 8px; }
.btn-link { background: none; border: none; color: #2b6cb0; font-size: 13px; padding: 0 0 0 6px; cursor: pointer; text-decoration: underline; }

.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 14px; margin: 12px 0; }
.detail-grid .muted { font-size: 12px; }

/* ---------- Branding ---------- */
/* A long organisation name must never push the settings and stay-awake buttons off a phone
   screen, so the header name truncates rather than wrapping or overflowing. */
.topbar h1 { min-width: 0; overflow: hidden; }
#app-brand { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; vertical-align: bottom; }

.brand-preview { background: #f4f7fa; border: 1px solid #e2e6ea; border-radius: 8px;
  padding: 10px 12px; font-size: 17px; font-weight: 700; color: #1a2733; margin-bottom: 10px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.brand-icons { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.brand-icon-btn { width: 40px; height: 40px; font-size: 19px; background: #fff;
  border: 1px solid #d7dde3; border-radius: 8px; cursor: pointer; }
.brand-icon-btn.active { border-color: #2b5876; box-shadow: inset 0 0 0 1px #2b5876; }

/* ---------- Collapsible settings sections ---------- */
/* Built on <details>, so it keeps keyboard support, screen-reader semantics and the browser's
   own find-in-page behaviour for free — none of which a div-and-a-click-handler would have. */
details.settings-section { padding: 0; }
details.settings-section > summary {
  /* The whole row is the target: a heading you have to hit precisely is worse than no heading. */
  display: flex; align-items: center; gap: 8px;
  padding: 15px 16px; min-height: 52px;
  font-size: 16px; font-weight: 600; color: #1e3a5f;
  cursor: pointer; list-style: none;
  -webkit-tap-highlight-color: transparent;
}
/* The default disclosure triangle is inconsistent between browsers and sits in the wrong place,
   so it is replaced with a caret that rotates. */
details.settings-section > summary::-webkit-details-marker { display: none; }
details.settings-section > summary::marker { content: ''; }
details.settings-section > summary::after {
  content: '⌄'; margin-left: auto; font-size: 18px; line-height: 1;
  color: #67748a; transform: translateY(-3px);
  transition: transform 0.15s ease;
}
details.settings-section[open] > summary::after { transform: rotate(180deg) translateY(2px); }
details.settings-section[open] > summary { border-bottom: 1px solid #eef1f4; }
details.settings-section > .section-body { padding: 4px 16px 16px; }
/* The h3 inside a section body (the second heading in Organization) keeps its own spacing, but
   must not look like a section header of its own. */
details.settings-section > .section-body h3 { font-size: 15px; }

/* What is inside, without opening it. */
.section-badge {
  margin-left: 8px; font-size: 12px; font-weight: 700;
  background: #eef1f4; color: #4a5666;
  padding: 2px 9px; border-radius: 999px; white-space: nowrap;
}

/* ---------- Silent alarm ---------- */
/* The banner sits under the header and PUSHES everything down rather than covering it, so the
   guest-pass countdown and the app content are both still reachable while an alarm is open. */
.alarm-banner {
  display: none; position: fixed; top: 56px; left: 0; right: 0; z-index: 45;
  align-items: center; justify-content: space-between; gap: 10px;
  background: #b3261e; color: #fff; padding: 10px 14px; cursor: pointer;
  box-shadow: 0 2px 8px rgba(0,0,0,0.25);
}
/* Acknowledged is amber, not green: somebody is dealing with it, which is not the same as over. */
.alarm-banner.acknowledged { background: #a8630b; }

/* ---------- People ----------
   One column on a phone, two side by side once there is room. Alan expects this screen on a
   computer — but he is on his phone when somebody leaves and their access needs removing today,
   so it stacks rather than being desktop-only. Same markup, one media query.

   The modal card already scrolls (max-height 88vh, overflow-y auto), so the wide layout gives each
   column its own scroll instead: picking somebody from a list of forty should not scroll the
   detail you are reading out of view. */
.people-wrap { display: flex; flex-direction: column; gap: 12px; }
.people-list { min-width: 0; }
.people-detail { min-width: 0; }
/* An address long enough to be its own paragraph — bartholomew.featherstonehaugh.the.third@… is a
   real row in Alan's data — pushes the whole column wider than its container and the panel scrolls
   sideways. Wrapped rather than truncated: an email is only useful in full, and the row is allowed
   to be two lines tall. Scoped to this list so it cannot change every other list in the app. */
.people-list .title, .people-list .muted { overflow-wrap: anywhere; }
.people-selected { background: #eef3f9; border-radius: 8px; }
.people-summary { font-size: 14px; color: #1f2933; margin: 4px 0 10px; }
.people-empty { padding: 8px 0; }
.people-block {
  border-top: 1px solid #e2e6ea; padding-top: 10px; margin-top: 12px;
  display: flex; flex-direction: column; gap: 6px;
}
.people-block h4 { margin: 0; font-size: 14px; color: #1e3a5f; }
.people-block select { width: 100%; }
.role-card {
  background: #f7f9fb; border-radius: 8px; padding: 10px 12px; margin: 8px 0; font-size: 13px;
}
.role-card ul { margin: 6px 0; padding-left: 18px; }
.role-card li { margin: 2px 0; }
.cap-person {
  border: 1px solid #e2e6ea; border-radius: 6px; padding: 8px 10px; margin: 6px 0;
  display: flex; flex-direction: column; gap: 4px; font-size: 14px;
}

@media (min-width: 900px) {
  .people-wrap { flex-direction: row; align-items: flex-start; gap: 20px; }
  /* 84vh matches .modal-card's own ceiling, less the header and padding above these columns. The
     two used to be capped at 66vh inside an 84vh card, which threw away almost a fifth of the
     height for no reason and made both columns scroll sooner than they had to — the cramped feeling
     in Alan's screenshots was as much this as it was the width. */
  .people-list { flex: 0 0 300px; max-height: calc(84vh - 116px); overflow-y: auto; }
  .people-detail { flex: 1 1 auto; max-height: calc(84vh - 116px); overflow-y: auto; }
}

/* One capability per block, separated rather than run together: each is a decision with its own
   advice underneath, and a flat list of selects would read as a form rather than as choices. */
.cap-row {
  border: 1px solid #e2e6ea; border-radius: 8px;
  padding: 12px 14px; margin: 10px 0; display: flex; flex-direction: column; gap: 6px;
}
.cap-row .cap-head { font-size: 15px; }
.cap-row select { width: 100%; }
.cap-row .muted { font-size: 13px; line-height: 1.45; }

/* The safety notes. Whitespace is PRESERVED — the owner wrote this as a document with headings and
   blank lines between them, and collapsing that into one grey paragraph would turn a page somebody
   is meant to read under pressure into a wall. pre-wrap keeps their line breaks and still wraps on
   a narrow phone; break-word stops a long [placeholder] pushing the card sideways. */
.safety-protocol-text {
  white-space: pre-wrap; overflow-wrap: break-word;
  background: #f7f9fb; border-left: 3px solid #1e3a5f; border-radius: 6px;
  padding: 12px 14px; margin: 10px 0; font-size: 14px; line-height: 1.55;
  max-height: 46vh; overflow-y: auto;
}
/* Monospace so the [square brackets] line up and stay obvious while being edited. */
.safety-editor {
  width: 100%; font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 13px; line-height: 1.5; white-space: pre;
}

/* Nobody has answered in 45 seconds. Deliberately the loudest thing in the app — a brighter red
   than the ordinary alarm banner, and it pulses, because the ordinary banner has by definition
   already failed to get anybody moving. Respects prefers-reduced-motion: the colour alone still
   carries it, and somebody who gets motion sick does not stop needing to see this. */
.alarm-banner.escalated { background: #b3000f; animation: alarm-pulse 1.4s ease-in-out infinite; }
@keyframes alarm-pulse {
  0%, 100% { background: #b3000f; }
  50%      { background: #e10a1c; }
}
@media (prefers-reduced-motion: reduce) {
  .alarm-banner.escalated { animation: none; background: #b3000f; }
}
/* "Waiting for Marcus" / "Anyone who can go, go now." Allowed to wrap, unlike the line above it:
   this one is an instruction and clipping it mid-word would be worse than a taller banner. */
.alarm-banner .alarm-waiting {
  font-size: 12px; font-weight: 600; opacity: 1;
  white-space: normal; overflow: visible; text-overflow: clip;
}
/* Left open a long time. Stops shouting so a forgotten alarm does not train people to ignore
   the banner — but stays visible, because it is still open. */
.alarm-banner.stale { background: #6b4a12; }
.alarm-banner-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.alarm-banner-main strong { font-size: 15px; }
.alarm-banner-main span { font-size: 13px; opacity: 0.95; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.alarm-banner .btn { margin: 0; flex: 0 0 auto; }
.btn-light { background: #fff; color: #b3261e; border: none; font-weight: 700; }

/* !important is load-bearing. #guest-pass-banner carries top:56px in an inline style attribute in
   index.html, and an inline style beats a stylesheet rule at any specificity — so without this
   the rule below is dead code. It WAS dead code, found 2026-08-17: during a silent alarm both
   banners sat at 56px and the alarm banner (z-index 45) covered the trial banner (z-index 40)
   completely, so a trial user lost their countdown for the duration of the emergency. */
body.alarm-active #guest-pass-banner { top: 116px !important; }
body.alarm-active #app-content { padding-top: 128px; }
body.alarm-active #app-content.has-guest-banner { padding-top: 176px; }

/* The button itself. Big enough to find without looking, and it fills as it is held so the
   2-second wait is visible rather than a guess. */
.panic-wrap { margin: 4px 0 18px; }
.panic-btn {
  position: relative; width: 100%; min-height: 132px;
  border: none; border-radius: 14px; background: #b3261e; color: #fff;
  cursor: pointer; overflow: hidden; padding: 0;
  /* A long press must not offer "copy"/"select all" over the top of the control. */
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent; touch-action: none;
}
.panic-btn.holding { background: #8c1d18; }
.panic-fill {
  position: absolute; top: 0; left: 0; bottom: 0; width: 0;
  background: rgba(255,255,255,0.28); pointer-events: none;
}
.panic-label { position: relative; display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 18px 12px; }
.panic-icon { font-size: 34px; line-height: 1; }
.panic-text { font-size: 19px; font-weight: 700; }
.panic-sub { font-size: 14px; opacity: 0.9; }
.panic-notice { font-size: 13px; color: #67748a; margin: 10px 2px 12px; line-height: 1.45; }
.emergency-heading { margin: 18px 0 8px; }

.alarm-sent { text-align: center; padding: 6px 0 10px; }
.alarm-sent-mark {
  width: 62px; height: 62px; margin: 0 auto 10px; border-radius: 50%;
  background: #1e7c3a; color: #fff; font-size: 34px; line-height: 62px;
}
.alarm-sent h2 { margin: 0 0 8px; }
.alarm-sent-warn {
  background: #fdeceb; border: 1px solid #f3c2be; border-radius: 8px;
  padding: 10px 12px; font-size: 14px; color: #7a1c16; text-align: left; margin-top: 12px;
}
/* On the "Alert sent" screen, when no area was set. Quiet — the alarm has gone and this is an offer,
   not a problem. Deliberately NOT styled like .alarm-sent-warn above: that one says nobody is watching
   this app, which is the sentence on that screen that must keep its weight. */
.alarm-sent-hint {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: center;
  font-size: 14px; color: #4a5568; margin-top: 10px;
}
.alarm-calls { margin: 10px 0; }
.alarm-detail-who { font-size: 17px; }
.alarm-detail-where { font-size: 16px; font-weight: 600; }
.alarm-detail-note { font-style: italic; background: #f4f6f8; border-radius: 8px; padding: 10px 12px; }
.alarm-detail-ack { color: #a8630b; font-weight: 600; }

/* The liability wording. Deliberately hard to skim past — this is the one block on the screen
   that is allowed to be visually loud without being decorative. */
.alarm-notice {
  background: #fdeceb; border: 1px solid #f3c2be; border-left: 5px solid #b3261e;
  border-radius: 8px; padding: 12px 14px; margin-bottom: 16px; font-size: 14px; line-height: 1.5;
}
.alarm-notice p { margin: 8px 0 0; }

.recipient-type { border-top: 1px solid #e2e6ea; padding: 10px 0; }
.recipient-type-head { font-size: 15px; margin-bottom: 4px; }
/* Colour carries the distinction Alan asked to be explicit about: green means the app really
   does something, grey means it does not and the number is there to be dialled by a human. */
.recipient-type-gets { font-size: 14px; font-weight: 600; padding: 6px 10px; border-radius: 6px; margin: 4px 0 6px; }
.recipient-type-gets.auto { background: #e7f4ea; color: #1e5c32; }
.recipient-type-gets.manual { background: #eef1f4; color: #4a5666; }

/* ---------- "A newer version is ready" ---------- */
/* Sits above the bottom nav rather than over the header, because the header is where the guest
   pass banner already lives and two stacked bars read as an error state.

   ⚠️ IT USED TO SIT BELOW THE DIM CURTAIN ON PURPOSE, and the reasoning was sound: "if the screen
   has gone dark to save battery, an update notice lighting part of it back up would defeat the
   point." Alan found the consequence on 2026-10-02 — with the curtain down the notice is simply
   gone, and this is the one app in the workspace where that happens; every other app's bar is at
   2147483000, miles above any curtain.

   Both halves of that are right, so neither is thrown away: the bar now comes THROUGH the curtain
   and is stripped back while it is there, which is exactly what crispy-copilot does with the speed
   and the destination. The standing rule is that an update must announce itself — a notice nobody
   can see is the same as no notice — and the battery argument is answered by dimming it rather than
   by hiding it. */
.pwa-update-bar {
  /* ⚠️ 74px clears the 68px nav; the inset clears the phone's buttons under it. Both, or the
     "a newer version is ready" bar lands on the nav it is meant to sit above. */
  display: none; position: fixed; left: 10px; right: 10px;
  bottom: calc(74px + env(safe-area-inset-bottom, 0px)); z-index: 60;
  align-items: center; gap: 10px;
  background: #1e3a5f; color: #fff; border-radius: 10px;
  padding: 10px 12px; font-size: 14px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.25);
}

/* ---------- the update bar, seen through the curtain ----------
   ⚠️ The z-index is raised ONLY while the curtain is down. Raising it permanently would put the
   bar above every modal and sheet in the app for the other 99% of the time, which is a different
   bug and a worse one — the layering above was chosen deliberately.

   Dimmer, not brighter, the same way every other carve-out in this workspace works: the lit navy
   panel and its shadow go, leaving an outline and muted text. That is a fraction of the light the
   full bar emits, which is the whole of the original objection. */
body.dimmed .pwa-update-bar {
  z-index: 10000;
  /* ⚠️⚠️ IT MUST NOT TAKE THE TAP THAT WAKES THE SCREEN (2026-10-11, swept from Daily Delivery's
     audit finding DD-43, which found the same hijack there).
     The block above reasoned entirely about LIGHT — and it is right about light — but raising the
     bar above the curtain also made it the thing a tap lands on. The curtain's own defences only
     cover a tap that lands on the curtain ITSELF, so a tap meant to bring the screen back pressed
     Update and reloaded the app: in this app that is mid-inspection, with whatever was on screen
     gone. Exactly the shape of birdhouse's audit I-71.
     So the bar stays READABLE through the curtain, which was the deliberate choice here and is
     worth keeping — somebody glancing at a dark phone should still see that an update is waiting —
     and the tap falls through to the curtain instead. This is the same answer as every other
     read-only carve-out in this workspace; crispy-copilot's ⚑ Mark this spot is the single one
     that goes the other way, because marking a run of signs is something you do WHILE the screen
     is dark. Do not copy that exception onto a thing that is only meant to be read. */
  pointer-events: none;
  background: transparent;
  border: 1px solid #39506e;
  box-shadow: none;
  color: #aab8cc;
}
body.dimmed .pwa-update-bar .btn {
  background: transparent;
  border: 1px solid #4a6a94;
  color: #c3d0e2;
}
body.dimmed .pwa-update-later { color: #7d8ca0; }
.pwa-update-bar span { flex: 1; min-width: 0; }
.pwa-update-bar .btn { margin: 0; flex: 0 0 auto; }
/* A real 44px target — dismissing this must not need a careful tap. */
.pwa-update-later {
  background: none; border: none; color: #cbd6e2; font-size: 17px;
  min-width: 44px; min-height: 44px; cursor: pointer; flex: 0 0 auto;
}

/* ---------- Bookings ---------- */
/* The grid has to answer "is Tuesday at 2 free?" at a glance on a phone, so the resource names
   stay pinned on the left while the hours scroll sideways under the thumb. */
.book-nav { display: flex; align-items: center; gap: 8px; margin: 10px 0 4px; }
.book-date { flex: 1; min-width: 0; border: 1px solid #d7dde3; border-radius: 7px; padding: 8px 10px; font-size: 14px; }
.book-daylabel { font-weight: 600; color: #2d3748; margin-bottom: 8px; }

/* ⚠️ THE SCROLLER IS THE WRAPPER, AND EXACTLY ONE OF THEM.
   .book-cells used to carry `overflow-x: auto` on every single row, header included, so a
   twelve-hour day of 34px columns showed about seven of them on a phone and each row slid
   independently of the times labelling it. Alan read that as a grid frozen at 11:00 — reasonably,
   since nothing about it looked scrollable and the later hours were simply not on screen.

   The border and radius live HERE rather than on .book-grid because .book-grid needed
   `overflow: hidden` to round its corners, and an overflow ancestor becomes the scroll container
   the sticky row heads stick to — a container that never scrolls, which is the same as no sticky
   at all. Radius on the scroller, no overflow on the grid, sticky works. */
.book-scroll { overflow-x: auto; border: 1px solid #e2e6ea; border-radius: 10px; background: #fff; }
.book-grid { width: max-content; min-width: 100%; }
.book-row { display: flex; border-bottom: 1px solid #eef1f4; }
.book-row:last-child { border-bottom: none; }
.book-head { background: #f4f7fa; }
/* Sticky so the room names stay readable while the hours slide under them — a name scrolled off
   the left leaves a wall of anonymous coloured blocks. Needs its own background or the cells
   show straight through it. */
.book-rowhead { width: 108px; flex-shrink: 0; padding: 8px 10px; border-right: 1px solid #e2e6ea;
  cursor: pointer; position: sticky; left: 0; z-index: 2; background: #fff; }
.book-head .book-rowhead { background: #f4f7fa; z-index: 3; cursor: default; }
.book-rowhead .title { font-size: 13px; font-weight: 600; }
.book-rowhead .muted { font-size: 11px; }
.book-cells { display: flex; }
.book-th { width: 34px; flex-shrink: 0; font-size: 10px; color: #8a94a0; padding: 6px 0 4px 2px; }

.book-cell { width: 34px; flex-shrink: 0; height: 42px; border-right: 1px solid #f0f3f6; position: relative; }
.book-cell.free { background: #fff; cursor: pointer; }
.book-cell.free:active { background: #e8f1f8; }
.book-cell.booked { background: #2b5876; cursor: pointer; }
.book-cell.maintenance { background: #b08300; cursor: pointer; }
.book-cell.buffer { background: repeating-linear-gradient(45deg, #f0f3f6, #f0f3f6 4px, #e2e6ea 4px, #e2e6ea 8px); }
.book-cell.closed { background: #f7e9e9; }
/* Escapes its own 34px cell so a real name is readable rather than one clipped letter. */
.book-cell-label { position: absolute; left: 4px; top: 4px; white-space: nowrap; color: #fff;
  font-size: 11px; font-weight: 600; pointer-events: none; z-index: 2; }

/* Week view: seven equal columns that fit a phone without scrolling, since each cell is a
   summary rather than a second hour-by-hour grid. */
.week-cells { display: flex; flex: 1; }
.week-th { flex: 1; text-align: center; font-size: 11px; color: #8a94a0; padding: 6px 0; line-height: 1.3; }
.week-th span { font-size: 13px; color: #4a5568; font-weight: 600; }
.week-th.today span { color: #2b5876; }
.week-cell { flex: 1; height: 44px; border-left: 1px solid #f0f3f6; display: flex;
  align-items: center; justify-content: center; font-size: 12px; font-weight: 600; cursor: pointer; }
.week-cell.free { background: #fff; color: #cbd3da; }
.week-cell.partly { background: #dce9f2; color: #2b5876; }
.week-cell.full { background: #2b5876; color: #fff; }
.week-cell.closed { background: #f7e9e9; color: #a05252; }

.book-key.partly { background: #dce9f2; }
.book-key.full { background: #2b5876; }
.book-key.closed { background: #f7e9e9; }

.report-ranges { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }

/* Shown only on a day with nothing booked — see the note on legendHTML. */
.book-hint { margin-top: 6px; font-size: 12px; line-height: 1.5; }
.book-legend { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-top: 10px; font-size: 12px; }
.book-key { display: inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid #d7dde3; }
.book-key.free { background: #fff; }
.book-key.booked { background: #2b5876; }
.book-key.maintenance { background: #b08300; }
.book-key.buffer { background: repeating-linear-gradient(45deg, #f0f3f6, #f0f3f6 3px, #e2e6ea 3px, #e2e6ea 6px); }

/* ---------- Office Suite (documents) ---------- */
/* A full-screen editor rather than the usual modal card: a memo is written, not filled in, and
   a phone keyboard already takes half the screen before any chrome is added. */
.doc-editor { position: fixed; inset: 0; background: #fff; display: flex; flex-direction: column; z-index: 120; }
.doc-editor-bar { flex-shrink: 0; display: flex; align-items: center; gap: 8px; padding: 8px 10px;
  border-bottom: 1px solid #e2e6ea; background: #fff; }
.doc-title-input { flex: 1; min-width: 0; border: 1px solid #d7dde3; border-radius: 7px;
  padding: 9px 10px; font-size: 15px; font-weight: 600; }
.doc-status { flex-shrink: 0; font-size: 11px; color: #8a94a0; min-width: 52px; text-align: right; }

/* Scrolls sideways rather than wrapping onto a second row — a wrapped toolbar silently pushes
   the writing area further down the screen every time the window narrows. */
.doc-toolbar { flex-shrink: 0; display: flex; align-items: center; gap: 3px; padding: 6px 8px;
  overflow-x: auto; background: #f4f7fa; border-bottom: 1px solid #e2e6ea; }
.doc-tb-btn { min-width: 36px; height: 36px; flex-shrink: 0; border: 1px solid #d7dde3; background: #fff;
  border-radius: 6px; font-size: 14px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
.doc-tb-btn.active { background: #2b5876; border-color: #2b5876; color: #fff; }
.doc-tb-wide { width: auto; padding: 0 10px; font-size: 12px; white-space: nowrap; }
.doc-tb-select { height: 36px; flex-shrink: 0; border: 1px solid #d7dde3; border-radius: 6px;
  background: #fff; font-size: 13px; padding: 0 6px; max-width: 120px; }
.doc-tb-sep { width: 1px; height: 22px; background: #d7dde3; margin: 0 3px; flex-shrink: 0; }

.doc-meta { flex-shrink: 0; display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 6px 12px; border-bottom: 1px solid #eef1f4; }
.doc-meta-label { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 12px; color: #4a5568; }
.doc-meta-select { border: 1px solid #d7dde3; border-radius: 6px; padding: 5px 8px; font-size: 13px; }

/* `-webkit-overflow-scrolling: touch` was removed from this rule and .doc-toolbar on 2026-08-18.
   It existed to turn on momentum scrolling on iOS BEFORE iOS 13; since iOS 13 (2019) momentum is
   the default and the property does nothing on any browser in use. Dead weight, not a fallback —
   nothing changes on any device by dropping it. */
.doc-body { flex: 1; overflow-y: auto; padding: 20px 18px 40px;
  font-family: Georgia, 'Times New Roman', serif; font-size: 16px; line-height: 1.75; color: #1a2733; outline: none; }
.doc-body h1 { font-size: 24px; } .doc-body h2 { font-size: 20px; } .doc-body h3 { font-size: 17px; }
.doc-body blockquote { border-left: 3px solid #cbd3da; margin-left: 0; padding-left: 14px; color: #5a6675; font-style: italic; }
.doc-body hr { border: none; border-top: 1px solid #d7dde3; margin: 14px 0; }
.doc-body ul, .doc-body ol { padding-left: 24px; }

/* A procedure being READ, not edited — opened from the "?" beside a checklist item.
   .doc-body is sized for a full-screen editor (flex:1, 40px of bottom padding, its own scroll).
   Inside a modal that produces a tall empty box under a short procedure, so the padding comes
   down and the scrolling is left to the modal, which already does it. */
.doc-readonly { flex: none; padding: 4px 2px 8px; max-height: 60vh; overflow-y: auto; }

.letterhead-preview { border: 1px solid #e2e6ea; border-radius: 8px; padding: 12px; background: #fff;
  overflow-x: auto; }

/* ---------- Emergency triage ---------- */
/* Deliberately loud. This is the one screen in the app where someone is under stress and needs
   the important thing to be unmissable, not tasteful. */
.emergency-banner {
  background: #fbe1de; color: #7d241a; border-left: 4px solid #c0392b;
  border-radius: 8px; padding: 12px 14px; margin: 12px 0; font-size: 14px; line-height: 1.45;
}
.emergency-banner strong { display: block; margin-bottom: 3px; }
.emergency-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 12px 0; }
/* Big targets on purpose — tapped in a hurry, possibly one-handed, possibly in the dark. */
.emergency-tile {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
  min-height: 104px; padding: 14px 10px; cursor: pointer;
  background: #fff; border: 1px solid #dbe2ea; border-radius: 12px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.08); color: #1f2933;
}
.emergency-tile:active { background: #eef2f6; }
.emergency-tile-icon { font-size: 34px; line-height: 1; }
.emergency-tile-label { font-size: 15px; font-weight: 600; text-align: center; }
/* The shut-off is the most time-critical thing on the page, so it reads as a heading, not a row. */
.shutoff-line { margin: 10px 0; font-size: 16px; line-height: 1.45; }
.shutoff-line strong { color: #1e3a5f; }
.shutoff-line.missing { color: #8a6d00; }
.triage-steps { margin: 8px 0 0; padding-left: 22px; }
.triage-steps li { margin: 8px 0; line-height: 1.45; font-size: 15px; }

/* ---------- Location selector ---------- */
/* Sticky rather than fixed, so it never fights the guest-pass banner for the same coordinates
   and needs no stacking-order arithmetic. It sits inside the scrolling content and parks itself
   under the header, which is exactly the behaviour wanted: visible while working down a long
   list, out of the way of nothing. */
.location-bar {
  position: sticky; top: 56px; z-index: 30;
  /* #app-content pads 68px down from the top and the header is 56px tall, so -12px closes that
     gap and the bar sits flush under the header from the very first paint — matching exactly
     where it parks once it sticks, so nothing visibly jumps on the first scroll. The negative
     side margins cancel the content padding to make it full-bleed. */
  margin: -12px -16px 12px; padding: 8px 16px;
  background: #eef2f6; border-bottom: 1px solid #dbe2ea;
}
/* The guest banner occupies the 40px directly under the header while it is showing. */
#app-content.has-guest-banner .location-bar { top: 96px; }
.location-select {
  width: 100%; min-height: 44px; margin: 0;
  font-size: 16px; font-weight: 600; color: #1e3a5f;
  background: #fff; border: 1px solid #cbd5e1; border-radius: 8px; padding: 10px 12px;
}

/* ---------- Assignee filter (work screens only) ---------- */
/* Not in the top bar beside Location: who a job belongs to only means something on the work
   screens, and on Vendors or Budget it would be half a phone screen of dropdown doing nothing. */
.assignee-bar { margin: 0 0 12px; }
.assignee-select {
  width: 100%; min-height: 44px; margin: 0;
  font-size: 16px; font-weight: 600; color: #1e3a5f;
  background: #fff; border: 1px solid #cbd5e1; border-radius: 8px; padding: 10px 12px;
}

/* ---------- My Schedule ---------- */
/* The calendar grid is ported from library/SwapManager.js, which has been in real use for
   months. What differs: the small glyph under each day number carries the HOURS rather than a
   driver's initial, and colour encodes what KIND of day it is — never how long it is. There is no
   standard workday in this app, so "shorter than normal" is not something it can know. */

.schedule-subject { display: block; margin: 0 0 10px; font-size: 14px; color: #67748a; }
.schedule-subject select {
  width: 100%; min-height: 44px; margin-top: 4px;
  font-size: 16px; color: #1f2933;
  background: #fff; border: 1px solid #cbd5e1; border-radius: 8px; padding: 10px 12px;
}

.schedule-toggle { display: flex; gap: 0; margin: 0 0 12px; }
.schedule-toggle button {
  flex: 1; min-height: 44px; padding: 10px;
  font-size: 15px; font-weight: 600; color: #4a5666;
  background: #fff; border: 1px solid #cbd5e1; cursor: pointer;
}
.schedule-toggle button:first-child { border-radius: 8px 0 0 8px; }
.schedule-toggle button:last-child { border-radius: 0 8px 8px 0; border-left: none; }
.schedule-toggle button.active { background: #1e3a5f; border-color: #1e3a5f; color: #fff; }

.sched-nav { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-bottom: 8px; }
.sched-nav strong { font-size: 16px; color: #1e3a5f; }
.sched-nav .btn { width: auto; margin: 0; padding: 8px 16px; }

.sched-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-bottom: 10px; }
.sched-dow { text-align: center; font-size: 12px; color: #67748a; padding-bottom: 2px; }

.sched-day {
  aspect-ratio: 1; border-radius: 8px; padding: 0; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  border: 1px solid #e2e8f0; background: #fff; color: #1f2933; font-size: 15px;
}
/* The hours figure. Deliberately tiny — it must not compete with the date, only answer
   "is this a short day?" at a glance so nobody has to tap to find out. */
.sched-day small { font-size: 10px; line-height: 1; margin-top: 2px; color: #67748a; }
.sched-out { opacity: 0.38; }
.sched-today { box-shadow: inset 0 0 0 2px #1e3a5f; font-weight: 700; }
.sched-selected { outline: 3px solid #1e3a5f; outline-offset: -1px; }

/* State, never duration. */
.sched-day.state-working  { background: #e8f0fb; border-color: #bcd4f0; }
.sched-day.state-covering { background: #e6f4ea; border-color: #b3ddc0; }
.sched-day.state-covered  { background: #fdf0e2; border-color: #f0d2a8; }
.sched-day.state-working small,
.sched-day.state-covering small,
.sched-day.state-covered small { color: #4a5666; }

.sched-key { font-size: 12px; color: #67748a; display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.sched-swatch { width: 12px; height: 12px; border-radius: 3px; display: inline-block; border: 1px solid #cbd5e1; }
.sched-swatch.state-working  { background: #e8f0fb; border-color: #bcd4f0; }
.sched-swatch.state-covering { background: #e6f4ea; border-color: #b3ddc0; }
.sched-swatch.state-covered  { background: #fdf0e2; border-color: #f0d2a8; }

.sched-hours { font-size: 15px; font-weight: 700; color: #1e3a5f; white-space: nowrap; padding-left: 8px; }
.sched-row-today { box-shadow: inset 3px 0 0 #1e3a5f; }
.sched-detail { border-left: 3px solid #1e3a5f; }

/* ---------- Pattern editor ---------- */

.sched-pattern { margin: 12px 0; }
.sched-pattern-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  padding: 10px 0; border-bottom: 1px solid #eef2f6;
}
.sched-pattern-row .checkbox-row { margin: 0; min-width: 92px; flex: 0 0 auto; }
.sched-pattern-row .btn { width: auto; margin: 0; }
.sched-block { display: flex; align-items: center; gap: 6px; }
/* Large enough to tap accurately one-handed, which is how these actually get set. */
.sched-block input[type="time"] {
  min-height: 44px; font-size: 16px; padding: 6px 8px;
  border: 1px solid #cbd5e1; border-radius: 8px; background: #fff; color: #1f2933;
}
.sched-block span { font-size: 14px; color: #67748a; }

/* ---------- Time off ---------- */
/* Two more day states. Both mean "not working", and they are kept apart because the difference
   matters to whoever is looking: `requested` is still a question, `timeoff` is a decision. */

.sched-day.state-timeoff   { background: #fdf0e2; border-color: #f0d2a8; }
/* Dashed, because a request that nobody has answered yet is not a fact about the week. */
.sched-day.state-requested { background: #fff; border-color: #f0d2a8; border-style: dashed; }
.sched-day.state-timeoff small,
.sched-day.state-requested small { color: #4a5666; }

.sched-swatch.state-timeoff { background: #fdf0e2; border-color: #f0d2a8; }

/* Request mode. Picked days are unmistakable — this is the one screen where getting the selection
   wrong means turning up on the wrong day. */
.sched-day.sched-picked {
  background: #1e3a5f; border-color: #1e3a5f; color: #fff; font-weight: 700;
}
.sched-day.sched-picked small { color: #cfe0f5; }
/* A day that cannot be picked is dimmed rather than hidden: seeing that Sunday is not selectable
   is more useful than wondering why tapping it does nothing. */
.sched-day.sched-locked { opacity: 0.34; cursor: default; }

/* ---------- Search ---------- */
/* Sits at the top of the dashboard because for a technician mid-job the search box IS the app.
   Renders nothing below itself until two characters are typed, so it costs everyone else one row. */

.search-wrap { position: relative; margin: 0 0 10px; }
.search-input {
  width: 100%; min-height: 48px; padding: 12px 40px 12px 14px;
  font-size: 16px; /* 16px exactly: anything smaller makes iOS zoom the page on focus */
  color: #1f2933; background: #fff;
  border: 1px solid #cbd5e1; border-radius: 10px;
  -webkit-appearance: none; appearance: none;
}
.search-input:focus { outline: 2px solid #1e3a5f; outline-offset: -1px; border-color: #1e3a5f; }
/* Chrome and Safari add their own clear affordance to type=search; ours is bigger and sits in a
   predictable place, so theirs is removed rather than shown alongside it. */
.search-input::-webkit-search-decoration,
.search-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

.search-clear {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  /* A full 40px target rather than a 16px glyph — this gets tapped one-handed. */
  width: 40px; height: 40px; padding: 0;
  font-size: 17px; line-height: 1; color: #67748a;
  background: none; border: none; border-radius: 8px; cursor: pointer;
}
.search-clear:active { background: #eef2f6; }

.chip-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 0 10px; }
.chip {
  min-height: 36px; padding: 7px 13px;
  font-size: 14px; font-weight: 600; color: #4a5666;
  background: #fff; border: 1px solid #cbd5e1; border-radius: 18px; cursor: pointer;
  white-space: nowrap;
}
.chip-on { background: #1e3a5f; border-color: #1e3a5f; color: #fff; }
.chip-sep { width: 1px; height: 22px; background: #dde3ea; margin: 0 2px; }

/* ---------- Work requests ----------
   "Urgent" is a word, not only a colour — same rule as the budget chip, and for the same reason:
   around one man in twelve has a red-green colour deficiency, and in a trades workforce that is a
   real number of people who would see an ordinary row. */
.wr-urgent {
  display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: 0.04em;
  text-transform: uppercase; color: #b42318; background: #fdf4f3;
  border: 1px solid #f3ccc8; border-radius: 4px; padding: 1px 5px; margin-right: 4px;
  vertical-align: 1px;
}
.badge-new { background: #eef3f8; border-color: #c5d5e6; color: #1e3a5f; font-weight: 700; }
/* A queue that has silently stopped being anybody's job looks exactly like one nobody has got to
   yet — so when the coordinator points at somebody who has left, it says so in the danger colour
   rather than in ordinary muted text. */
.wr-warn { color: #b42318; }
.wr-warn a { color: #b42318; }
.wr-free-head { font-size: 12px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
  color: #67748a; margin: 16px 0 6px; }
.wr-free { list-style: none; margin: 0; padding: 0; }
.wr-free li { display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 7px 0; border-bottom: 1px solid #f0f3f6; font-size: 14px; }
.wr-free li:last-child { border-bottom: none; }
.wr-free-name { color: #1e3a5f; font-weight: 600; }
/* Tabular so a column of counts lines up and the busy one is obvious at a glance. */
.wr-free-count { font-variant-numeric: tabular-nums; color: #67748a; font-weight: 700;
  min-width: 24px; text-align: right; }
/* The poster QR. Capped so it stays a square on a phone and does not push the link box off. */
.wr-qr { max-width: 240px; margin: 14px auto 4px; }
.wr-qr svg { width: 100%; height: auto; display: block; }

/* One line of a multi-item order, in the mark-as-ordered form. */
.ord-line { padding: 10px 0; border-bottom: 1px solid #f0f3f6; }
.ord-line:last-child { border-bottom: none; }
.ord-line-name { font-size: 14px; font-weight: 600; color: #1e3a5f; margin-bottom: 4px; }

/* ---------- The owner's view ----------
   Sits between the attention list and the button grid: attention is what needs doing now and
   outranks a summary, buttons are navigation and belong at the bottom. */
.ov-block { margin-top: 12px; }
/* ⚠️ THE TABLE SCROLLS INSIDE ITS OWN BOX. Five columns will not fit a phone, and the lesson from
   the booking grid is that the alternative is not "it fits" but "the page scrolls sideways" or,
   worse, a per-row scroller that slides the numbers away from their headings. */
.ov-scroll { overflow-x: auto; }
/* ⚠️ NO min-width. It was 340px, which forced a scrollbar the table did not need and left it
   overflowing a 390px phone by a few pixels — Alan, 2026-08-21: "that section has a little bit of
   horizontal scrolling while the other sections don't. The text was just a little bit more than
   what the screen can handle." A table that CLEARLY scrolls is fine; one that misses by six pixels
   reads as broken. It now takes the width it is given and .ov-scroll stays as the safety net for a
   genuinely long building name or a six-figure spend. */
.ov-table { border-collapse: collapse; width: 100%; }
.ov-table th { text-align: left; font-size: 11px; font-weight: 700; letter-spacing: 0.04em;
  text-transform: uppercase; color: #8a94a0; padding: 6px 6px; white-space: nowrap; }
.ov-table td { padding: 9px 6px; border-top: 1px solid #f0f3f6; font-size: 14px;
  font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The spend column carried the money AND its month-on-month delta on one line, which made it the
   widest thing on the screen by a distance. Stacking the delta underneath costs a few pixels of
   height and buys about forty of width — and reads better, which is the usual way round for this
   kind of fix. Scoped to the table: beside the figure is right on the area cards. */
.ov-table .ov-move { display: block; margin-left: 0; margin-top: 1px; }
.ov-table tbody tr { cursor: pointer; }
.ov-table tbody tr:active { background: #eef2f6; }
.ov-name { font-weight: 600; color: #1e3a5f; white-space: normal !important; min-width: 92px; }
.ov-note { margin: 4px 12px 12px; }

.ov-good { color: #1e7a3c; font-weight: 700; }
.ov-warn { color: #8a6d00; font-weight: 700; }
.ov-bad { color: #b42318; font-weight: 700; }
.ov-quiet { color: #98a2b3; }
.ov-plain { color: #1e3a5f; font-weight: 700; }
/* Inspections with no schedule, stacked under the compliance figure. Amber rather than red: an
   inspection nobody has scheduled is not late, it is unmeasured, and sending somebody to inspect a
   thing that may have been done last week is its own kind of wrong. Block for the same reason as
   .ov-move above — this table has five pixels of width spare and plenty of height. */
.ov-unsched { display: block; font-size: 11px; font-weight: 700; color: #8a6d00; margin-top: 1px; }
/* Movement against last month. Small, because it is context for the figure rather than the figure. */
.ov-move { font-size: 11px; font-weight: 700; margin-left: 4px; }
.ov-move.good { color: #1e7a3c; }
.ov-move.bad { color: #b42318; }

.ov-areas { display: grid; gap: 10px; margin: 12px 0 4px; }
.ov-area { background: #fff; border-radius: 10px; padding: 12px 14px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
.ov-area-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.ov-area-title { font-size: 14px; font-weight: 700; color: #1e3a5f; }
.ov-figs { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin: 10px 0 4px; }
.ov-fig { min-width: 0; }
.ov-fig-val { font-size: 18px; line-height: 1.2; overflow-wrap: break-word; }
.ov-fig-label { font-size: 11px; color: #67748a; margin-top: 2px; line-height: 1.3; }

/* ⚠️ THE CONTACT ROW IS THE POINT OF THE CARD, not a footer on it. A number an owner cannot act on
   is just anxiety; a number with a name and a phone beside it is a ten-second decision. */
.ov-lead { border-top: 1px solid #f0f3f6; margin-top: 10px; padding-top: 10px; }
.ov-lead-who { font-size: 13px; color: #333; margin-bottom: 8px; }
.ov-lead-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.ov-lead-empty { display: flex; align-items: center; justify-content: space-between;
  gap: 8px; font-size: 13px; }

/* An area nobody covers. The one state on this screen worth raising — an overlap costs nothing,
   a gap is silent until it matters. Amber rather than red: it is a thing to sort out, not a fault. */
.ov-gap { background: #fdfaf0; border: 1px solid #f0e3b8; border-radius: 10px;
  padding: 12px 14px; margin: 12px 0; }
.ov-gap-head { font-size: 13px; font-weight: 700; color: #8a6d00; }
.ov-gap-list { font-size: 14px; color: #4a5666; margin: 4px 0 10px; line-height: 1.4; }
.ov-mine { margin-top: 4px; }

/* "What I cover" — the manager's own checklist, and the owner's version of the same store. */
.mya-row { padding: 12px 0; border-bottom: 1px solid #f0f3f6; }
.mya-row:last-child { border-bottom: none; }
.mya-head { font-size: 15px; color: #1e3a5f; }
.mya-head .checkbox-row { margin: 0; }
.mya-scope { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 8px 0 0; }
.mya-chip { display: inline-flex; align-items: center; gap: 5px; font-size: 13px;
  background: #eef3f8; border: 1px solid #c5d5e6; color: #1e3a5f;
  border-radius: 14px; padding: 4px 6px 4px 10px; }
/* 28px so it is a real target on a phone, not a 10px cross beside a word. */
.mya-x { width: 24px; height: 24px; border: none; background: none; color: #67748a;
  font-size: 13px; line-height: 1; cursor: pointer; padding: 0; border-radius: 50%; }
.mya-x:active { background: #d7e3ef; }
.mya-add { font-size: 13px; padding: 5px 8px; max-width: 190px; }
/* The hint, never a block — somebody else covering the same thing is a real arrangement. */
.mya-others { font-size: 12px; color: #67748a; margin-top: 6px; line-height: 1.4; }
.mya-gap { font-size: 12px; color: #8a6d00; margin-top: 6px; font-weight: 600; }

/* ---------- Compliance & reports ----------
   Two figures side by side at the top, because this screen exists to answer two questions and
   both answers are a single number. A dash is a legitimate value here and must not look broken —
   see the note on printBoardPack about why a figure that cannot be honestly computed is never
   drawn as a nought. */
.cmp-figures { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 12px 0; }
.cmp-fig { background: #fff; border: 1px solid #e2e6ea; border-radius: 10px; padding: 12px 10px;
  text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.06); min-width: 0; }
.cmp-fig-val { font-size: 26px; font-weight: 700; line-height: 1.1; color: #1e3a5f;
  font-variant-numeric: tabular-nums; }
.cmp-fig-val.good { color: #1e7a3c; }
.cmp-fig-val.warn { color: #8a6d00; }
.cmp-fig-val.bad { color: #b42318; }
.cmp-fig-val.quiet { color: #98a2b3; }
.cmp-fig-label { font-size: 12px; font-weight: 600; color: #4a5666; margin-top: 4px; line-height: 1.3; }
.cmp-fig-sub { font-size: 11px; color: #8a94a0; margin-top: 2px; line-height: 1.3; }

/* The inspection checklist on the log form. Big rows: this is ticked standing at a fire panel
   with a phone in one hand, not at a desk. */
.chk-list { list-style: none; margin: 0 0 12px; padding: 0; }
.chk-list li { display: flex; align-items: center; gap: 10px; padding: 10px 0;
  border-bottom: 1px solid #f0f3f6; }
.chk-list li:last-child { border-bottom: none; }
.chk-list input[type="checkbox"] { width: 22px; height: 22px; flex-shrink: 0; margin: 0; }
.chk-list label { margin: 0; font-size: 14px; font-weight: 400; color: #333; line-height: 1.4; }

/* "How many did this job use", sitting at the end of a parts row on the log form. Narrow enough
   that the part's name and stock level keep the width, wide enough to stay a real touch target —
   this is filled in on a phone, standing at the machine, with gloves on. */
.parts-used-qty { width: 72px; flex-shrink: 0; margin: 0; text-align: center;
  min-height: 44px; font-size: 16px; }

/* ---------- Parts on a machine ---------- */
/* "None in stock" on a part a technician is about to go and fit is the one line on that record
   that must not read as ordinary muted text — it is the difference between doing the job and
   driving back. Same red as every other danger state in this app. */
.muted.danger-text { color: #c0392b; }
/* ⚠️ `.danger-text` previously had NO rule of its own — it only took a colour when it happened to
   sit alongside `.muted`. Anywhere else (a grid cell, a list row) the class was inert and the text
   simply rendered black, which is the worst possible failure for a class whose entire job is to
   say "this one is a problem". The combined rule above still wins where both are present.
   `.warn-text` is the amber step below it, for a wait that is worth a question but not yet a
   problem. */
.danger-text { color: #c0392b; }
.warn-text { color: #b9770e; }


/* ---------- Scanning ---------- */
/* The viewfinder is deliberately large: a barcode on a box held at arm's length needs enough
   pixels to resolve, and a postage-stamp preview makes people hold the phone too close for the
   camera to focus. */
.scan-wrap {
  position: relative; width: 100%; aspect-ratio: 4 / 3;
  background: #000; border-radius: 10px; overflow: hidden; margin-bottom: 10px;
}
.scan-wrap video { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A framing guide rather than a crop — BarcodeDetector reads the whole frame, so this only tells
   somebody where to aim. Pretending it is a scan window would be a lie. */
.scan-wrap::after {
  content: ''; position: absolute; left: 12%; right: 12%; top: 28%; bottom: 28%;
  border: 2px solid rgba(255,255,255,0.85); border-radius: 8px; pointer-events: none;
}


/* The scanned number, across the viewfinder. A signal that only says "something happened" answers
   half the question; the digits answer "did it get the RIGHT one?" — which is what somebody
   scanning a shelf of similar boxes actually needs. */
.scan-flash {
  position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%);
  /* Above the framing guide. ::after counts as the LAST child for painting, so without this the
     guide's white outline is drawn straight across the banner. */
  z-index: 2;
  background: #1e7c3a; color: #fff;
  font-family: "Courier New", monospace; font-size: 30px; font-weight: 700;
  letter-spacing: 1px; text-align: center; padding: 14px 8px;
  opacity: 0; pointer-events: none;      /* never blocks a tap on what is underneath */
  word-break: break-all; line-height: 1.1;
}
.scan-flash.show { opacity: 1; }
/* The whole frame, not a hairline border — the old version was too easy to miss entirely. */
.scan-wrap.scan-hit { box-shadow: inset 0 0 0 10px #1e7c3a; }
.scan-wrap.scan-hit::after { border-color: #35c46a; }

/* And where the code is listed afterwards, it is the thing being checked against the box — so it
   is readable at a glance rather than grey four-point filler. */
.scan-code {
  font-family: "Courier New", monospace; font-size: 20px; font-weight: 700;
  color: #1f2933; background: #eaf6ee; border: 1px solid #b3ddc0; border-radius: 8px;
  padding: 8px 10px; margin: 6px 0; word-break: break-all; text-align: center;
}
.scan-bad { color: #c0392b; }


/* ---------- Stock Up (mass stocking) ---------- */

/* Sections start closed: 150 rows opened at once is a wall, and the whole point of grouping is to
   turn one long job into six short ones somebody can do across several visits. */
.ms-group { border: 1px solid #e2e8f0; border-radius: 10px; margin-bottom: 8px; overflow: hidden; }
.ms-group > summary {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 14px 14px; cursor: pointer; background: #f7fafc; font-size: 16px;
  list-style: none;
}
.ms-group > summary::-webkit-details-marker { display: none; }
.ms-group > summary::after { content: '▾'; color: #67748a; }
.ms-group[open] > summary::after { content: '▴'; }
.ms-progress { color: #67748a; font-size: 13px; font-weight: 600; white-space: nowrap; }

.ms-row { border-top: 1px solid #eef2f6; }
/* The whole row is the target, not the 20px checkbox — this gets used one-handed on a phone while
   the other hand is holding a box. */
.ms-label {
  display: flex; align-items: center; gap: 12px;
  padding: 14px 14px; min-height: 48px; cursor: pointer;
}
.ms-label input[type="checkbox"] { width: 22px; height: 22px; flex: 0 0 auto; }
.ms-name { flex: 1 1 auto; }
.ms-unit { color: #67748a; font-size: 13px; white-space: nowrap; }

/* Revealed by a class, never by rebuilding the row. Rebuilding is what would throw away every
   quantity already typed — see the note at the top of UIMassStock.js. */
.ms-detail { display: none; padding: 0 14px 14px 48px; }
.ms-detail.open { display: block; }
.ms-detail input {
  width: 100%; box-sizing: border-box; margin-bottom: 8px;
  padding: 12px; font-size: 16px;   /* 16px or iOS zooms the whole page on focus */
  border: 1px solid #cbd5e0; border-radius: 8px;
}
.ms-hint { color: #67748a; font-size: 13px; margin: 0; }

/* Already stocked. Kept on the list rather than removed, so a second visit shows progress instead
   of a list that has mysteriously shrunk. */
.ms-have { display: flex; align-items: center; gap: 12px; padding: 12px 14px; color: #8b97a8; }
.ms-tick { color: #1e7c3a; font-weight: 700; width: 22px; text-align: center; }

/* Sticky inside the modal card, which is the scrolling container — the count and the button have
   to stay reachable from the middle of a long section. */
.ms-footer {
  position: sticky; bottom: 0; z-index: 2;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 12px 0 4px; margin-top: 10px;
  background: #fff; border-top: 1px solid #e2e8f0;
}
.ms-footer span { color: #67748a; font-size: 14px; }

/* A barcode, a part number, a serial — anything read off a box and compared character by
   character. JobSheet.js and Labels.js each define their own copy because they build standalone
   print documents with their own <style>; this is the one for the app itself, which was missing
   until a barcode had somewhere to be displayed. */
.mono { font-family: "Courier New", monospace; letter-spacing: 0.5px; }
.mono.small { font-size: 13px; }

/* The "how do I copy the address?" note on the supply form. Collapsed by default: it is the
   answer to a question somebody asks once and never again, so it must be findable without being
   in the way of the person who already knows. */
.how-to-copy { margin: 4px 0 10px; }
.how-to-copy > summary {
  cursor: pointer; color: #1e3a5f; font-size: 14px; padding: 6px 0; min-height: 32px;
}
.how-to-copy p { margin: 6px 0; }

/* ---------- Purchasing queue: grouped by supplier ---------- */
/* A buyer places one order per supplier, so the supplier is the heading and the items belong to
   it — a flat list makes them read the whole thing once per supplier to find its lines. */
.supplier-block { margin-bottom: 18px; }
.supplier-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 8px 4px; border-bottom: 2px solid #1e3a5f; margin-bottom: 4px;
}
.supplier-head strong { color: #1e3a5f; font-size: 16px; }
.queue-links { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.queue-links .btn { flex: 0 0 auto; }

/* ---------- Order placed ---------- */
.order-line {
  display: flex; align-items: center; gap: 10px; margin: 0;
  padding: 8px 0; border-bottom: 1px solid #eef2f6;
}
.order-where { flex: 1 1 auto; font-size: 15px; color: #1f2933; }
.order-qty {
  width: 84px; flex: 0 0 auto; margin: 0; text-align: center;
  font-size: 16px;   /* 16px or iOS zooms the page on focus */
}

/* On order — the loudest thing on a queue line when it applies, because the whole point is that
   the next buyer does not order it a second time. */
.on-order {
  display: inline-block; background: #fdf2d0; color: #8a6d00;
  border-radius: 10px; padding: 1px 8px; font-size: 12px; font-weight: 600;
}
/* Budget: a warning, never a block. Amber rather than red — over budget is a fact to know about,
   not an error somebody made. */
.budget-warn {
  background: #fdf2d0; color: #6b5500; border-left: 4px solid #d4a017;
  padding: 10px 12px; border-radius: 6px; margin: 8px 0; font-size: 14px;
}

/* Queue controls: how the buyer arranges the list, and how far ahead they buy. */
.queue-controls { display: flex; gap: 12px; flex-wrap: wrap; margin: 8px 0; }
.queue-controls label { flex: 1 1 160px; margin: 0; font-size: 13px; }

/* Why a quantity was suggested. Small and beside the branch name — a pre-filled number nobody can
   account for is the same lie as a hard-coded one, just more confident. */
.order-why { display: block; font-size: 12px; color: #67748a; font-weight: 400; }

/* A line the horizon brought in is not urgent, and must not read as if it were. */
.queue-branch-soon { opacity: 0.75; }
.queue-soon {
  display: inline-block; background: #e7ebf0; color: #4a5568;
  border-radius: 10px; padding: 1px 8px; font-size: 12px; font-weight: 600;
}

/* How long a line has been sitting on the queue. Quiet at first — a queue that comments on a
   one-day-old line is a queue people stop reading. */
.queue-waited {
  display: inline-block; background: #e7ebf0; color: #4a5568;
  border-radius: 10px; padding: 1px 8px; font-size: 12px; font-weight: 600;
}
.queue-waited-long { background: #fbe1de; color: #a5342a; }

/* ---------- Notice board ---------- */
.notice-board { margin: 0 0 14px; display: flex; flex-direction: column; gap: 8px; }
.notice {
  background: #fff; border: 1px solid #e2e6ea; border-left: 4px solid #cbd3da;
  border-radius: 8px; padding: 10px 12px;
}
/* Unread gets the accent edge, not a different background — a board where half the cards are a
   different colour reads as two kinds of notice rather than as "these are the new ones". */
.notice-new { border-left-color: #1e3a5f; }
.notice-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.notice-head strong { font-size: 15px; color: #1f2933; }
.notice-dot {
  background: #1e3a5f; color: #fff; border-radius: 10px;
  padding: 1px 8px; font-size: 11px; font-weight: 700;
}
.notice-auto {
  background: #e7ebf0; color: #67748a; border-radius: 10px; padding: 1px 8px; font-size: 11px;
}
.notice-body { font-size: 14px; color: #3d4852; margin-top: 4px; white-space: pre-wrap; }
.notice-meta { font-size: 12px; color: #8a94a0; margin-top: 6px; }

/* ---------- Covering for somebody ---------- */
/* Blue-grey, not amber. A cover is somebody's actual job this fortnight, not a warning state —
   the amber preview banner is the one that means "you should leave this mode". Still shown on
   every screen, because a person who has quietly gained manager powers and does not realise it is
   the one most likely to change something they did not mean to. */
.cover-banner {
  background: #e4ecf5; color: #1e3a5f; border-left: 4px solid #1e3a5f;
  padding: 10px 14px; border-radius: 8px; margin-bottom: 10px; font-size: 14px;
}
.cover-banner div { font-size: 12px; opacity: 0.85; margin-top: 2px; }
.cover-line { font-size: 13px; color: #1e3a5f; margin: 4px 0; }
.cover-line .btn-link { font-size: 13px; margin-left: 6px; }

/* ---------- Delegation ---------- */
/* Amber, not red: a gap in who-does-what is a thing to fix, not a failure that has happened yet.
   Shown only when there IS a gap — a permanent all-clear trains people to stop reading it. */
.delegation-gaps {
  background: #fdf2d0; color: #5c4a00; border-radius: 8px;
  padding: 12px 14px; margin: 0 0 12px; font-size: 14px;
}
.delegation-gaps ul { margin: 6px 0 6px 18px; padding: 0; }
.delegation-gaps li { margin: 4px 0; }
.delegation-gaps .muted { color: #6b5400; font-size: 12px; margin: 6px 0 0; }

/* ---------------- Access log ----------------
   Three states, and the colours are doing real work rather than decorating: a refused attempt
   involving somebody's own access is the one line a manager must not scroll past, an ordinary
   refusal is worth noticing, and a permitted action is deliberately the quietest thing on the
   screen because there will be far more of those than anything else. */
.alog-controls { display: flex; flex-wrap: wrap; gap: 12px; margin: 10px 0 4px; }
.alog-controls label { flex: 1 1 180px; min-width: 0; }

.alog-flag {
  background: #fdf2d0; color: #5c4a00; border-radius: 8px;
  padding: 12px 14px; margin: 0 0 12px; font-size: 14px;
}
.alog-flag ul { margin: 6px 0 0; }
.alog-flag li { margin: 4px 0; }
.alog-flag .muted { color: #6b5400; font-size: 12px; margin-top: 4px; }

/* The empty state is the normal state, so it reads as a result rather than a blank panel. */
.alog-clear {
  background: #e7f4ea; color: #1e4620; border-radius: 8px;
  padding: 10px 14px; margin: 0 0 8px; font-size: 14px;
}

.alog-list { margin-top: 8px; }
.alog-row {
  display: flex; gap: 10px; align-items: baseline;
  padding: 8px 4px 8px 10px; border-bottom: 1px solid #e2e6ea;
  border-left: 3px solid transparent; font-size: 14px;
}
.alog-when { flex: 0 0 84px; font-size: 12px; color: #6b7280; }
.alog-what { flex: 1 1 auto; min-width: 0; }
.alog-high    { border-left-color: #b42318; background: #fdf3f2; }
.alog-refused { border-left-color: #c47f00; }
.alog-ok      { border-left-color: #d3d8de; color: #4a5568; }

.alog-footer { margin-top: 14px; }
.alog-footer .muted { font-size: 12px; margin-top: 8px; }

@media (min-width: 900px) {
  .alog-list { max-height: 55vh; overflow-y: auto; }
}

.deleg-list { list-style: none; margin: 4px 0 0; padding: 0; font-size: 13px; color: #4a5568; }
.deleg-list li { margin: 2px 0; }
.deleg-area { font-weight: 600; color: #1e3a5f; }
/* Says plainly that this one shapes screens rather than locking data. Understated on purpose —
   it is a clarification, not a warning. */
.deleg-soft {
  background: #e7ebf0; color: #67748a; border-radius: 8px;
  padding: 0 6px; font-size: 11px; white-space: nowrap;
}

/* ---------- Org chart ---------- */
/* The staff list IS the chart: indentation comes from an inline padding-left per row, so these
   only style the twisty and the warnings. */
.org-twisty {
  background: none; border: none; cursor: pointer; padding: 0;
  color: #67748a; font-size: 13px; width: 18px; text-align: left;
  /* A hit target this small fails on a phone, so it is widened by a transparent pseudo-element
     rather than by making the glyph itself bigger and pushing the name around. */
  position: relative;
}
.org-twisty::after { content: ''; position: absolute; top: -12px; left: -8px; right: -8px; bottom: -12px; }
.org-twisty-gap { display: inline-block; width: 18px; flex: 0 0 auto; }

/* ⚠️ The name line has to be a flex row, not a block.
   As a block the twisty is inline content followed by a long email that cannot wrap mid-word — so
   on a 390px phone at depth three the address overflowed and the twisty was pushed onto its own
   line ABOVE the name. Found by rendering it, not by reading it. The name now truncates with an
   ellipsis, which is the one behaviour that degrades gracefully at any depth. */
#staff-list .list-row-main .title { display: flex; align-items: center; gap: 4px; min-width: 0; }
#staff-list .list-row-main .title .org-name {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
/* The role selector competes with the name for the same row. Capped so the name keeps a usable
   share of it rather than the select growing to fit its longest option. */
#staff-list > .list-row > select { max-width: 108px; flex: 0 0 auto; }

.org-reports-to { display: block; font-size: 13px; color: #67748a; margin-top: 4px; }
.org-reports-to select {
  font-size: 13px; padding: 4px 6px; min-height: 32px; margin-left: 4px;
  /* Bounded to the row it sits in. Email addresses are long and a select does not ellipsis its
     own text, so an unbounded one runs past the edge of an indented row. */
  max-width: 100%;
}

/* Loud, because the silent version of this is a deleted manager quietly reparenting a whole team
   with nobody ever finding out why the chart looks wrong. */
.org-issue {
  background: #fdf2d0; color: #6b5400; border-radius: 6px;
  padding: 6px 8px; margin: 4px 0; font-size: 12px;
}

/* On the record itself, where somebody has opened it specifically to ask "where is it?" — so it
   is louder here than the row chip, which has a badge competing with it. */
.detail-storage {
  background: #eef2f6; color: #1e3a5f; font-weight: 600;
  border-radius: 8px; padding: 8px 12px; margin: 8px 0; font-size: 15px;
}

/* Where a supply is kept, on the stock row. Quiet — it is a wayfinding aid, not a status: the
   badge on the right is the thing that should still win the eye. */
.storage-chip {
  display: inline-block; margin-top: 3px;
  background: #eef2f6; color: #4a5568;
  border-radius: 10px; padding: 1px 8px; font-size: 12px;
}

/* ---------- View as: the banner ---------- */
/* Deliberately loud. The preview changes what the whole app shows, and somebody who forgets they
   are in it will report every hidden feature as a bug. Amber rather than red: this is a mode, not
   an error. */
.preview-banner {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  background: #d4a017; color: #1f2933;
  padding: 10px 14px; border-radius: 8px; margin-bottom: 10px;
  font-size: 14px;
}
.preview-banner div div { font-size: 12px; opacity: 0.85; margin-top: 2px; }
.preview-banner .btn { background: #1f2933; color: #fff; flex: 0 0 auto; }

/* ==========================================================================
   DESKTOP — the bottom bar becomes a left rail
   ==========================================================================

   Everything above this line is the phone layout, and it stays the phone layout: this app is
   mobile-first by house rule and nothing here changes below 900px.

   Why it was needed: this stylesheet had no @media query of any kind, so a desktop browser got
   the phone design stretched to full width — eleven navigation buttons pinned to the bottom edge
   of a 27-inch monitor, as far from the pointer as it is possible to put them, in a bar that
   scrolls sideways because it was built for a thumb.

   ONE navigation, repositioned. The same <nav>, the same buttons, the same data-screen
   attributes and the same handlers — only CSS. A second desktop-only nav in the markup would be
   two lists to keep in step, and they would fall out of step the first time a module is added.

   ⚠️ This buys navigation headroom on DESKTOP, which is not where the ceiling actually bites.
   Spec §3m is about the phone bar, and the phone bar still holds eleven buttons. Unresolved.

   Everything grouped here rather than scattered through the file, so the desktop layout can be
   read (or removed) in one piece and always cascades after the rule it overrides. */
@media (min-width: 900px) {

  /* The rail sits BELOW the full-width top bar rather than beside it, so the title and the
     settings gear stay exactly where they already are. */
  .bottom-nav {
    top: 56px; bottom: 0; left: 0; right: auto;
    width: 212px; height: auto;
    flex-direction: column;
    padding: 8px 0;
    border-top: none; border-right: 1px solid #ddd;
    overflow-x: hidden; overflow-y: auto;
    scroll-snap-type: none;   /* horizontal snapping means nothing in a vertical list */
  }

  /* Icon beside the label, not stacked above it. A rail is read as a list of words. */
  .bottom-nav button {
    flex: 0 0 auto; width: 100%; min-width: 0; min-height: 46px;
    flex-direction: row; justify-content: flex-start;
    gap: 12px; padding: 0 16px;
    font-size: 14px; text-align: left;
    scroll-snap-align: none;
  }
  .bottom-nav button .nav-icon { font-size: 19px; width: 24px; text-align: center; flex: 0 0 auto; }
  .bottom-nav button .nav-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .bottom-nav button:hover { background: #f0f3f7; }   /* a pointer exists here; on a phone it does not */

  /* Colour alone carried "which screen am I on" in a bar of six words. In a rail of eleven it is
     too quiet, and it is invisible to anyone who cannot separate those two blues. */
  .bottom-nav button.active { background: #eaf0f7; box-shadow: inset 3px 0 0 #1e3a5f; }

  /* Content clears the rail: 212px of it, plus a 24px gutter.

     Longhand rather than the padding shorthand because only three sides change — padding-top is
     already right and is owned by the two rules above (68px normally, 128px under
     body.alarm-active). Restating it here would put the header height in a second place to keep
     in step. Checked rather than assumed: body.alarm-active #app-content is specificity (0,1,1)
     and outranks this (0,1,0), so the alarm case survives either way — the shorthand would NOT
     have broken it. Longhand is the tidier choice, not a bug fix. */
  #app-content {
    padding-left: 236px; padding-right: 28px; padding-bottom: 28px;
  }

  /* Both banners are fixed with left:0/right:0 and would otherwise run under the rail. */
  .alarm-banner { left: 212px; }

  /* !important is load-bearing: this element carries left:0 in an inline style attribute in
     index.html, and an inline style beats a stylesheet rule at any specificity. Remove the
     !important and the banner slides under the rail. */
  #guest-pass-banner { left: 212px !important; }

  /* A sheet sliding up from the bottom edge is a phone gesture. On a monitor it is just a card
     stuck to the floor, so it centres and rounds all four corners. */
  .modal-backdrop { align-items: center; padding: 24px; }
  .modal-card { border-radius: 14px; max-height: 84vh; }

  /* ⚠️ THIS IS THE HALF THAT WAS MISSING. People and the access log lay themselves out in two
     columns from 900px up, but they were doing it inside the 480px card above — a 300px list and
     roughly 120px of detail beside it, scrolling sideways. Alan's screenshot, 2026-08-18.
     1040px rather than "as wide as the window": past about that, the list and the detail end up so
     far apart that reading one while pointing at the other is a head-turn. */
  .modal-card-wide { max-width: 1040px; }
}

/* ==========================================================================
   THE SHEET VIEW — DataGrid.js / UISheets.js
   ==========================================================================

   Alan, 2026-08-21: "there should be a spreadsheet like look… each piece of equipment on a row…
   and this would be desktop or laptop prioritized."

   ⚠️ THE SCROLLER IS THE WRAPPER, NOT THE ROWS. .book-cells once carried `overflow-x: auto` on
   every row including the header, so each row scrolled independently and the columns fell out of
   line with their own headings. One scroller around the whole table is what keeps a grid a grid.

   ⚠️ AND THAT SCROLLER IS WHY NOTHING ABOVE IT MAY SET `overflow`. position: sticky resolves
   against the nearest scrolling ancestor: give .grid-wrap an overflow of its own and both the
   frozen header and the frozen first column silently stick to the wrong box — they keep working,
   just to nothing. Same trap that would have killed the booking grid's row heads. */

.grid-wrap { margin: 10px 0 24px; }

.grid-bar {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 8px; margin-bottom: 8px;
}
.grid-bar-left { display: flex; align-items: baseline; gap: 10px; }
.grid-bar-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.grid-count { font-size: 13px; color: #67748a; }
.grid-hint { font-size: 12px; color: #67748a; margin: 6px 2px 0; }
.grid-empty { margin: 18px 2px; }

.grid-scroll {
  overflow-x: auto;
  border: 1px solid #e2e6ea; border-radius: 10px; background: #fff;
}

.grid-table { border-collapse: separate; border-spacing: 0; width: 100%; font-size: 13.5px; }

.grid-th {
  position: sticky; top: 0; z-index: 2;
  background: #f4f7fa; color: #47536b;
  text-align: left; font-weight: 700; font-size: 11.5px;
  letter-spacing: 0.04em; text-transform: uppercase;
  padding: 9px 10px; border-bottom: 1px solid #dbe2ea;
  /* -webkit- prefix first: Safari and iOS Safari still need it, and a heading that selects its own
     text on a double-tap-to-sort is a small but constant irritation. */
  white-space: nowrap; cursor: pointer; -webkit-user-select: none; user-select: none;
}
.grid-th:hover { background: #eaf0f7; }
.grid-sorted { color: #1e3a5f; background: #eaf0f7; }

.grid-td {
  padding: 8px 10px; border-bottom: 1px solid #f0f3f6;
  color: #1f2933; vertical-align: top;
  /* Cells do NOT wrap: a grid is scanned down its columns, and one long note re-flowing a row to
     four lines pushes everything below it out of alignment with the eye. The full text is one
     click away in the record itself. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 320px;
}
.grid-tr:last-child .grid-td { border-bottom: none; }
.grid-tr:nth-child(even) .grid-td { background: #fbfcfd; }
.grid-clickable { cursor: pointer; }
.grid-clickable:hover .grid-td { background: #eef4fb; }

/* Numbers line up only if their glyphs are the same width. Without tabular-nums a column of
   prices is ragged and cannot be compared at a glance, which is most of why it is a column. */
.grid-right { text-align: right; font-variant-numeric: tabular-nums; }

.grid-name { font-weight: 600; color: #1e3a5f; }

.grid-drill-head { margin: 10px 0 4px; }
.grid-drill-head h2 { margin: 6px 0 2px; }
.grid-drill-head .btn { margin-top: 8px; }

/* Shown on phones only — see UISheets.narrowNoticeHTML for why this screen says so rather than
   pretending a twenty-column grid works on a 390px screen. */
.grid-narrow-note { display: block; }

@media (min-width: 900px) {
  /* The frozen first column. Only from 900px up: on a phone a sticky column eats a third of the
     width to hold still, which makes the scroll worse rather than better. */
  .grid-table .grid-td:first-child,
  .grid-table .grid-th:first-child {
    position: sticky; left: 0; z-index: 1;
    background: #fff; border-right: 1px solid #e8edf2;
  }
  .grid-table .grid-th:first-child { z-index: 3; background: #f4f7fa; }
  .grid-tr:nth-child(even) .grid-td:first-child { background: #fbfcfd; }
  .grid-clickable:hover .grid-td:first-child { background: #eef4fb; }

  /* The whole point of the screen: on a desk it uses the desk. */
  .grid-td { max-width: 460px; }
  .grid-narrow-note { display: none; }

  /* ⚠️ THE HEADER NEEDS THIS TO STICK AT ALL, and the reason is genuinely counter-intuitive.
     Measured in a real browser on 2026-08-21: the header scrolled 600px clean off the top.

     Setting `overflow-x: auto` FORCES `overflow-y` to compute to `auto` as well — the spec does
     not allow one axis to be visible while the other scrolls. So .grid-scroll was already the
     nearest scrolling ancestor, which is what `position: sticky; top: 0` resolves against. But it
     had no height limit, so it never scrolled vertically: the PAGE did, and the header rode along
     with it, stuck to a box that was itself moving.

     Capping the height makes the pane scroll instead of the page, which is what a spreadsheet
     does anyway — the headings stay put and the rows move under them. 230px is the top bar, the
     building selector, the role bar, the sheet tabs and the grid's own toolbar.

     This is why it is desktop-only: on a phone a fixed-height pane inside a scrolling page gives
     you two scrollbars fighting each other, and the phone does not get the frozen column either. */
  .grid-scroll { max-height: calc(100vh - 230px); overflow-y: auto; }
}

/* The Buy link inside a grid row. Small and quiet — it sits in a 74px column next to prices, and
   a full-size button would make every row taller for the sake of one cell. */
.grid-buy {
  display: inline-block; white-space: nowrap;
  padding: 2px 8px; border-radius: 12px;
  background: #eaf0f7; border: 1px solid #c5d5e6; color: #1e3a5f;
  font-size: 12px; font-weight: 600; text-decoration: none;
}
.grid-buy:hover { background: #dce7f3; }

/* The copy-the-part-number button, shown only to buyers, sitting immediately after the number in
   its cell. Deliberately faint until hovered: it is a second option beside the number, not a call
   to action, and every parts row carries two of them.

   ⚠️ The 28px min-width/height is the thumb target, not the glyph. The ⧉ is tiny, and on a phone
   this button sits inches from the row itself — an undersized target here means opening the machine
   when somebody meant to copy a number, which is the exact clumsiness this replaces. */
.grid-copy {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 28px; min-height: 28px; margin-left: 6px; padding: 0;
  background: transparent; border: 1px solid transparent; border-radius: 6px;
  color: #7a8b9c; font-size: 13px; line-height: 1; cursor: pointer;
  vertical-align: middle;
}
.grid-copy:hover { background: #eaf0f7; border-color: #c5d5e6; color: #1e3a5f; }
.grid-copy.copied { background: #e8f5ec; border-color: #b7dcc4; color: #1f6b3a; }

/* The "some sample records lost their label" repair notice. Amber rather than red: it is untidy
   data, not danger, and nothing it offers deletes anything. Disappears once nothing is orphaned. */
.orphan-notice {
  margin: 10px 0 14px; padding: 12px 14px;
  background: #fdfaf0; border: 1px solid #f0e3b8; border-radius: 10px;
}

/* One card per supplier on the purchasing sheet. The grid below it is the detail; these are the
   thing somebody acts on — one call or one visit each, rather than one per part. */
.vendor-cards { margin: 4px 0 18px; }
.vendor-card {
  padding: 12px 14px; margin-bottom: 10px;
  background: #fff; border: 1px solid #e2e6ea; border-radius: 10px;
}
/* Parts with nobody to buy them from are a gap to close, not a supplier to ring. */
.vendor-card-none { background: #fdfaf0; border-color: #f0e3b8; }
/* The plan picker beside "Viewing as". Two questions, two controls — who you are, and what the
   customer bought. Kept visually quieter than the role button: the role is what you are usually
   checking, the plan is the qualifier on it. */
.role-bar-plan { display: inline-flex; align-items: center; gap: 6px; margin: 0 0 0 auto;
  font-size: 12px; font-weight: 600; color: #5a6b7d; }
.role-bar-plan select { min-height: 36px; padding: 4px 8px; font-size: 13px; width: auto;
  margin: 0; }

/* ---------- Departments ---------- */
/* The name of the department you are in, above its tabs. Without it the tabs are just four more
   buttons and the reader is back to memorising a flat list — this is the line that turns a strip
   into a place. */
.dept-head { display: flex; align-items: center; gap: 8px; padding: 12px 16px 4px; }
.dept-icon { font-size: 20px; line-height: 1; }
.dept-name { font-size: 13px; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: #5a6b7d; }

/* Where a screen points at work that used to live on it. Reads as a signpost, not a tab, so it is
   not mistaken for the thing still being here. */
.dept-signpost { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  background: #f3f6f9; border-radius: 10px; padding: 10px 12px; margin: 0 0 10px; }

/* "Go to" — grouped by department rather than one flat grid of fifteen. */
.more-group { margin-bottom: 18px; }
.more-group:last-child { margin-bottom: 0; }
.more-group-head { margin: 0 0 8px; font-size: 12px; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: #5a6b7d; }

/* One tab, two layouts — the list works anywhere, the spreadsheet is for a desk. */
.layout-toggle { display: flex; gap: 6px; margin: 0 0 12px; }

/* Working in a sheet: the tick column and the bar that appears once something is ticked. */
.grid-tick { width: 44px; text-align: center; padding-left: 8px; padding-right: 8px; }
.grid-tick input[type="checkbox"] { width: 20px; height: 20px; margin: 0; vertical-align: middle; }
.grid-actions {
  display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  padding: 10px 14px; background: #eef4fd; border-bottom: 1px solid #d6e2f5;
}
.grid-actions-count { font-weight: 600; color: #1f4b8e; }
.grid-actions-btns { display: flex; gap: 8px; flex-wrap: wrap; }

.vendor-card-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
/* The head opens that supplier's own order — one order per supplier is the unit of work, so it is
   the unit the drill-down uses. Only the HEAD is tappable: the card also carries phone, website and
   Buy links, and making the whole card a target would fight every one of them. */
.vendor-card-open { cursor: pointer; margin: -4px -6px; padding: 4px 6px; border-radius: 8px; min-height: 44px; }
.vendor-card-open:hover { background: #f3f6f9; }
.vendor-card-open:focus-visible { outline: 2px solid #2d6cdf; outline-offset: 2px; }
.vendor-card-note { margin: 6px 0 0; }
.vendor-card-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.vendor-card-links { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }

/* Asking purchasing for something, on the supply record. */
.request-block { padding: 12px 14px; border-radius: 10px; margin-bottom: 4px; }
.request-block-on { background: #eef4fb; border: 1px solid #c5d5e6; }

/* Who asked for a line on the purchasing queue, and why. Quiet — it is context for the buyer,
   not a second headline competing with the quantity. */
.queue-asked { display: inline-block; margin-left: 6px; font-size: 12px; color: #67748a; }
.queue-note { display: block; margin: 2px 0 0; font-size: 12.5px; color: #47536b; font-style: italic; }

/* The Ask button in a grid row. Shares .grid-buy's look so the column reads consistently whether
   it offers a link (a buyer) or an action (everyone else) — but it is a <button>, so it needs the
   font and border reset that an <a> does not. */
button.grid-buy { font-family: inherit; cursor: pointer; }

/* ── Plan cards: the paywall's tier picker, and Settings → Plan's upgrade offers ──────────────
   Two places, one look, because they answer the same question — what do I get for what. Kept as
   plain stacked cards rather than a pricing table: this renders inside a modal on a phone, and a
   three-column table there is either unreadable or sideways-scrolling. */
.paywall-tiers { display: flex; flex-direction: column; gap: 10px; margin: 14px 0; text-align: left; }

.paywall-tier, .upgrade-card {
  border: 1px solid #dbe2ea; border-radius: 10px; padding: 12px 14px; background: #fff;
}
/* The one we steer people to. A border rather than a colour wash, so it reads as emphasis and
   not as "already selected". */
.paywall-tier.recommended { border-color: #2f6fb2; border-width: 2px; padding: 11px 13px; }

.paywall-tier-head, .upgrade-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.paywall-tier-name, .upgrade-name { font-weight: 700; font-size: 16px; }
.paywall-tier-price, .upgrade-price { font-weight: 700; font-size: 18px; white-space: nowrap; }
.paywall-tier-what, .upgrade-gains { margin: 6px 0 10px; font-size: 13px; line-height: 1.45; }

.upgrade-card { margin-bottom: 10px; }

/* "sold" on a stock movement. Only ever appears for organisations that sell — see
   CentralSupplies.tracksSales — so it marks the exception, never the rule. Green rather than the
   status colours: a sale is a good outcome, not a warning, and it must not read like "low stock". */
.pill {
  display: inline-block; padding: 1px 7px; border-radius: 10px;
  font-size: 11px; font-weight: 700; letter-spacing: 0.02em; vertical-align: middle;
}
.pill-sold { background: #e8f5ec; border: 1px solid #b7dcc4; color: #1f6b3a; }

/* ---------- Rentals (UIRentals.js) ---------- */

/* The hired-kit block sits below the equipment list, so it needs a rule above it — otherwise the
   two lists read as one and somebody's scissor lift looks like an owned asset. */
.rentals-header {
  border-top: 1px solid #dfe4ea;
  margin-top: 22px;
  padding-top: 16px;
}

/* The one line somebody reads before deciding whether to reach for the phone. Colour AND words —
   "3 days over" says it on its own in greyscale, in sunlight, and to somebody who cannot tell
   red from amber. The tint is the hint, never the message. */
.rental-status {
  font-weight: 600; font-size: 16px;
  padding: 10px 12px; border-radius: 8px;
  background: #f4f6f8; margin: 0 0 12px;
}
.rental-status.is-soon { background: #fdf2d0; color: #8a6d00; }
.rental-status.is-overdue { background: #fbe1de; color: #c0392b; }

.rental-past, .rental-history { margin-top: 14px; }
.rental-past > summary, .rental-history > summary {
  cursor: pointer; color: #67748a; font-size: 14px; padding: 6px 0;
}

/* ---------- The data plate photo (UIEquipment.js, §8b.5) ---------- */

.plate-block {
  border: 1px dashed #cfd6df;
  border-radius: 8px;
  padding: 10px 12px;
  margin: 10px 0 14px;
}

/* ⚠️ contain, NOT cover — unlike every other photo in the app. A data plate is a rectangle of
   small text, and object-fit:cover crops it to fill the box, which reliably slices off the serial
   number along one edge. Letterboxing an image is a cosmetic cost; cropping the one line somebody
   opened the photo to read defeats the whole feature. Taller, too: a plate is read, not glanced at. */
.plate-photo {
  object-fit: contain;
  max-height: 320px;
  background: #f4f6f8;
}

/* ---------- Shift handover (UIHandover.js, §8b.8) ---------- */

.handover-block { margin-bottom: 18px; }
.handover-empty p { margin: 0 0 10px; }

.handover-note {
  background: #fff;
  border: 1px solid #e3e8ee;
  border-left: 3px solid #c9d2dc;
  border-radius: 8px;
  padding: 10px 12px;
  margin-bottom: 8px;
}

/* The body is what somebody has to read across a room while putting a coat down, so it is the
   only thing on the card at full size. Everything else on it is metadata. */
.handover-body { font-size: 16px; line-height: 1.45; margin-bottom: 4px; white-space: pre-wrap; }
.handover-meta { font-size: 12px; margin-bottom: 8px; }

/* ⚠️ A LEFT EDGE, NOT A BACKGROUND WASH. Flagged and stale notes sit in a list with ordinary ones,
   and tinting the whole card makes the ordinary notes read as greyed-out/disabled. The stripe says
   "this one" without saying "the others don't count". Both carry words too — "not picked up" is on
   the card itself — because a color alone fails for about one man in twelve. */
.handover-note.is-flagged { border-left-color: #c0392b; }
.handover-note.is-stale { border-left-color: #8a6d00; background: #fffdf5; }
.handover-note.is-done { opacity: 0.72; border-left-color: #b7dcc4; }

.handover-history { margin-top: 10px; }
.handover-history > summary { cursor: pointer; color: #67748a; font-size: 14px; padding: 6px 0; }

/* The checkbox row in the note form — a tappable label, not a 13px box on its own. */
.check-row {
  display: flex; align-items: flex-start; gap: 10px;
  font-weight: 400; line-height: 1.4; padding: 4px 0;
}
.check-row input[type=checkbox] { width: 22px; height: 22px; margin: 0; flex: none; }

/* ---------- Machine handover + on-duty claim (UIHandover.js, §8b.8b) ---------- */

.machine-handover {
  border: 1px solid #e3e8ee;
  border-radius: 10px;
  padding: 12px;
  margin: 12px 0 16px;
  background: #fbfcfd;
}
.machine-handover h3 { margin-top: 0; }

/* The line naming who else is on the machine, and the confirmation that you are. Deliberately
   quiet: it is context, not an instruction. */
.on-duty-line { margin: 8px 0; font-size: 14px; }

/* The "just looking? you don't need to tap this" note under the claim button. Small, because it
   only matters to the person hesitating over the button. */
.machine-handover small.muted { display: block; margin-top: 6px; line-height: 1.45; }

/* ---------- Out of service (Equipment.js / UIHandover.js, 2026-08-25) ---------- */

/* ⚠️ THE LOUDEST THING ON THE SCREEN, deliberately. Everything else in this app is a status; this
   one means somebody must not start work. It sits at the very top of the machine card, above the
   photo, and it is the only place in the app that fills a whole block with the danger colour. */
.oos-banner {
  background: #fbe1de;
  border: 2px solid #c0392b;
  border-radius: 10px;
  padding: 12px 14px;
  margin: 0 0 14px;
}
.oos-head { font-size: 18px; font-weight: 700; color: #a5281b; margin-bottom: 6px; }
.oos-why { font-size: 16px; line-height: 1.45; margin-bottom: 6px; }

/* The list badge. A word, not only a colour — the same rule the status badges follow, and it
   matters more here than anywhere else in the app. */
.badge-stopped {
  background: #c0392b;
  color: #fff;
  font-weight: 700;
}

/* ---------- The shift sheet screen (UIRoster.js) ---------- */

.roster-daybar { display: flex; align-items: center; gap: 8px; margin: 10px 0 6px; }
.roster-day { flex: 1; text-align: center; font-weight: 600; font-size: 16px; }

/* The picker sits on the right of each name. Wide enough to read a machine name, capped so a long
   one does not push the name off a phone. */
.roster-pick { max-width: 52%; }

/* A row whose machine has been stopped since printing. Left edge, not a wash — same rule as the
   handover notes: the marked row must stand out without greying out the ordinary ones. */
.roster-broken { border-left: 3px solid #c0392b; padding-left: 9px; }
