/* Palette aligned to occamsig.com.au (its own tokens, read from the live
   site). One deliberate departure: the brand blue #00A3FF scores only
   2.73:1 on white, below the 4.5:1 readability floor, so it is never used
   for text or as a fill behind text. Navy carries those; #00A3FF is kept
   for accents that don't have to be read — underlines, focus rings,
   in-progress arcs. Links use --text-link, a darkened brand blue that
   does pass. This app is used on phones outdoors; legibility wins. */
:root {
  /* Both schemes exist now, so form controls, scrollbars and the like get
     drawn to match whichever is in force. The signature pad is the one
     thing that stays light either way — see .sig-wrap. */
  color-scheme: light dark;
  --surface-1: #f5f7fa;
  --surface-2: #ffffff;
  --surface-3: #ffffff;
  --border: #e2e8f0;
  --border-strong: #cbd5e1;
  --fill-accent: #0a2642;
  --on-accent: #ffffff;
  --brand-highlight: #00a3ff;
  --text-primary: #0a0a0a;
  --text-secondary: #45566b;
  --text-muted: #5a6b80;
  --text-accent: #0a2642;
  --text-link: #0074b8;
  --bg-accent: #e6f4ff;
  --bg-success: #e7f5ec;
  --text-success: #0f7a3d;
  --bg-warning: #fdf1e0;
  --text-warning: #a05a00;
  --bg-danger: #fbeceb;
  --text-danger: #c0392b;
  --fill-disabled: #eef2f7;
  --radius: 8px;
  --shadow-popover: 0 4px 16px rgba(10, 38, 66, 0.14);
  --shadow-card: 0 1px 2px rgba(10, 38, 66, 0.05);
  --footer-bg: #0a2642;
  --font-sans: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-display: "Space Grotesk", Inter, -apple-system, BlinkMacSystemFont, sans-serif;
  /* Minimum comfortable touch target — this gets tapped with gloves. */
  --tap-min: 42px;
}

/* ---- dark ---------------------------------------------------------------- *
 * Tokens only: every screen is drawn from these, so there is no second set of
 * templates and no per-screen dark rules to keep in step.
 *
 * The brand constraint inverts here. #00A3FF was barred from text because it
 * scores 2.73:1 on white; on this ground it reads about 8:1, so the brand blue
 * finally carries links and emphasis — the thing it could never do in light.
 *
 * Three states, not two. "Follow device" stamps nothing on <html>, so only the
 * media query applies; an explicit choice stamps data-theme and has to win in
 * both directions, which is why the media query is guarded against
 * [data-theme="light"] and the dark tokens are repeated below it.
 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
  --surface-1: #0d1520;
  --surface-2: #161f2b;
  --surface-3: #1c2632;
  --border: #27333f;
  --border-strong: #3d4b5a;
  --fill-accent: #14395c;
  --on-accent: #ffffff;
  --brand-highlight: #00a3ff;
  --text-primary: #e6edf5;
  --text-secondary: #a9bbcf;
  --text-muted: #8397ad;
  --text-accent: #6cc4ff;
  --text-link: #6cc4ff;
  --bg-accent: #12304d;
  --bg-success: #12301f;
  --text-success: #4ade80;
  --bg-warning: #33260d;
  --text-warning: #f5b544;
  --bg-danger: #3a1a17;
  --text-danger: #ff8175;
  --fill-disabled: #1b242f;
  --footer-bg: #0a1420;
  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.45);
  --shadow-popover: 0 4px 16px rgba(0, 0, 0, 0.55);
  }
}

:root[data-theme="dark"] {
  --surface-1: #0d1520;
  --surface-2: #161f2b;
  --surface-3: #1c2632;
  --border: #27333f;
  --border-strong: #3d4b5a;
  --fill-accent: #14395c;
  --on-accent: #ffffff;
  --brand-highlight: #00a3ff;
  --text-primary: #e6edf5;
  --text-secondary: #a9bbcf;
  --text-muted: #8397ad;
  --text-accent: #6cc4ff;
  --text-link: #6cc4ff;
  --bg-accent: #12304d;
  --bg-success: #12301f;
  --text-success: #4ade80;
  --bg-warning: #33260d;
  --text-warning: #f5b544;
  --bg-danger: #3a1a17;
  --text-danger: #ff8175;
  --fill-disabled: #1b242f;
  --footer-bg: #0a1420;
  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.45);
  --shadow-popover: 0 4px 16px rgba(0, 0, 0, 0.55);
}

/* The bar keeps its navy so the app still looks like itself, but the primary
   action has to carry on a dark ground rather than sink into it. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .primary,
  :root:not([data-theme="light"]) .icon-btn.primary,
  :root:not([data-theme="light"]) .switch-set input:checked + span,
  :root:not([data-theme="light"]) .switch-set label:has(input:checked) {
    background: #1a6fb0;
    border-color: #1a6fb0;
    color: #ffffff;
  }
}

:root[data-theme="dark"] .primary,
:root[data-theme="dark"] .icon-btn.primary,
:root[data-theme="dark"] .switch-set label:has(input:checked) {
  background: #1a6fb0;
  border-color: #1a6fb0;
  color: #ffffff;
}


* {
  box-sizing: border-box;
}

body {
  font-family: var(--font-sans);
  background: var(--surface-1);
  color: var(--text-primary);
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3 {
  font-family: var(--font-display);
  letter-spacing: -0.015em;
}

button,
input,
select,
textarea {
  font-family: inherit;
  font-size: 14px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text-primary);
  padding: 9px 12px;
  min-height: var(--tap-min);
}

button {
  cursor: pointer;
  font-weight: 500;
  letter-spacing: 0.01em;
}

/* Anything sitting inside a table row stays compact — the members table
   would become unusable at full touch height. */
.member-table button,
.member-table input,
.member-table select {
  min-height: 34px;
  padding: 5px 8px;
  font-size: 13px;
}

input[type="checkbox"],
input[type="radio"] {
  min-height: 0;
  padding: 0;
  width: 17px;
  height: 17px;
  accent-color: var(--fill-accent);
}

input:focus-visible,
select:focus-visible,
textarea:focus-visible,
button:focus-visible,
a:focus-visible {
  outline: 2px solid var(--brand-highlight);
  outline-offset: 1px;
  border-color: var(--brand-highlight);
}

a {
  color: var(--text-link);
}

/* One page width, shared by the header and the content under it, so the
   logo and the list below it line up on a wide screen. Content used to be
   pinned to a phone-sized column against the left edge: on a 1600px screen
   a cable list was 520px wide with 1050px of nothing beside it. */
:root {
  --page-max: 1280px;
}

.app-main {
  flex: 1;
  padding: 24px;
  width: 100%;
  max-width: var(--page-max);
  margin-inline: auto;
}

/* The navy bar stays full-bleed; only what sits in it is constrained. */
.topnav-row,
.topnav > .page-title {
  width: 100%;
  max-width: var(--page-max);
  margin-inline: auto;
}

/* The menu is a drawer, not a page. It used to run the full width of the
   bar, which on a desktop is 1,280px of mostly empty navy for a column of
   short links, and on a phone covered the screen so completely that you
   lost track of what was behind it. It hangs under the hamburger it opens
   from instead, at a width set by the longest link in it. */
.topnav .nav-links {
  width: 100%;
  max-width: 320px;
  margin-inline: 0 auto;
}

/* Frozen to the top: on a list hundreds of cables long, the menu and the
   name of the screen you're on stay reachable without scrolling back up.
   z-index clears the filter popover (20) so the panel scrolls under the
   bar rather than over it. */
.topnav {
  background: var(--fill-accent);
  border-bottom: 2px solid var(--brand-highlight);
  padding: 8px 16px 0;
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  z-index: 50;
}

.topnav-row {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 46px;
}

.topnav-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 6px;
  flex: none;
}

/* The screen name lives in the header rather than the page body, so the
   first thing under the bar is content, not a heading. Its own row, below
   the logo and buttons, so the full width is available to it — no screen
   name has to be truncated to make room for a button. */
.page-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 20px;
  letter-spacing: -0.015em;
  color: #ffffff;
  padding: 2px 0 12px;
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}

.page-title .page-sub {
  font-family: var(--font-sans);
  font-size: 12px;
  font-weight: 400;
  color: rgba(255, 255, 255, 0.7);
  letter-spacing: 0;
}

.topnav .brand {
  display: flex;
  align-items: center;
}

/* Source PNG is 560px wide, so this is a heavy downscale — sharp on any
   phone rather than merely adequate. */
.topnav .brand img {
  height: 34px;
  width: auto;
  display: block;
}

.topnav .nav-links {
  display: flex;
  gap: 16px;
  font-size: 14px;
  color: rgba(255, 255, 255, 0.85);
  align-items: center;
  flex: 1;
}

.topnav .nav-links a {
  color: #ffffff;
  text-decoration: none;
  border-bottom: 1px solid transparent;
}

.topnav .nav-links a:hover {
  border-bottom-color: var(--brand-highlight);
}

.topnav .nav-links button {
  color: #ffffff !important;
}

/* Desktop: push the account items (username onward) to the right. */
.topnav .nav-links > span {
  margin-left: auto;
  color: rgba(255, 255, 255, 0.72);
}

.nav-toggle {
  order: -1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: var(--radius);
  background: transparent;
  color: #ffffff;
  padding: 0 10px;
  min-height: var(--tap-min);
  line-height: 1;
  cursor: pointer;
  flex: none;
}

/* The links collapse behind the hamburger at every width, not just on
   phones — one predictable place to find them. */
.topnav .nav-links {
  display: none;
  flex-direction: column;
  align-items: stretch;
  flex: 0 0 100%;
  gap: 0;
  padding-bottom: 6px;

  /* Scrolls inside itself rather than growing the bar it lives in. The bar
     is sticky, so a menu taller than the window turned the whole header
     into the window: five projects, one of them open to ten screens, is
     more rows than a phone has height for and the screen behind vanished.

     Capped at a share of the window rather than merely at its height, so
     there is always some of the page left to see — a drawer that reaches
     the bottom of the glass is a page, and you stop knowing what is behind
     it. dvh, not vh, so a phone's retracting address bar cannot leave the
     last row under it. */
  max-height: min(62dvh, calc(100dvh - 92px));
  overflow-y: auto;
  overscroll-behavior: contain;
}

.topnav.nav-open .nav-links { display: flex; }

.topnav .nav-links > a,
.topnav .nav-links > span,
.topnav .nav-links > form {
  padding: 11px 4px;
  border-top: 1px solid rgba(255, 255, 255, 0.16);
  font-size: 15px;
}

.topnav .nav-links > span { margin-left: 0; }

@media (max-width: 720px) {
  .app-main {
    padding: 16px;
  }

  .page-title {
    font-size: 18px;
  }
}

/* Aligned form layout: labels above full-width inputs, one column. */
.form-stack {
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-width: 440px;
}

.form-stack .field {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.form-stack .field > label {
  font-size: 12px;
  color: var(--text-secondary);
}

.form-stack input:not([type="checkbox"]):not([type="radio"]),
.form-stack select,
.form-stack textarea {
  width: 100%;
}

.form-row {
  display: flex;
  gap: 10px;
}

.form-row .field {
  flex: 1;
}

.field-error {
  color: var(--text-danger);
  font-size: 12px;
}

/* Installed → IR tested → Ready, as a three-segment bar plus the stage
   actually reached. Replaces dot-line-dot-line-dot with three repeated
   labels, which was the noisiest thing on a card. */
.progress {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-top: 9px;
}

.progress-track {
  display: flex;
  gap: 3px;
  flex: 0 0 auto;
  width: 84px;
}

.progress-track .seg {
  height: 5px;
  flex: 1;
  border-radius: 3px;
  background: var(--border);
}

.progress-track .seg.on {
  background: var(--text-success);
}

.progress-label {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-secondary);
  white-space: nowrap;
  letter-spacing: 0.01em;
}

.progress-label.is-ready { color: var(--text-success); }
.progress-label.is-pending { color: var(--text-muted); font-weight: 500; }

.card {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px;
  box-shadow: var(--shadow-card);
}

/* Cards that are links shouldn't look like body text. */
a.card {
  text-decoration: none;
}

a.card:hover {
  border-color: var(--brand-highlight);
}

/* Marks a required field in a form label. */
.req {
  color: var(--text-danger);
  font-weight: 600;
}

/* "48 installed · 7 to be installed" under a list heading. */
.result-count {
  font-size: 13px;
  color: var(--text-muted);
}

.member-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.member-table th,
.member-table td {
  padding: 8px 10px;
  border-bottom: 0.5px solid var(--border);
  text-align: center;
  vertical-align: middle;
}

.member-table th {
  background: var(--surface-1);
  color: var(--text-secondary);
  font-weight: 500;
  white-space: nowrap;
}

.member-table tbody tr:hover {
  background: var(--surface-1);
}

.badge {
  font-size: 11px;
  padding: 3px 8px;
  border-radius: var(--radius);
  white-space: nowrap;
}

.badge-success {
  background: var(--bg-success);
  color: var(--text-success);
}

.badge-warning {
  background: var(--bg-warning);
  color: var(--text-warning);
}

.badge-accent {
  background: var(--bg-accent);
  color: var(--text-accent);
}

.badge-danger {
  background: var(--bg-danger);
  color: var(--text-danger);
}

.auth-wrap {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.auth-card {
  width: 100%;
  max-width: 360px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 28px 24px;
  text-align: center;
}

.auth-card .brand {
  font-size: 18px;
  font-weight: 500;
  margin-bottom: 4px;
}

.auth-card .tagline {
  font-size: 13px;
  color: var(--text-muted);
  margin-bottom: 28px;
}

.auth-card h1 {
  font-size: 15px;
  font-weight: 500;
  margin: 0 0 20px;
}

.auth-card form {
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.auth-card label {
  font-size: 12px;
  color: var(--text-secondary);
  display: block;
  margin-bottom: 4px;
}

.auth-card input[type="text"],
.auth-card input[type="email"],
.auth-card input[type="password"] {
  width: 100%;
}

.auth-card .field-error {
  color: var(--text-danger);
  font-size: 12px;
  margin-top: 4px;
}

.auth-card .form-error {
  background: var(--bg-danger);
  color: var(--text-danger);
  border-radius: var(--radius);
  padding: 8px 10px;
  font-size: 12px;
  margin-bottom: 12px;
  text-align: left;
}

.auth-card .primary-submit {
  background: var(--fill-accent);
  color: var(--on-accent);
  border-color: var(--fill-accent);
  width: 100%;
  padding: 9px;
  margin-top: 4px;
}

.auth-divider {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 18px 0;
  color: var(--text-muted);
  font-size: 11px;
}

.auth-divider::before,
.auth-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

.oauth-button {
  width: 100%;
  margin-bottom: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 8px;
  text-decoration: none;
  color: var(--text-primary);
}

.auth-footer-note {
  margin-top: 20px;
  font-size: 12px;
  color: var(--text-muted);
}

.field-hint {
  margin: 6px 0 0;
  padding-left: 18px;
  font-size: 11px;
  color: var(--text-muted);
}

.field-hint li {
  margin-bottom: 2px;
}

/* ---- form sections ------------------------------------------------------ *
 * Groups the fields recorded during the pull, so it's obvious at a glance
 * which half of the form a planned run can skip.
 */
.form-section {
  border: 0.5px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 14px 14px;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.section-note {
  font-size: 12px;
  color: var(--text-muted);
  margin: 0 0 2px;
}

.form-section > legend {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-secondary);
  padding: 0 6px;
}

/* A cable's From end and To end, each in its own box inside Cable inputs
   (Jason, 2026-09-26: six rows of location, chainage and terminals in one
   list made it hard to tell which end a figure belonged to). A firmer
   border and a tinted ground than the section around it, so the two boxes
   read as a pair of things rather than more section. */
.end-box {
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-1);
  padding: 8px 12px 12px;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 0;
}

.end-box > legend {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-primary);
  padding: 0 6px;
}

/* "+ New drum" under the drum picker: a native disclosure, so it opens
   without scripting. */
.drum-quick-add {
  margin-top: 8px;
}

.drum-quick-add > summary {
  cursor: pointer;
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-link);
}

.drum-quick-add[open] {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* ---- paged lists -------------------------------------------------------- */

/* Columns rather than one long strip: a card is readable from about 400px,
   so a wide screen shows two or three side by side instead of a phone-width
   ribbon with the rest of the monitor empty. min(100%, ...) keeps it to one
   column on a phone, where 400px is wider than the screen. */
.run-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 400px), 1fr));
  gap: 8px;
}

.load-more {
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: 520px;
  margin: 14px 0 4px;
}

.load-more .btn-secondary { flex: none; }

.load-more-count {
  font-size: 12px;
  color: var(--text-muted);
}

/* ---- date picker --------------------------------------------------------- *
 * A calendar we draw ourselves, so Today can sit inside it rather than as a
 * separate button on the form. Only applied on pointer devices — on a phone
 * the OS picker beats anything drawn at this size, so the native one stays.
 */
.dp {
  position: relative;
  display: block;
}

.dp input { width: 100%; }

/* Hides the browser's own calendar trigger, which would open a second,
   different picker over the top of this one. */
.dp input::-webkit-calendar-picker-indicator { display: none; }

.dp-panel {
  position: absolute;
  z-index: 30;
  top: calc(100% + 4px);
  left: 0;
  width: 268px;
  max-width: calc(100vw - 32px);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-popover);
  padding: 10px;
}

.dp-panel[hidden] { display: none; }

.dp-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  margin-bottom: 8px;
}

.dp-title {
  font-size: 13px;
  font-weight: 600;
}

.dp-nav {
  width: 30px;
  height: 30px;
  min-height: 30px;
  padding: 0;
  border: 0.5px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text-primary);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
}

.dp-nav:hover { background: var(--bg-accent); }

.dp-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}

.dp-dow {
  font-size: 10px;
  font-weight: 600;
  color: var(--text-muted);
  text-align: center;
  padding-bottom: 4px;
}

.dp-day {
  height: 32px;
  min-height: 32px;
  padding: 0;
  border: none;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text-primary);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.dp-day:hover { background: var(--bg-accent); }

/* Bigger cells where there's a finger rather than a cursor. */
@media (pointer: coarse) {
  .dp-panel { width: 300px; padding: 12px; }
  .dp-day { height: 40px; min-height: 40px; font-size: 13px; }
  .dp-action { min-height: 42px; font-size: 13px; }
  .dp-nav { width: 38px; height: 38px; min-height: 38px; }
}

/* Today is outlined, the chosen day is filled — so you can see both at once
   and tell which is which. */
.dp-day.is-today {
  box-shadow: inset 0 0 0 1.5px var(--brand-highlight);
  font-weight: 600;
}

.dp-day.is-selected {
  background: var(--fill-accent);
  color: var(--on-accent);
  font-weight: 600;
}

.dp-foot {
  display: flex;
  gap: 6px;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 0.5px solid var(--border);
}

.dp-action {
  flex: 1 1 0;
  min-height: 34px;
  padding: 0 10px;
  border: 0.5px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text-primary);
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
}

.dp-action:hover { background: var(--bg-accent); }

.dp-action.is-primary {
  background: var(--fill-accent);
  border-color: var(--fill-accent);
  color: var(--on-accent);
  font-weight: 600;
}

/* ---- two-option switch --------------------------------------------------- *
 * Radio inputs styled as a segmented control: both options stay visible and
 * selecting one costs a single tap, where a dropdown costs two and hides
 * the alternative. Still radios underneath, so it works without JavaScript
 * and reads correctly to a screen reader.
 */
.switch-set {
  display: flex;
  border: 0.5px solid var(--border-strong);
  border-radius: var(--radius);
  overflow: hidden;
  min-height: var(--tap-min);
}

.switch-set label {
  flex: 1 1 0;
  min-width: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0;
  padding: 0 8px;
  border-left: 0.5px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--text-secondary);
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  margin: 0;
}

.switch-set label:first-of-type { border-left: none; }

/* The radio itself is hidden but still focusable and still the thing being
   checked — the label is only its skin. */
.switch-set input[type="radio"] {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: 0;
  pointer-events: none;
}

.switch-set label:hover {
  background: var(--bg-accent);
  color: var(--text-primary);
}

.switch-set label:has(input:checked) {
  background: var(--fill-accent);
  color: var(--on-accent);
  font-weight: 600;
}

.switch-set label:has(input:focus-visible) {
  outline: 2px solid var(--brand-highlight);
  outline-offset: -2px;
}

/* The cable type dropdown carries a long name; the switch needs only
   enough for "Through". */
.form-row-type .field:first-child { flex: 1 1 60%; }
.form-row-type .field:last-child { flex: 0 1 40%; }

/* ---- inline field helpers ----------------------------------------------- */

.btn-inline {
  flex: none;
  min-height: var(--tap-min);
  padding: 0 14px;
  border: 0.5px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text-accent);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
}

.btn-inline:hover {
  border-color: var(--fill-accent);
  background: var(--bg-accent);
}

/* A clear button inside the field's right edge. */
.search-with-clear {
  position: relative;
  display: block;
}

.search-with-clear input {
  width: 100%;
  padding-right: 38px;
}

.search-clear {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  min-height: 0;
  border: none;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}

.search-clear[hidden] { display: none; }

/* The native one only wipes the text without re-running anything, so the
   app draws its own — see core/templates/core/search_clear.html. */
.search-with-clear input[type="search"]::-webkit-search-cancel-button {
  display: none;
}

.search-clear:hover { color: var(--text-primary); }

/* Chrome and Safari draw their own clear affordance on type=search, which
   would sit alongside this one doing the same job. */
.search-with-clear input::-webkit-search-cancel-button { display: none; }

/* ---- stage filter (reporting options) ----------------------------------- */

.stage-filter {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 168px;
  overflow-y: auto;
  border: 0.5px solid var(--border);
  border-radius: var(--radius);
  padding: 6px;
}

.stage-filter #stageAll {
  align-self: flex-start;
  margin-bottom: 4px;
}

.stage-check {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 4px;
  font-size: 13px;
  cursor: pointer;
  border-radius: var(--radius);
}

.stage-check:hover { background: var(--bg-accent); }

.stage-check input { margin: 0; flex: none; }

/* ---- cable test entry ---------------------------------------------------- */

/* Hidden from sight but still operable and still submitted — the file
   inputs are driven by the buttons above them. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.check-rows {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Description left, control right: the eye reads what it is, then lands on
   the control, and on a phone the thumb is already at that edge. The whole
   row is the target, not just the box. */
.check-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 12px 14px;
  border: 0.5px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  min-height: var(--tap-min);
  cursor: pointer;
  margin: 0;
}

.check-row:hover { border-color: var(--fill-accent); }

.check-row:has(input:checked) {
  border-color: var(--text-success);
  background: var(--bg-success);
}

.check-row:has(input:focus-visible) {
  outline: 2px solid var(--brand-highlight);
  outline-offset: -2px;
}

.check-label {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 13px;
  min-width: 0;
}

.check-hint {
  font-size: 11px;
  color: var(--text-muted);
}

/* Big enough to hit in gloves. */
.check-row input[type="checkbox"] {
  flex: none;
  width: 26px;
  height: 26px;
  margin: 0;
  accent-color: var(--text-success);
  cursor: pointer;
}

/* A labelled row: description left, control right, matching the check rows
   so the eye runs down one column of descriptions and one of controls. */
.entry-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 10px 14px;
  border: 0.5px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  min-height: var(--tap-min);
}

.entry-label {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 13px;
  min-width: 0;
  margin: 0;
}

.entry-hint {
  font-size: 11px;
  color: var(--text-muted);
}

/* Derived from the input's own required attribute, like the markers on the
   plain form fields, so a row can never say optional while the server
   rejects it as missing. */
.entry-row:has(input[required]) > .entry-label::after,
.entry-row:has(select[required]) > .entry-label::after {
  content: " *";
  color: var(--text-danger);
  font-weight: 600;
}

/* The signature has no visible required input to derive from — its value
   is a hidden field the canvas writes — so that one marker is in the
   markup. */
.req {
  color: var(--text-danger);
  font-weight: 600;
}

.entry-control {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 190px;
}

/* The cable run form reuses the entry rows, but its labels are short field
   names rather than the test screen's sentences. A grid rather than the
   flex row, because space-between sizes each control to its own content:
   the boxes started at a different x in every row and none were the same
   width. One fixed column means every control lines up down the screen and
   measures the same, whichever section it sits in — both groups are inset
   by a fieldset so the two line up with each other too. */
.entry-row-form {
  display: grid;
  /* Proportional rather than a fixed 160px: on the 440px form that fixed
     column left the labels about 210px for text needing ~140, so the inputs
     were the narrow half of a screen with room to spare. The floor keeps the
     phone exactly as it was — there, 160px is already all that fits beside
     a label. */
  grid-template-columns: minmax(0, 1fr) minmax(160px, 1.6fr);
  gap: 12px;
  align-items: center;
}

/* Plain block, not the test screen's column flex: these labels are a single
   run of text with no hint under them, and in a column flex container the
   required marker becomes its own flex line — the asterisk dropped under
   the label instead of sitting after it. */
.entry-row-form > .entry-label {
  display: block;
}

.entry-row-form > .entry-control {
  max-width: none;
  width: 100%;
}

/* Actual against estimated, so working down a project's cables shows at a
   glance how good its estimating is: over 110% of the estimate reads red,
   98-110% orange, under 98% green. The colour goes on the text — the field
   is disabled, and a coloured fill would read as something you could type
   in. -webkit-text-fill-color too, or the disabled style wins in WebKit. */
.len-over,
.len-near,
.len-under {
  font-weight: 600;
}

/* text-fill-color as well as color: WebKit greys out a disabled field's
   text through that property, and would otherwise win. */
.len-over {
  color: var(--text-danger);
  -webkit-text-fill-color: var(--text-danger);
}

.len-near {
  color: var(--text-warning);
  -webkit-text-fill-color: var(--text-warning);
}

.len-under {
  color: var(--text-success);
  -webkit-text-fill-color: var(--text-success);
}

#actuallength:disabled {
  background: var(--fill-disabled);
}

.entry-control input { width: 100%; }

/* An icon-only button sized like the text ones beside it. */
.btn-icon {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  min-height: var(--tap-min);
  padding: 0;
  border: 0.5px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text-accent);
  cursor: pointer;
}

.btn-icon:hover {
  border-color: var(--fill-accent);
  background: var(--bg-accent);
}

/* The picker's own field: a box you tap, not one you type in. */
.dp-input {
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}

/* ---- a unit inside its field --------------------------------------------- */

.input-with-unit {
  position: relative;
  display: block;
  max-width: 240px;
}

.input-with-unit input {
  width: 100%;
  padding-right: 34px;
}

/* Sits over the field's right edge, after the number. */
.input-unit {
  position: absolute;
  top: 0;
  right: 12px;
  height: 100%;
  display: inline-flex;
  align-items: center;
  font-size: 14px;
  color: var(--text-secondary);
  pointer-events: none;
}

/* The spinner would collide with the unit, and nobody steps a resistance
   reading up by 0.001 with arrows. */
.input-with-unit input::-webkit-outer-spin-button,
.input-with-unit input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.input-with-unit input[type="number"] { -moz-appearance: textfield; }

.photo-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.photo-name {
  font-size: 12px;
  color: var(--text-muted);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- import drop zone ---------------------------------------------------- *
 * A label wrapped round the real file input, so the input is still the whole
 * mechanism: clicking opens the picker, the keyboard reaches it, and with no
 * JavaScript this is the plain field it always was.
 */
.dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 26px 18px;
  max-width: 520px;
  border: 1.5px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text-secondary);
  text-align: center;
  cursor: pointer;
}

.dropzone:hover,
.dropzone:focus-within {
  border-color: var(--brand-highlight);
}

/* Dragging is a pointer affordance; on a phone the box is just a big button
   to open the file picker, which is what it already was. */
.dropzone.is-over {
  border-color: var(--brand-highlight);
  background: var(--bg-accent);
}

/* The input still does the work, so it stays in the form and keeps its
   focus ring via :focus-within above — it just isn't drawn. */
.dropzone input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.dropzone-icon { color: var(--text-muted); }

.dropzone-main {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
  overflow-wrap: anywhere;
}

.dropzone-sub { font-size: 12px; color: var(--text-muted); }

/* ---- signature pad ------------------------------------------------------- *
 * A hairline box on white was easy to miss altogether. A ruled line to sign
 * on, a heavier frame and more height make it read as somewhere to write.
 */
/* Fixed light in both themes, deliberately. What you sign on has to be what
   gets stored, and the stored image ends up on a certificate, in a PDF and
   possibly in an email — all of them white. The canvas paints its own white
   surface too (see testing/entry.html), so this holds even where a browser
   force-darkens the page. In dark it is dimmed on screen only; the exported
   pixels are read from the bitmap, which a CSS filter does not touch. */
.sig-wrap {
  position: relative;
  border: 1.5px solid var(--border-strong);
  border-radius: var(--radius);
  background: #ffffff;
  max-width: 400px;
  box-shadow: inset 0 1px 3px rgba(10, 38, 66, 0.06);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .sig-wrap { border-color: #4a5766; }
  :root:not([data-theme="light"]) .sig-foot { background: #eef1f5; border-color: #d7dde5; }
  :root:not([data-theme="light"]) .sig-caption,
  :root:not([data-theme="light"]) .sig-clear { color: #5a6b80; }
  :root:not([data-theme="light"]) .sig-wrap canvas { filter: brightness(0.93); }
}

:root[data-theme="dark"] .sig-wrap { border-color: #4a5766; }
:root[data-theme="dark"] .sig-foot { background: #eef1f5; border-color: #d7dde5; }
:root[data-theme="dark"] .sig-caption,
:root[data-theme="dark"] .sig-clear { color: #5a6b80; }
:root[data-theme="dark"] .sig-wrap canvas { filter: brightness(0.93); }

.sig-wrap canvas {
  display: block;
  width: 100%;
  height: auto;
  touch-action: none;
  cursor: crosshair;
  position: relative;
  z-index: 1;
}

/* The line to sign on, set above the footer strip so a descender has room.
   Above the canvas too: the canvas paints its own opaque white surface, so
   a rule behind it would no longer show through. It stays a DOM element
   rather than being drawn onto the canvas, so it never becomes part of the
   saved signature. */
.sig-rule {
  position: absolute;
  left: 16px;
  right: 16px;
  bottom: 46px;
  z-index: 2;
  border-top: 1px dashed var(--border-strong);
  pointer-events: none;
}

/* The box labels itself: caption left, clear on the right, inside the
   frame — rather than a label above and a button below it. */
.sig-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 4px 6px 4px 14px;
  border-top: 0.5px solid var(--border);
  background: var(--surface-1, #fdfdfc);
  border-radius: 0 0 var(--radius) var(--radius);
}

.sig-caption {
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.sig-clear {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  min-height: 32px;
  padding: 0;
  border: none;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}

.sig-clear:hover {
  background: var(--bg-danger);
  color: var(--text-danger);
}

/* ---- header: project nav + account avatar -------------------------------- */

.nav-heading {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55) !important;
  padding: 10px 4px 4px !important;
  border-top: none !important;
}

.nav-empty {
  color: rgba(255, 255, 255, 0.6) !important;
  font-size: 13px !important;
}

.topnav .nav-links .nav-manage {
  font-size: 13px;
  color: rgba(255, 255, 255, 0.75);
}

/* Each project opens to its own screens. The details are named, so the
   browser closes the others when one is opened — an accordion without
   JavaScript, which matters because this menu is the only way around the
   app. */
.topnav .nav-links .nav-proj {
  border-top: 1px solid rgba(255, 255, 255, 0.16);
}

.nav-proj-name {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 4px;
  font-size: 15px;
  color: #ffffff;
  position: relative;
}

.nav-proj-name::-webkit-details-marker { display: none; }

/* A caret that turns, the same shape the pit and drum cards use, so an
   openable row looks openable before it is touched.

   Pushed right by its own margin rather than by space-between on the row:
   with space-between, the current project — which has an accent bar before
   its name as well — became three items spread across the row, and its
   name sat centred while every other name sat left. */
.nav-proj-name::after {
  content: "";
  flex: none;
  margin-left: auto;
  width: 6px;
  height: 6px;
  margin-right: 6px;
  border-right: 1.5px solid rgba(255, 255, 255, 0.6);
  border-bottom: 1.5px solid rgba(255, 255, 255, 0.6);
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.15s ease;
}

.nav-proj[open] > .nav-proj-name::after { transform: translateY(1px) rotate(-135deg); }

/* The project the screen belongs to: the only one worth marking, and the
   one that starts open. */
.nav-proj-name.is-current { font-weight: 600; }

/* Out of the flow, or the bar takes 9px of the row and pushes the current
   project's name right — indenting the one project that should sit level
   with the rest. The indent in this menu belongs to the screens under a
   project, not to the project itself. */
.nav-proj-name.is-current::before {
  content: "";
  position: absolute;
  left: -4px;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--brand-highlight);
}

/* The screens themselves: indented, quieter and tighter than the project
   names, so the menu reads as a list of projects with one opened out
   rather than one long list of equals. */
.topnav .nav-links .nav-proj > a {
  display: block;
  padding: 9px 4px 9px 22px;
  font-size: 14px;
  color: rgba(255, 255, 255, 0.82);
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  position: relative;
}

.topnav .nav-links .nav-proj > a::before {
  content: "";
  position: absolute;
  left: 9px;
  top: 0;
  bottom: 0;
  width: 1px;
  background: rgba(255, 255, 255, 0.22);
}

/* Whatever the screen itself added, under the projects. The divider is
   drawn only when it added something — several screens add nothing, and a
   rule over nothing reads as a rendering fault. */
.nav-screen { display: contents; }

.topnav .nav-links .nav-screen:has(a) > a:first-of-type {
  border-top: 2px solid rgba(255, 255, 255, 0.28);
  margin-top: 6px;
}

/* ---- your projects: which appear in my menu ------------------------------ */

.project-choices {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-width: 520px;
}

.project-choices-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding: 0 2px 2px;
}

.project-choice {
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  padding-right: 12px;
}

/* The row still opens the project — the tick is a separate target so
   choosing what's in your menu can't be confused with going there. */
.project-choice-link {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 12px 14px;
  text-decoration: none;
  color: inherit;
  min-height: var(--tap-min);
  justify-content: center;
}

.project-choice-link:hover .project-choice-name { text-decoration: underline; }

.project-choice-name {
  font-size: 14px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.project-choice-meta {
  font-size: 12px;
  color: var(--text-muted);
}

.project-choice-toggle {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  margin: 0;
  cursor: pointer;
}

.project-choice-toggle input {
  width: 24px;
  height: 24px;
  margin: 0;
  accent-color: var(--fill-accent);
  cursor: pointer;
}

/* ---- account avatar ------------------------------------------------------ */

.account-menu > summary.avatar {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  min-width: 34px;
  min-height: 34px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.4);
  background: rgba(255, 255, 255, 0.14);
  color: #ffffff;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.02em;
  user-select: none;
}

.account-menu > summary.avatar::-webkit-details-marker { display: none; }

.account-menu > summary.avatar:hover { background: rgba(255, 255, 255, 0.24); }

.account-menu[open] > summary.avatar {
  background: #ffffff;
  color: var(--fill-accent);
  border-color: #ffffff;
}

.account-who {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 10px 6px;
  min-width: 0;
}

.account-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-primary);
}

/* The address is here rather than in the navigation menu, where it read as
   something to click. */
.account-email {
  font-size: 11px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
}

.menu-signout {
  width: 100%;
  text-align: left;
  border: none;
  background: none;
  padding: 8px 10px;
  min-height: var(--tap-min);
  font-size: 13px;
  color: var(--text-danger);
  cursor: pointer;
}

.menu-signout:hover { background: var(--bg-danger); }

/* ---- test readiness cards ----------------------------------------------- *
 * Description left, dials right. Own classes rather than the pit card's:
 * that summary is a centred column, and at equal specificity it won,
 * stacking the dials under the description instead of beside it.
 */
.rd-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 400px), 1fr));
  gap: 8px;
}

.rd-head { display: none; }

.rd-card {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
}

.rd-summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 14px;
  border-radius: inherit;
}

.rd-summary::-webkit-details-marker { display: none; }

.rd-card[open] > .rd-summary {
  border-bottom: 0.5px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
}

.rd-summary:focus-visible {
  outline: 2px solid var(--brand-highlight);
  outline-offset: -2px;
}

/* Left: what it is. Takes the slack, so the dials never move. */
.rd-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  min-width: 0;
  flex: 1 1 auto;
}

.rd-name {
  font-size: 14px;
  font-weight: 500;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rd-summary:hover .rd-name { text-decoration: underline; }

/* Caret after the name, so the card reads as openable. */
.rd-name::after {
  content: "";
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-left: 7px;
  border-right: 1.5px solid var(--text-muted);
  border-bottom: 1.5px solid var(--text-muted);
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.15s ease;
}

.rd-card[open] .rd-name::after { transform: translateY(1px) rotate(-135deg); }

.rd-badge {
  font-size: 10px;
  padding: 2px 7px;
  border-radius: var(--radius);
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

.rd-badge.is-done { background: var(--bg-success); color: var(--text-success); }
.rd-badge.is-pending { background: var(--bg-danger); color: var(--text-danger); }

/* Right: the two dials, fixed so they line up down the whole list. */
.rd-dials {
  display: flex;
  align-items: center;
  gap: 14px;
  flex: none;
}

/* The export sits in the opened panel, not the summary's right edge, where
   it competed with the dials for the same corner. */
.rd-export {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin-top: 10px;
  padding: 9px 12px;
  border: 0.5px solid var(--border-strong);
  border-radius: var(--radius);
  font-size: 12px;
  color: var(--text-primary);
  text-decoration: none;
  min-height: var(--tap-min);
}

.rd-export:hover {
  border-color: var(--fill-accent);
  background: var(--bg-accent);
}

/* An untested cable is listed but isn't a link — there's no certificate to
   open — so it must not look like one. */
.cable-row.is-untested {
  cursor: default;
  opacity: 0.8;
}

.cable-row.is-untested:hover { background: transparent; }

.readiness-state {
  font-size: 11px;
  padding: 3px 8px;
  border-radius: var(--radius);
  white-space: nowrap;
}

.readiness-state.is-done { background: var(--bg-success); color: var(--text-success); }
.readiness-state.is-pending { background: var(--bg-danger); color: var(--text-danger); }

/* ---- stage list --------------------------------------------------------- */

.stage-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 400px), 1fr));
  gap: 8px;
}

.stage-head { display: none; }

/* Above 860px a stage is one row across the page under a header, like every
   other list on the site, rather than a card in a grid of columns. */
@media (min-width: 860px) {
  .stage-head {
    display: grid;
    /* Name, work package, run count. */
    grid-template-columns: minmax(0, 1fr) 260px 120px;
    gap: 0 16px;
    padding: 0 14px 6px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  .stage-head span:last-child { text-align: right; }

  .stage-list {
    grid-template-columns: 1fr;
    gap: 4px;
  }

  .stage-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 260px 120px;
    gap: 0 16px;
    align-items: center;
    padding: 9px 14px;
  }

  .stage-count { text-align: right; }
  .stage-cwp { justify-content: flex-start; }

}

.stage-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 14px;
}

.stage-name {
  font-size: 14px;
  font-weight: 500;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.stage-count {
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* ---- pit cards ---------------------------------------------------------- *
 * A pit opens to show the cables running through it. The pencil is absolutely
 * positioned rather than sitting in the <summary>, because a link inside a
 * summary both follows and toggles — the card would flick open as you left.
 */
/* On a phone the type, remaining and count read as one muted line, the
   separators drawn rather than written into the template so the wide
   layout can drop them without the markup changing. */
.drum-remaining::before,
.drum-count::before { content: " · "; }

.drum-head { display: none; }


/* Matches .pit-list: one column on a phone, flowing into columns only
   when there is room for them. */
/* The sum of what has been pulled from a drum, shown above its cables so
   the card's own arithmetic is visible. Only ever a statement of fact —
   nothing in the app keeps the typed remaining figure in step with the
   runs recorded against the drum, so the two can and do disagree. */
/* Lines up with the cables under it, not with the summary above: it is
   part of what the card opened to show. */
.drum-sum {
  margin-left: 14px;
  padding: 6px 14px;
  font-size: 12px;
  color: var(--text-muted);
  border-top: 0.5px solid var(--border);
}

.drum-sum.is-over {
  color: var(--text-danger);
  font-weight: 600;
}

.drum-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 400px), 1fr));
  gap: 8px;
}

.pit-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 400px), 1fr));
  gap: 8px;
}

.pit-card,
.drum-card {
  position: relative;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
}

.pit-summary,
.drum-summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* Right padding clears the pencil so a long pit name can't run under it. */
  padding: 12px 52px 12px 14px;
  border-radius: inherit;
  min-height: var(--tap-min);
  justify-content: center;
}

.pit-summary::-webkit-details-marker,
.drum-summary::-webkit-details-marker { display: none; }

/* On a phone the count and chainage read as one muted line, the separator
   drawn rather than written into the template so the wide layout can drop
   it without the markup changing. Literal characters, not hex escapes. */
.pit-chainage::before { content: " · "; }

.pit-head { display: none; }

/* ---- pits, wide ---------------------------------------------------------- *
 * Cards flowing into 400px columns read well on a phone and badly on a
 * desktop, the same wall the cable runs list had. Above 860px each pit is one
 * row with its detail across the page, under sortable headings — and it still
 * opens in place to show the cables through it, which is the reason these are
 * cards rather than table rows in the first place.
 */
@media (min-width: 860px) {
  .pit-list {
    grid-template-columns: 1fr;
    gap: 4px;
  }

  .pit-head,
  .pit-summary {
    display: grid;
    grid-template-columns: minmax(0, 2fr) 110px minmax(0, 1fr);
    gap: 0 16px;
    align-items: center;
  }

  .pit-head {
    padding: 0 52px 6px 16px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  .pit-head > a {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    color: inherit;
    text-decoration: none;
    min-width: 0;
  }

  .pit-head > a:hover,
  .pit-head > a:focus-visible { color: var(--text-link); }

  .pit-head > a.is-sorted { color: var(--text-accent); }

  .pit-summary {
    padding: 8px 52px 8px 16px;
  }

  .pit-summary .pit-meta { display: contents; }

  .pit-chainage::before { content: none; }

  .pit-count,
  .pit-chainage {
    font-size: 12px;
    color: var(--text-muted);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* ---- sortable, filterable column headings -------------------------------- *
 * The label sorts in one click; the chevron opens the directions and the
 * values the column can be filtered to. Only ever shown in the wide layouts,
 * where the lists read like a spreadsheet — the phone cards have no headings.
 */
.col-head {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  min-width: 0;
}

.col-label {
  color: inherit;
  text-decoration: none;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

a.col-label:hover,
a.col-label:focus-visible { color: var(--text-link); }

.col-head.is-sorted .col-label { color: var(--text-accent); }

/* A filtered column has to look different from a sorted one: sorting changes
   the order, filtering changes what is there at all, and only the second can
   hide a cable someone is looking for. */
.col-head.is-filtered .col-label {
  color: var(--text-accent);
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 3px;
}

.col-menu > summary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border-radius: 3px;
  color: var(--text-muted);
  cursor: pointer;
  list-style: none;
}

.col-menu > summary::-webkit-details-marker { display: none; }

.col-menu > summary:hover,
.col-menu[open] > summary {
  background: var(--bg-accent);
  color: var(--text-accent);
}

.col-menu > summary:focus-visible {
  outline: 2px solid var(--brand-highlight);
  outline-offset: 1px;
}

/* Headings are small and near the top, so the panel hangs below-left rather
   than pushing off the right edge of the last column. */
.col-menu .menu-panel {
  right: auto;
  left: 0;
  min-width: 190px;
  max-height: 320px;
  overflow-y: auto;
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
}

.col-head:last-child .col-menu .menu-panel,
.run-head > *:last-child .col-menu .menu-panel {
  left: auto;
  right: 0;
}

.col-menu .menu-panel a.is-current {
  background: var(--bg-accent);
  color: var(--text-accent);
  font-weight: 600;
}

/* ---- drums, wide --------------------------------------------------------- *
 * The same treatment as the cable runs and pits lists: above 860px each drum
 * is one row with its detail across the page, under sortable headings, and it
 * still opens in place to show the cables pulled from it.
 */
@media (min-width: 860px) {
  .drum-list {
    grid-template-columns: 1fr;
    gap: 4px;
  }

  .drum-head,
  .drum-summary {
    display: grid;
    grid-template-columns:
      minmax(0, 1.5fr) minmax(0, 1.2fr) minmax(150px, 1fr) 92px 100px;
    gap: 0 16px;
    align-items: center;
  }

  .drum-head {
    padding: 0 52px 6px 16px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  .drum-head > a {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    color: inherit;
    text-decoration: none;
    min-width: 0;
  }

  .drum-head > a:hover,
  .drum-head > a:focus-visible { color: var(--text-link); }

  .drum-head > a.is-sorted { color: var(--text-accent); }

  .drum-summary {
    padding: 8px 52px 8px 16px;
  }

  .drum-summary .drum-meta { display: contents; }

  .drum-remaining::before,
  .drum-count::before,
  .drum-state::before { content: none; }

  .drum-type,
  .drum-remaining,
  .drum-count,
  .drum-state {
    font-size: 12px;
    color: var(--text-muted);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

.pit-summary:hover .pit-name,
.drum-summary:hover .drum-name { text-decoration: underline; }

.pit-summary:focus-visible,
.drum-summary:focus-visible {
  outline: 2px solid var(--brand-highlight);
  outline-offset: -2px;
}

.pit-card details[open] > .pit-summary,
.drum-card details[open] > .drum-summary {
  border-bottom: 0.5px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
}

.pit-name,
.drum-name {
  font-size: 14px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A caret after the name so the card reads as openable, matching the drum
   stock cards on the reporting screen. */
.pit-name::after,
.drum-name::after {
  content: "";
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-left: 7px;
  border-right: 1.5px solid var(--text-muted);
  border-bottom: 1.5px solid var(--text-muted);
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.15s ease;
}

.pit-card details[open] .pit-name::after,
.drum-card details[open] .drum-name::after {
  transform: translateY(1px) rotate(-135deg);
}

.pit-meta,
.drum-meta {
  font-size: 12px;
  color: var(--text-muted);
}

.pit-edit,
.drum-edit {
  position: absolute;
  top: 4px;
  right: 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius);
  color: var(--text-secondary);
  text-decoration: none;
}

.pit-edit:hover,
.drum-edit:hover {
  background: var(--bg-accent);
  color: var(--text-accent);
}

.pit-edit:focus-visible {
  outline: 2px solid var(--brand-highlight);
  outline-offset: -2px;
}

/* The pencil on a cable testing row. The row itself opens the certificate
   or the certify form, so this is the only way from here to the cable — the
   same affordance the drums and pits lists have, in the same place. */
.test-row {
  position: relative;
}

.test-row > .test-card {
  /* Room for the pencil, so a long route does not run under it. */
  padding-right: 46px;
}

/* The C/TC sheet a cable is certified on. Above the route on a phone card,
   its own first column on a wide row. Tabular figures so 3.011 and 3.111
   line up down the list. */
.test-ctc {
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
}

.test-ctc-head {
  align-self: center;
}

.test-edit {
  position: absolute;
  top: 50%;
  right: 4px;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius);
  color: var(--text-secondary);
  text-decoration: none;
}

.test-edit:hover {
  background: var(--bg-accent);
  color: var(--text-accent);
}

.test-edit:focus-visible {
  outline: 2px solid var(--brand-highlight);
  outline-offset: -2px;
}

/* The segmented control on a form row, where it has the full width of the
   control column rather than a filter popover's 232px. */
.segset-wide { width: 100%; }

/* Indented from the header that opens it, with a rule down the left, so
   an opened card reads as belonging to the row above rather than as the
   next row in the list. The summary's own text starts at 14px; these start
   at 28px, which is enough to see at a glance without pushing a long cable
   name into an ellipsis. */
.cable-rows {
  padding: 4px 14px 10px 28px;
  position: relative;
}

.cable-rows::before {
  content: "";
  position: absolute;
  left: 16px;
  top: 2px;
  bottom: 8px;
  width: 1px;
  background: var(--border-strong);
}

/* A row of cable, wherever cables are listed: inside a pit, inside a drum,
   and on the testing readiness screen. It was called .pit-cable-row until
   three of the four places using it stopped being pits. */
.cable-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 0;
  border-top: 0.5px solid var(--border);
  font-size: 13px;
  color: var(--text-primary);
  text-decoration: none;
  min-height: var(--tap-min);
}

.cable-rows > .cable-row:first-child { border-top: none; }

.cable-row:hover { background: var(--bg-accent); }

.cable-row-where {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cable-row-meta {
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
}

.cable-rows-empty {
  margin: 0;
  padding: 6px 0 2px;
  font-size: 12px;
  color: var(--text-muted);
}

/* A cable row on the pit edit screen: the row itself opens the cable, the
   x beside it takes the cable out of this pit. */
.pit-cable-edit {
  display: flex;
  align-items: center;
  gap: 6px;
  border-top: 0.5px solid var(--border);
}

.pit-cable-edit-link {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 4px;
  font-size: 13px;
  color: var(--text-primary);
  text-decoration: none;
  min-height: var(--tap-min);
}

.pit-cable-edit-link:hover { background: var(--bg-accent); }

.pit-cable-remove {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  min-height: 36px;
  padding: 0;
  border: none;
  border-radius: var(--radius);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
}

.pit-cable-remove:hover {
  background: var(--bg-danger);
  color: var(--text-danger);
}

/* ---- staged removals ---------------------------------------------------- *
 * Removed cables move here rather than vanishing. Nothing has happened in
 * the database yet, so the group has to look pending rather than done.
 */
.removing-group {
  margin-top: 14px;
  padding: 10px 12px;
  border: 1px dashed var(--text-warning);
  border-radius: var(--radius);
  background: var(--bg-warning);
}

.removing-group[hidden] { display: none; }

.removing-group h3 {
  margin: 0;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-warning);
}

.removing-note {
  margin: 2px 0 6px;
  font-size: 11px;
  color: var(--text-warning);
}

.pit-cable-edit.is-removing {
  border-top: none;
  opacity: 0.75;
}

.pit-cable-edit.is-removing .cable-row-where {
  text-decoration: line-through;
}

.btn-undo {
  flex: none;
  min-height: 34px;
  padding: 0 12px;
  border: 0.5px solid var(--text-warning);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text-warning);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
}

.btn-undo:hover {
  background: var(--text-warning);
  color: #ffffff;
}

/* ---- add panels behind the "+" buttons ---------------------------------- *
 * The list screens hide their add form behind the same "+" as cable runs,
 * so the button means one thing everywhere and the screen stays a list
 * until you actually want to add something.
 */
.list-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  max-width: 720px;
  margin-bottom: 10px;
}

.add-panel {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  padding: 14px;
  margin-bottom: 20px;
  max-width: 520px;
}

.add-panel[hidden] { display: none; }

.add-panel .primary {
  background: var(--fill-accent);
  color: var(--on-accent);
  border-color: var(--fill-accent);
  font-weight: 600;
}

.add-panel input[type="text"],
.add-panel input[type="number"],
.add-panel select {
  flex: 1 1 140px;
  min-width: 0;
}

/* form.as_p on the drums panel emits <p> wrappers. */
.add-panel p {
  margin: 0 0 10px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.add-panel p label {
  font-size: 12px;
  color: var(--text-secondary);
}

/* ---- reporting: chart heading + its options dropdown -------------------- */

/* Never wraps: the options popover hangs off the chip's right edge, so if
   the chip dropped to its own line and moved left, the panel would open off
   the side of the screen. The heading gives way instead. */
.chart-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: nowrap;
  margin-bottom: 8px;
  max-width: 900px;
}

.chart-head h3 {
  margin: 0;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (max-width: 560px) {
  .chart-head h3 { font-size: 14px; }
}

/* A quiet chip that reads as the chart's current state first and a control
   second, so the heading row stays calm. The popover opens from its right
   edge, staying on screen at 375px like the cable-runs filter. */
.chart-options > summary {
  min-width: 0;
  gap: 6px;
  padding: 0 10px 0 12px;
  min-height: 34px;
  font-size: 12px;
  font-weight: 500;
  white-space: nowrap;
  color: var(--text-secondary);
  background: transparent;
  border-color: var(--border);
}

.chart-options > summary:hover {
  background: var(--surface-2);
  color: var(--text-primary);
}

.chart-options > summary .chev {
  color: var(--text-muted);
  transition: transform 0.15s ease;
  flex: none;
}

.chart-options[open] > summary .chev { transform: rotate(180deg); }

.chart-options .filter-panel { min-width: 232px; }

/* Labels sit tight above their control rather than reading as form fields. */
.chart-options .filter-panel .field { gap: 6px; }

.chart-options .filter-panel label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* ---- segmented control -------------------------------------------------- *
 * One connected group of options, the selected one filled. Every choice is
 * visible and one tap away, unlike a <select> which costs a tap to open and
 * hides the alternatives behind it.
 */
.segset {
  display: flex;
  border: 0.5px solid var(--border-strong);
  border-radius: var(--radius);
  overflow: hidden;
}

.segset button {
  flex: 1 1 0;
  min-width: 0;
  min-height: 36px;
  padding: 0 8px;
  border: none;
  border-left: 0.5px solid var(--border-strong);
  border-radius: 0;
  background: var(--surface-2);
  color: var(--text-secondary);
  font-size: 12px;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
}

.segset button:first-child { border-left: none; }

.segset button:hover { background: var(--bg-accent); color: var(--text-primary); }

.segset button.is-active {
  background: var(--fill-accent);
  color: var(--on-accent);
  font-weight: 600;
}

.segset button:focus-visible {
  outline: 2px solid var(--brand-highlight);
  outline-offset: -2px;
}

/* The same control made of radios, so a choice submits with the form rather
   than needing a script to copy it into a hidden field. The reporting
   dashboard's chart is drawn on the server now, and a control that cannot
   be used without JavaScript would have taken that away again.

   The input is moved off the label rather than display:none'd: a hidden
   input is not focusable, and the arrow keys that walk a radio group would
   have stopped working. */
.segset label {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  min-height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 8px;
  border-left: 0.5px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--text-secondary);
  font-size: 12px;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
}

.segset label:first-child { border-left: none; }

.segset label input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.segset label:hover { background: var(--bg-accent); color: var(--text-primary); }

.segset label:has(input:checked) {
  background: var(--fill-accent);
  color: var(--on-accent);
  font-weight: 600;
}

.segset label:has(input:focus-visible) {
  outline: 2px solid var(--brand-highlight);
  outline-offset: -2px;
}

/* "All stages" is a link now — it clears the ticks by going to the report
   without any stage in the query, which needs no script. It has to be told
   to look like the button it replaced. */
a.btn-pager {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

/* Nothing ticked IS every stage, and this is the control that says so. It
   has carried an is-active class since the stage filter was built and there
   has never been a rule for it, so the state it was announcing was invisible
   — an empty row of boxes over a full report still read as a fault. */
.btn-pager.is-active {
  border-color: var(--fill-accent);
  background: var(--fill-accent);
  color: var(--on-accent);
  font-weight: 600;
}

/* ---- reporting: layout + drum stock cards ------------------------------ */

/* One chart, drawn twice, shown once. The text in an SVG scales with the
   SVG, so the trend chart drawn 880 wide had 4px labels on a phone. The
   narrow copy is drawn at a phone's own size instead, so its labels are the
   size they are written at. Both are the same figures — see
   reporting/charts.py. */
.chart-narrow { display: none; }

@media (max-width: 640px) {
  .chart-wide { display: none; }
  .chart-narrow { display: block; }
}

/* ---- reporting: panels and tables -------------------------------------- *
 * The two charts share a row and everything else runs the full width under
 * them. Before this the page was a stack with the drum stock squeezed into a
 * narrow right-hand column: the widest panel on the screen had the least
 * room, and nothing lined up with anything else. Jason's layout, 2026-09-12.
 */
.report-charts {
  display: grid;
  /* Equal columns, and both charts drawn at the same size, so the two
     panels end level. They were 1.15fr to 1fr with each chart sized to its
     own content, which left a hole under the shorter one — Jason,
     2026-09-24. */
  grid-template-columns: 1fr 1fr;
  gap: 14px;
  align-items: stretch;
  margin-bottom: 14px;
}

@media (max-width: 900px) {
  .report-charts { grid-template-columns: 1fr; }
}

/* Every block on the report is the same card, so their edges line up down
   the page rather than each panel finding its own margin. */
.report-panel {
  background: var(--surface-2);
  border: 0.5px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  padding: 12px 14px 14px;
  margin-bottom: 14px;
  min-width: 0;
}

.report-panel .chart-head { margin-bottom: 6px; }

.report-panel .section-note {
  font-size: 11px;
  color: var(--text-muted);
}

/* A table with nine columns does not fit a phone and never will, so it
   scrolls inside its own panel rather than pushing the page sideways. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.rep-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
  white-space: nowrap;
}

.rep-table th,
.rep-table td {
  padding: 7px 10px;
  text-align: left;
  border-bottom: 0.5px solid var(--border);
}

.rep-table thead th {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border-strong);
  position: sticky;
  top: 0;
  background: var(--surface-2);
}

.rep-table tbody th {
  font-weight: 600;
  color: var(--text-primary);
}

/* Tabular figures so a column of numbers lines up on the digit rather than
   only on its right edge. */
.rep-table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.rep-table tfoot th,
.rep-table tfoot td {
  border-top: 1px solid var(--border-strong);
  border-bottom: none;
  font-weight: 700;
  background: var(--surface-1);
}

/* The share, beside the count rather than instead of it: the count is what
   gets reported and the percentage is how it is read at a glance. */
.rep-pct {
  color: var(--text-muted);
  font-size: 11px;
  font-weight: 400;
  margin-left: 3px;
}

.rep-bad { color: var(--text-danger); font-weight: 700; }

/* Count and ring on one line, the ring reading as the share of the number
   beside it rather than as a figure of its own. The same rings the exported
   sheet draws, from the same include — one report, two places it is read. */
.rep-table .pair { white-space: nowrap; }

.rep-table .pair-n {
  display: inline-block;
  min-width: 20px;
  text-align: right;
  vertical-align: middle;
}

.rep-table .ring {
  width: 26px;
  height: 26px;
  vertical-align: middle;
  margin-left: 5px;
}

/* Left of this rule everything counts cables, right of it everything counts
   places. One of the table's own lines rather than a heavy divider. */
.rep-table .rep-split { border-left: 1px solid var(--border-strong); }

.rep-table .rep-total { font-weight: 700; color: var(--text-primary); }

.rep-table .rep-group th {
  font-size: 10px;
  letter-spacing: 0.06em;
  color: var(--text-accent);
  text-align: center;
  border-bottom: none;
  padding-bottom: 2px;
}

.rep-table .rep-group th:first-child { border: none; }

.rep-empty {
  color: var(--text-muted);
  font-size: 13px;
  text-align: center;
  padding: 18px 0;
}

/* The cable type opens the breakdown, so it is a button — it reads as the
   heading it is and behaves as the control it is. */
.stock-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.stock-toggle:hover { color: var(--text-link); }

.stock-toggle[aria-expanded="true"] .chev { transform: rotate(180deg); }

.stock-toggle .chev {
  flex: none;
  color: var(--text-muted);
  transition: transform 0.15s ease;
}

/* The status on the row, in words as well as colour: a state told only by a
   shade is easy to miss in daylight, and harder still if you don't
   distinguish red from green. */
.stock-pill {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
}

.stock-pill.is-critical,
.stock-pill.is-red {
  background: var(--bg-danger);
  color: var(--text-danger);
}

.stock-pill.is-amber {
  background: var(--bg-warning);
  color: var(--text-warning);
}

.stock-pill.is-green {
  background: var(--bg-success);
  color: var(--text-success);
}

/* The left edge carries the status too, so a row can be found by running
   down the table rather than by reading every pill. */
.stock-row.is-critical th[scope="row"],
.stock-row.is-red th[scope="row"] { box-shadow: inset 3px 0 0 var(--text-danger); }
.stock-row.is-amber th[scope="row"] { box-shadow: inset 3px 0 0 var(--text-warning); }
.stock-row.is-green th[scope="row"] { box-shadow: inset 3px 0 0 var(--text-success); }

.stock-detail-row-wrap > td {
  padding: 0;
  background: var(--surface-1);
}

.stock-detail-row-wrap .stock-detail {
  white-space: normal;
  border-radius: 0;
}

/* Desktop puts the two panels side by side. A phone stacks them, metres
   chart first, so the drum cards get the full width instead of ~140px. */
.report-split {
  display: grid;
  grid-template-columns: 1.3fr 1fr;
  gap: 1.5rem;
  align-items: start;
  max-width: 1100px;
}

@media (max-width: 720px) {
  .report-split {
    grid-template-columns: 1fr;
    gap: 1.5rem;
  }
}

.stock-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* The card is a <details>: the summary is the at-a-glance state, opening
   it shows the drums and the runs waiting on them. */
.stock-card {
  border: 0.5px solid var(--border);
  border-left: 3px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

/* Name and drum count share the top line, the length sits right-aligned
   against them, and any warning runs full width underneath. */
.stock-summary {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 2px 10px;
  padding: 10px 12px;
  cursor: pointer;
  list-style: none;
  border-radius: inherit;
}

.stock-summary::-webkit-details-marker { display: none; }

.stock-summary:hover .stock-name { text-decoration: underline; }

.stock-card[open] > .stock-summary {
  border-bottom: 0.5px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
}

.stock-summary:focus-visible {
  outline: 2px solid var(--brand-highlight);
  outline-offset: -2px;
}

/* A caret after the name, rotating when open, so it reads as openable
   rather than as a static tile. */
.stock-name::after {
  content: "";
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-left: 7px;
  border-right: 1.5px solid var(--text-muted);
  border-bottom: 1.5px solid var(--text-muted);
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.15s ease;
}

.stock-card[open] .stock-name::after {
  transform: translateY(1px) rotate(-135deg);
}

.stock-detail {
  padding: 10px 12px 12px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  background: var(--surface-1);
  border-radius: 0 0 var(--radius) var(--radius);
}

.stock-detail h4 {
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.stock-detail-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

/* Every row links to the thing it names, so the panel is a way in rather
   than a dead-end readout. */
.stock-detail-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 7px 4px;
  border-top: 0.5px solid var(--border);
  font-size: 13px;
  color: var(--text-primary);
  text-decoration: none;
  min-height: var(--tap-min);
}

.stock-detail-list li:first-child .stock-detail-row { border-top: none; }

.stock-detail-row:hover { background: var(--bg-accent); }

.stock-detail-row.is-muted { color: var(--text-muted); }

.stock-detail-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.stock-detail-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.stock-detail-sub {
  font-size: 11px;
  color: var(--text-muted);
}

.stock-detail-value {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--text-secondary);
}

.stock-detail-empty {
  margin: 0;
  font-size: 12px;
  color: var(--text-muted);
}

/* Pager for a detail list that runs past one page. */
.detail-pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 10px;
  padding-top: 8px;
  border-top: 0.5px solid var(--border);
}

.detail-pager-count {
  font-size: 11px;
  color: var(--text-muted);
}

.detail-pager-buttons {
  display: flex;
  gap: 6px;
}

.btn-pager {
  min-height: 34px;
  padding: 0 12px;
  border: 0.5px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text-primary);
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
}

.btn-pager:hover:not(:disabled) {
  border-color: var(--fill-accent);
  background: var(--bg-accent);
}

/* Kept visible rather than hidden at the ends, so the control doesn't move
   around under a thumb as you page. */
.btn-pager:disabled {
  opacity: 0.45;
  cursor: default;
}

.stock-name {
  font-size: 13px;
  font-weight: 500;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The length is the number being colour-coded, so it gets the weight. */
.stock-length {
  grid-column: 2;
  grid-row: 1;
  font-size: 15px;
  font-weight: 600;
  text-align: right;
  white-space: nowrap;
}

.stock-drums {
  grid-column: 1;
  grid-row: 2;
  font-size: 12px;
  color: var(--text-muted);
}

.stock-note {
  grid-column: 1 / -1;
  font-size: 11px;
  margin-top: 4px;
  padding-top: 6px;
  border-top: 0.5px solid var(--border);
}

/* Colour carries on the left edge as well as the figure: a status told
   only by the shade of a number is easy to miss in daylight, and harder
   still if you don't distinguish red from green. */
.stock-card.stock-red,
.stock-card.stock-critical {
  border-left-color: var(--text-danger);
  background: var(--bg-danger);
}
.stock-card.stock-red .stock-length,
.stock-card.stock-red .stock-note,
.stock-card.stock-critical .stock-length,
.stock-card.stock-critical .stock-note { color: var(--text-danger); }

/* Out of cable with runs still to go — the one state that should catch
   your eye without being looked for.
 *
 * A slow pulse, not a flash: anything faster than three cycles a second
 * can trigger photosensitive seizures (WCAG 2.3.1), and this is a work
 * screen people have open all day. The colour never leaves, so the card
 * still reads correctly in a screenshot or at the bottom of the cycle. */
@keyframes stock-pulse {
  0%, 100% {
    border-left-color: var(--text-danger);
    box-shadow: 0 0 0 0 rgba(192, 57, 43, 0);
  }
  50% {
    border-left-color: #e8574a;
    box-shadow: 0 0 0 3px rgba(192, 57, 43, 0.22);
  }
}

.stock-card.stock-critical {
  border-left-width: 4px;
  animation: stock-pulse 1.6s ease-in-out infinite;
}

.stock-card.stock-critical[hidden] { display: none; }

/* The missing-estimate card sits above the stock list and needs to read as
   a caveat on it, not as one of the cable types. */
#missingEstimates { margin-bottom: 10px; }

/* Honour a system setting that asks for no motion: the card keeps the
   heavier border and a solid ring, it just stops moving. */
@media (prefers-reduced-motion: reduce) {
  .stock-card.stock-critical {
    animation: none;
    box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.3);
  }
}

.stock-card.stock-amber {
  border-left-color: var(--text-warning);
  background: var(--bg-warning);
}
.stock-card.stock-amber .stock-length,
.stock-card.stock-amber .stock-note { color: var(--text-warning); }

.stock-card.stock-green {
  border-left-color: var(--text-success);
}
.stock-card.stock-green .stock-length { color: var(--text-success); }

/* The open panel sits on the plain card surface whatever the card's status
   tint, so the drum and run rows stay readable.

   It was a literal #ffffff, written before dark mode existed and missed by
   the token pass. On a dark card that put --text-primary — near-white — on
   white, so opening a stock card produced a panel nobody could read a
   figure off. Jason reported it 2026-09-12. The token is the whole point:
   there is no colour here that does not flip. */
.stock-card .stock-detail { background: var(--surface-2); }

/* ---- password show/hide ------------------------------------------------ *
 * The eye sits inside the field's right edge. The input gets padding on
 * that side so a long password scrolls under the label, never behind the
 * button where it can't be read.
 */
.pw-field {
  position: relative;
  display: block;
}

.pw-field input {
  width: 100%;
  padding-right: 44px;
}

.pw-reveal {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: 42px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  min-height: 0;
  border: none;
  background: none;
  color: var(--text-secondary);
  border-radius: 0 var(--radius) var(--radius) 0;
  cursor: pointer;
}

.pw-reveal:hover {
  color: var(--text-primary);
}

/* Focus lives on the input behind it, so the button gets a ring of its own
   only when it is itself focused — otherwise the field would look doubly
   outlined while someone is simply typing. */
.pw-reveal:focus-visible {
  outline: 2px solid var(--brand-highlight);
  outline-offset: -2px;
}

/* Chrome and Safari add their own reveal control to password inputs, which
   would sit next to this one and do the same job. */
.pw-field input::-ms-reveal,
.pw-field input::-ms-clear,
.pw-field input::-webkit-credentials-auto-fill-button {
  display: none;
}

/* ---- form actions: primary next to a way out --------------------------- *
 * Save and Cancel sit on one row at the foot of a form. Cancel is a link,
 * not a button, so it can never submit anything — the whole point of it is
 * that a record can be opened and read without being written back.
 */
/* Save and Cancel take half the row each. Equal weight suits the pair:
   on a form you may have opened only to read, backing out is as ordinary
   an outcome as saving. */
.form-actions {
  display: flex;
  align-items: stretch;
  gap: 8px;
  flex-wrap: nowrap;
  max-width: 440px;
}

/* A class that sets display beats the browser's own [hidden] rule, so both
   reports went on showing the Apply button their scripts had already hidden
   — a second way to do what every control on the page does by itself. */
.form-actions[hidden] { display: none; }

/* ---- numeric column filters ------------------------------------------- */

/* An operator and one number, for the columns where a list of every
   distinct value would be no use — length, remaining. */
.col-filter-num { padding: 8px 10px 0; }

.num-filter-row {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 8px;
}

.num-filter-row select {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 34px;
  font-size: 12px;
}

.num-filter-row input {
  flex: 0 1 78px;
  min-width: 0;
  min-height: 34px;
  font-size: 12px;
  text-align: right;
}

.num-filter-unit {
  flex: none;
  font-size: 12px;
  color: var(--text-secondary);
}

/* The tick-box columns count what is ticked; a numeric one has nothing to
   count, so it marks itself with a dot of the same weight. */
.col-count-dot {
  font-size: 15px;
  line-height: 1;
}

.form-actions > * {
  flex: 1 1 0;
  min-width: 0;
  text-align: center;
}

.form-actions .primary {
  background: var(--fill-accent);
  color: var(--on-accent);
  border-color: var(--fill-accent);
  font-weight: 600;
  padding: 9px 18px;
}

.btn-secondary,
.form-actions .btn-secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  padding: 9px 18px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text-primary);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.01em;
  text-decoration: none;
  cursor: pointer;
}

.btn-secondary:hover,
.form-actions .btn-secondary:hover {
  border-color: var(--fill-accent);
}

/* ---- list filter bar + result counts ---------------------------------- */

/* Single row, never wrapping: field then icon buttons. The field is the
   only thing that gives, so the buttons stay in the same place whatever
   the width. */
.list-filters {
  display: flex;
  gap: 8px;
  flex-wrap: nowrap;
  align-items: center;
  max-width: 720px;
  margin-bottom: 10px;
}

.list-filters input[type="search"],
.list-filters > .search-with-clear {
  flex: 1 1 0;
  min-width: 0;
}

.list-filters > .icon-btn,
.list-filters > a.icon-btn {
  flex: none;
  padding: 0 10px;
  text-decoration: none;
}

.list-filters select {
  flex: 0 0 auto;
}

.result-count {
  font-size: 12px;
  color: var(--text-secondary);
  margin-bottom: 12px;
}

/* ---- required-field markers ------------------------------------------- *
 * Derived from the input's own `required` attribute rather than hand-added
 * to each label, so a field can never say "optional" while the server
 * rejects it as missing. :has() is supported by every browser this app
 * targets; where it isn't, the marker is simply absent and nothing breaks.
 */
.form-stack .field:has(input[required], select[required], textarea[required]) > label::after,
.form-stack .field:has(input[required], select[required], textarea[required]) > label > .label-text::after {
  content: " *";
  color: var(--text-danger);
  font-weight: 600;
}

/* ---- filter icon button + panel --------------------------------------- */

.filter-menu {
  position: relative;
  flex: 0 0 auto;
}

.filter-menu > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  padding: 0 10px;
  border: 0.5px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text-primary);
  font-size: 13px;
  user-select: none;
}

.filter-menu > summary::-webkit-details-marker { display: none; }
.filter-menu[open] > summary {
  border-color: var(--fill-accent);
  box-shadow: 0 0 0 2px var(--bg-accent);
}

.filter-badge {
  background: var(--brand-highlight);
  color: #ffffff;
  font-size: 11px;
  font-weight: 600;
  min-width: 18px;
  height: 18px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 5px;
}

/* The filter is the last thing in the toolbar row, so the popover hangs
   off its right edge and opens leftward — it stays on screen at 375px
   without having to break the row up. */
.filter-panel {
  position: absolute;
  z-index: 20;
  top: calc(100% + 6px);
  right: 0;
  min-width: 240px;
  max-width: calc(100vw - 32px);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-popover);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.filter-panel .field {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.filter-panel label {
  font-size: 12px;
  color: var(--text-secondary);
}

.filter-panel select { width: 100%; }

.filter-actions {
  display: flex;
  align-items: center;
  gap: 12px;
}

.filter-actions .primary {
  background: var(--fill-accent);
  color: var(--on-accent);
  border-color: var(--fill-accent);
  font-weight: 600;
}

/* ---- header overflow menu (three dots) -------------------------------- */

details.menu { position: relative; }

details.menu > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  border-radius: var(--radius);
  border: 1px solid transparent;
  color: #ffffff;
}

details.menu > summary::-webkit-details-marker { display: none; }
details.menu > summary:hover,
details.menu[open] > summary { border-color: rgba(255, 255, 255, 0.35); }

.menu-panel {
  position: absolute;
  z-index: 30;
  top: calc(100% + 6px);
  right: 0;
  min-width: 216px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-popover);
  padding: 6px;
  display: flex;
  flex-direction: column;
}

.menu-panel a,
.menu-panel button {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 10px 10px;
  min-height: var(--tap-min);
  border: none;
  background: none;
  border-radius: var(--radius);
  color: var(--text-primary);
  font-size: 14px;
  font-family: inherit;
  text-decoration: none;
  text-align: left;
  cursor: pointer;
  width: 100%;
}

.menu-panel a:hover,
.menu-panel button:hover { background: var(--surface-1); }

.menu-panel .menu-sep {
  height: 1px;
  background: var(--border);
  margin: 5px 4px;
}

/* ---- a two-way switch made of links ------------------------------------- *
 * The segmented look of the form switches, for a choice that is a filter in
 * the URL rather than a field: each side is a link, so it works with
 * scripting off and the state survives a reload.
 */
.seg-links {
  display: inline-flex;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 14px;
}

.seg-links a {
  display: flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: 0 16px;
  color: var(--text-primary);
  text-decoration: none;
  font-size: 14px;
}

.seg-links a + a { border-left: 1px solid var(--border-strong); }

.seg-links a[aria-current="true"] {
  background: var(--fill-accent);
  color: var(--on-accent);
  font-weight: 600;
}

/* ---- project home --------------------------------------------------------- *
 * The screens as tiles, grouped install / test / report / project (Jason
 * approved it from a mock-up, 2026-09-26). Two across on a phone, as many as
 * fit on a desktop.
 */
.home-role {
  font-size: 13px;
  color: var(--text-muted);
  margin-bottom: 16px;
}

.home-groups {
  display: flex;
  flex-direction: column;
  gap: 18px;
  max-width: var(--page-max);
}

.home-group {
  margin: 0 0 8px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.home-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(160px, 45%), 1fr));
  gap: 10px;
}

@media (min-width: 640px) {
  .home-tiles { grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }
}

.home-tile {
  display: flex;
  align-items: center;
  min-height: 64px;
  font-weight: 600;
  text-decoration: none;
  color: inherit;
}

.home-tile:hover { border-color: var(--border-strong); }

/* ---- test certificate ---------------------------------------------------- *
 * One column on a phone; on a wide screen the record on the left and the
 * signature, the cable's history and New test on the right (Jason approved
 * it from a mock-up, 2026-09-26).
 */
.cert-layout {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: 520px;
}

.cert-side {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.cert-side .form-actions { margin-top: 0; }

@media (min-width: 900px) {
  .cert-layout {
    display: grid;
    grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
    align-items: start;
    gap: 16px;
    max-width: var(--page-max);
  }
}

.cert-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 0;
  border-bottom: 0.5px solid var(--border);
  font-size: 13px;
}

.cert-label { color: var(--text-secondary); }

.cert-caption {
  font-size: 12px;
  color: var(--text-secondary);
  margin-bottom: 4px;
}

/* Fixed light in both themes, like the pad it was signed on: the ink is
   dark and the stored image is what a certificate prints. */
.cert-signature {
  display: block;
  max-width: 280px;
  width: 100%;
  border: 0.5px solid var(--border);
  border-radius: var(--radius);
  background: #fff;
}

.cert-photo {
  display: block;
  max-width: 100%;
  border-radius: var(--radius);
}

/* ---- project logo on project admin --------------------------------------- */
.logo-card { margin-bottom: 16px; }
.logo-title { font-size: 14px; margin: 0 0 8px; }

.logo-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 12px;
}

/* On a white ground whatever the theme, as it will print. */
.logo-preview {
  max-height: 56px;
  max-width: 180px;
  padding: 6px 8px;
  border-radius: var(--radius);
  background: #fff;
  border: 0.5px solid var(--border);
}

.logo-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
}

.logo-form input[type="file"] { flex: 1; min-width: 0; font-size: 13px; }

/* ---- project members: table on a wide screen, tiles on a phone ------------ *
 * Jason approved the tiles from a mock-up, 2026-09-26: a tile per person
 * that opens, and every field the same width. One or the other is shown;
 * each posts to its own form, so the hidden one never sends a thing.
 */
.member-table-wrap { overflow-x: auto; }
.member-tiles { display: none; }

@media (max-width: 759px) {
  .member-table-wrap { display: none; }
  .member-tiles { display: flex; flex-direction: column; gap: 8px; }
}

.member-tile {
  background: var(--surface-1);
  border: 0.5px solid var(--border);
  border-radius: var(--radius);
}

.member-tile > summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 56px;
  padding: 10px 14px;
  cursor: pointer;
}

.member-tile > summary::-webkit-details-marker { display: none; }

.member-who { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.member-name { font-weight: 600; }

.member-email {
  font-size: 13px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.member-chevron { flex: none; color: var(--text-muted); transition: transform 0.15s; }
.member-tile[open] .member-chevron { transform: rotate(180deg); }

@media (prefers-reduced-motion: reduce) {
  .member-chevron { transition: none; }
}

.member-fields { padding: 0 14px 12px; }

.member-field {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 48px;
  border-top: 0.5px solid var(--border);
}

.member-field label { font-size: 13px; color: var(--text-secondary); }

/* One width for every control, so the column lines up — Jason's note. The
   date picker wraps its box, so the wrapper takes the width and the box
   fills it. */
.member-field > select,
.member-field > input[type="text"],
.member-field > .dp {
  flex: none;
  width: 190px;
  max-width: 58%;
}

.member-field .dp-input { width: 100%; }
.member-field > input[type="checkbox"] { flex: none; }

.member-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  padding-top: 10px;
}

.member-actions button { min-height: var(--tap-min); }

/* ---- reporting on a phone -------------------------------------------------- *
 * Jason approved it from a mock-up, 2026-09-26. Drum Stock becomes a row per
 * cable type — name and state, then its four figures — and the progress
 * table splits at its rule into a cables table and a locations table
 * (.rep-locs-phone), with shorter headings so the cables half fits. Wide
 * screens keep both tables as they were.
 */
.rep-short,
.rep-locs-phone { display: none; }

@media (max-width: 759px) {
  .rep-long { display: none; }
  .rep-short { display: inline; }
  .rep-table .rep-loc { display: none; }
  .rep-locs-phone { display: block; margin-top: 14px; }

  /* Tighter on a phone so the six cable columns and their rings fit
     without the table scrolling sideways. */
  .rep-table:not(.stock-table) th,
  .rep-table:not(.stock-table) td {
    padding-left: 3px;
    padding-right: 3px;
    font-size: 12.5px;
    white-space: nowrap;
  }

  .rep-table:not(.stock-table) thead th { font-size: 10.5px; letter-spacing: 0.03em; }
  .rep-table .rep-group th { white-space: normal; }
  .rep-table svg.ring { width: 24px; height: 24px; }
  .rep-table .pair { gap: 3px; }

  .rep-locs-title {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-link);
    margin-bottom: 4px;
  }

  .stock-table,
  .stock-table tbody { display: block; }
  .stock-table thead { display: none; }

  .stock-table tr.stock-row {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 6px 8px;
    padding: 10px 12px;
    border-top: 0.5px solid var(--border);
  }

  .stock-table tr.stock-row > * {
    padding: 0;
    border: 0;
    text-align: left;
    box-shadow: none !important;
  }

  .stock-table tr.stock-row > th { grid-column: 1 / 4; grid-row: 1; }
  .stock-table tr.stock-row > td:last-child { grid-column: 4; grid-row: 1; justify-self: end; }
  .stock-table tr.stock-row > td.num { grid-row: 2; font-variant-numeric: tabular-nums; }

  .stock-table tr.stock-row > td.num::before {
    display: block;
    font-size: 11px;
    color: var(--text-muted);
  }

  .stock-table tr.stock-row > td.num:nth-of-type(1)::before { content: "Drums"; }
  .stock-table tr.stock-row > td.num:nth-of-type(2)::before { content: "On the drums"; }
  .stock-table tr.stock-row > td.num:nth-of-type(3)::before { content: "Still to pull"; }
  .stock-table tr.stock-row > td.num:nth-of-type(4)::before { content: "Cover"; }

  .stock-row.is-critical,
  .stock-row.is-red { box-shadow: inset 3px 0 0 var(--text-danger); }
  .stock-row.is-amber { box-shadow: inset 3px 0 0 var(--text-warning); }
  .stock-row.is-green { box-shadow: inset 3px 0 0 var(--text-success); }

  .stock-table tr.stock-detail-row-wrap:not([hidden]),
  .stock-table tr.stock-detail-row-wrap > td { display: block; }
}

/* ---- paging ---------------------------------------------------------------- *
 * The count and how many to a page on one row, and Previous / Next under
 * the list (Jason, 2026-09-26: 50, 100, 200 or all).
 */
.list-count-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 16px;
  margin-bottom: 10px;
}

.list-count-row .result-count { margin: 0; }

.seg-links-small { margin-bottom: 0; }
.seg-links-small a { padding: 0 12px; font-size: 13px; }

.pager {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 12px;
  margin-top: 16px;
}

.pager > a:last-child { justify-self: end; }
.pager-at { font-size: 13px; color: var(--text-secondary); font-variant-numeric: tabular-nums; }

/* ---- the cables at one location ------------------------------------------ */
.location-cables {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.location-cable {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  text-decoration: none;
  color: inherit;
}

.location-cable.is-waiting { opacity: 0.75; }
.location-cable-route { font-weight: 500; font-size: 13px; }
.location-cable-meta { font-size: 12px; color: var(--text-muted); }

.location-cable-state {
  flex: none;
  font-size: 11px;
  padding: 3px 8px;
  border-radius: var(--radius);
}

.location-cable-state.is-good { background: var(--bg-success); color: var(--text-success); }
.location-cable-state.is-bad { background: var(--bg-danger); color: var(--text-danger); }

/* ---- info tips ---------------------------------------------------------- *
 * An explanation behind a small (i): shown on hover, or on tap, where the
 * button takes focus (Jason, 2026-09-26 — the screens should be clear
 * without a caption beside every function). core/templates/core/info_tip.html.
 */
.info-tip {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 30px;
  height: 30px;
  padding: 0;
  margin: 0;
  border: none;
  border-radius: 50%;
  background: none;
  color: var(--text-muted);
  cursor: help;
  vertical-align: middle;
}

.info-tip:hover,
.info-tip:focus-visible { color: var(--text-link); }

.info-tip:focus-visible {
  outline: 2px solid var(--brand-highlight);
  outline-offset: 1px;
}

.info-tip-text {
  display: none;
  position: absolute;
  z-index: 40;
  top: calc(100% + 4px);
  left: -6px;
  width: max-content;
  max-width: min(260px, 80vw);
  padding: 8px 10px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface-2);
  box-shadow: var(--shadow-popover);
  color: var(--text-primary);
  font-size: 12px;
  font-weight: 400;
  line-height: 1.45;
  letter-spacing: normal;
  text-transform: none;
  text-align: left;
  white-space: normal;
}

.info-tip:hover .info-tip-text,
.info-tip:focus .info-tip-text { display: block; }

/* In a three-dot menu the (i) sits at the end of its item's row: a menu item
   is a link, and a button cannot go inside one. */
.menu-panel .menu-row {
  display: flex;
  align-items: center;
}

/* Every menu link is width:100% on its own; beside an (i) that squeezed it
   to a word a line, so in a row it takes its natural width and one line. */
.menu-panel .menu-row > a { flex: 1 1 auto; width: auto; white-space: nowrap; }

/* ".menu-panel button" makes every menu button a full-width row; the (i) is
   a button too, and took that width, pushing itself outside the menu. */
.menu-panel .menu-row > .info-tip {
  width: 30px;
  min-height: 30px;
  padding: 0;
  margin-right: 4px;
  background: none;
}

/* Menus hang off the right-hand edge, so their tips open leftwards; every
   other tip sits near the left and opens to the right. */
.menu-panel .info-tip-text { left: auto; right: -6px; }

/* On the navy header, beside the screen name. */
.page-title .info-tip { color: rgba(255, 255, 255, 0.72); }
.page-title .info-tip:hover,
.page-title .info-tip:focus-visible { color: #fff; }

/* Beside a heading or a lead line, the (i) keeps to the text's baseline. */
.with-tip {
  display: flex;
  align-items: center;
  gap: 2px;
}

.menu-panel .menu-note {
  font-size: 11px;
  color: var(--text-muted);
  padding: 4px 10px 6px;
}

/* ---- icon buttons ------------------------------------------------------ */

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  padding: 0 12px;
  border-radius: var(--radius);
  border: 0.5px solid var(--border-strong);
  background: var(--surface-2);
  color: var(--text-primary);
  cursor: pointer;
}

.icon-btn.primary {
  background: var(--fill-accent);
  border-color: var(--fill-accent);
  color: var(--on-accent);
  font-size: 20px;
  font-weight: 600;
  line-height: 1;
}

.linklike {
  border: none;
  background: none;
  padding: 0;
  min-height: 0;
  font-size: inherit;
  font-family: inherit;
  cursor: pointer;
  color: #ffffff;
}

/* ---- cable run card ---------------------------------------------------- *
 * Two columns. The right one is a fixed width, which is the whole point:
 * cards used to size their own columns from their own content, so the
 * length and type landed at a different x on every row and the eye had
 * nothing to follow down the list.
 */
.run-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 76px;
  gap: 3px 12px;
  align-items: baseline;
}

/* The route identifies the cable, so it carries the weight and takes
   whatever width is left. */
.run-card .run-where {
  grid-column: 1;
  grid-row: 1;
  font-weight: 600;
  font-size: 14px;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Tabular figures so the digits line up vertically, not just the column. */
.run-card .run-len {
  grid-column: 2;
  grid-row: 1;
  font-size: 13px;
  color: var(--text-secondary);
  text-align: right;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* On a phone the measured length sits under the estimate in the same
   column rather than beside it: 76px of card cannot hold two figures, and
   taking the width from the route would clip the one thing on the row that
   has to stay readable. Quieter than the estimate, because the estimate is
   what a planned run has and the measurement is what a pulled one adds. */
.run-card .run-act {
  grid-column: 2;
  grid-row: 2;
  font-size: 12px;
  color: var(--text-muted);
  text-align: right;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.run-card .run-act::before {
  content: "act ";
  font-size: 10px;
  letter-spacing: 0.03em;
}

/* Cable type, kind and stage read as one qualifying line rather than three
   competing columns. */
.run-card .run-meta {
  grid-column: 1 / -1;
  grid-row: 2;
  font-size: 12px;
  color: var(--text-muted);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.run-card .run-progress {
  grid-column: 1 / -1;
  grid-row: 3;
  margin-top: 5px;
}

/* The muted qualifying line on a phone: three cells read as one sentence,
   with the separators drawn rather than written into the template, so the
   wide layout can drop them without the markup changing. */
.run-card .run-kind::before,
.run-card .run-stage::before {
  /* A literal non-breaking space, middot, non-breaking space. Plain
     spaces collapse against the neighbouring text and the gap ends up
     on one side only; writing them as CSS hex escapes did not survive
     into the served file. */
  content: " · ";
}

.run-head {
  display: none;
}

/* The speed dial, on the phone cards as well as the wide rows — Jason,
   2026-09-26. It replaced the three-segment bar the cards used to carry, so
   the two layouts now show progress the same way. */
.run-card .pdonut {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-width: 0;
}

.pdonut svg {
  flex: none;
}

.pdonut-label {
  font-size: 12px;
  color: var(--text-muted);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pdonut.is-ready .pdonut-label {
  color: var(--text-success);
  font-weight: 600;
}

/* Green at every filled step, exactly as the bar's segments are — the ring
   shows how far along the cable is, not a different kind of state. */
.pdonut.is-ready,
.pdonut.is-tested,
.pdonut.is-installed { color: var(--text-success); }

/* A count out of a total on test readiness: the dial, the figure under it,
   then what it counts. Replaced a donut with the figure inside it (Jason,
   2026-09-26); a half dial has no middle to write in. */
.count-dial {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
}

.count-dial-figure {
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

.count-dial-label {
  font-size: 10px;
  color: var(--text-muted);
}

/* A testing card on a phone leads with its C/TC number, left of the route
   on the same line. Left to the runs card's grid it fell into the length
   column and sat top right. Here rather than beside .test-ctc: it has to
   come after .run-card to win, and before the wide rules below that replace
   it. */
.test-card {
  grid-template-columns: auto minmax(0, 1fr);
}

.test-card .test-ctc  { grid-column: 1; grid-row: 1; }
.test-card .run-where { grid-column: 2; }

/* ---- cable runs, wide ---------------------------------------------------- *
 * Cards flowing into 400px columns turn a long list into a wall on a desktop:
 * three columns of stacked cards, and the eye has no line to follow. Above
 * 760px the same cards become one row each with the detail spread across the
 * page, under a header. The phone keeps the cards exactly as they were — this
 * is one media query, not a second template.
 */
@media (min-width: 860px) {
  .run-list {
    grid-template-columns: 1fr;
    gap: 4px;
  }

  /* Minimums on the text columns, so a long stage name ellipses only once
     there is genuinely no room rather than at every width. The progress
     column is sized for its widest state — the 84px bar, its gap, and
     "Not installed" — because that label truncating is what makes a row
     look broken. 860px rather than 760: below that the fixed columns eat
     enough of the row to start clipping the route, which is the one thing
     on the line that has to stay readable.

     Every heading is a label beside a 42px sort-and-filter control, so a
     column narrower than its label plus 42 clips the heading itself. Type
     had 70px, which left 26px for a 31px word and showed "Typ…", and
     Length was a pixel short of "Length" — while Stage, on 1.7fr, had 251px
     for one word and opened a gap beside them. The minimums below all
     clear 42px plus their label, and Stage gives the slack back. */
  .run-head,
  .run-card {
    display: grid;
    grid-template-columns:
      minmax(0, 2fr) minmax(90px, 1fr) minmax(88px, 0.5fr)
      minmax(96px, 1fr) 116px 100px 172px;
    gap: 0 16px;
    align-items: center;
  }

  /* Back on the row the card stacked it under, and without the little
     "act" label, which would be a second heading over a column that
     already has one. */
  .run-card .run-act {
    grid-row: auto;
    font-size: 13px;
    color: var(--text-secondary);
  }

  .run-card .run-act::before { content: none; }

  /* The testing list is the same row without the length column: nobody
     certifying a cable is reading its estimate. It gains the C/TC number
     at the far left (Jason, 2026-09-25), sized for "3.123" beside nothing
     else — the column needs no sort control. */
  .test-head,
  .test-card {
    grid-template-columns:
      64px minmax(0, 2fr) minmax(90px, 1fr) minmax(88px, 0.5fr)
      minmax(96px, 1fr) 172px;
  }

  .run-head {
    padding: 0 16px 6px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  .run-head-len {
    text-align: right;
    justify-content: flex-end;
  }

  /* Headings are links, but they are still headings: no underline, and the
     colour only lifts on hover or once a column is the one being sorted. */
  .run-head > a {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    color: inherit;
    text-decoration: none;
    min-width: 0;
  }

  .run-head > a:hover,
  .run-head > a:focus-visible {
    color: var(--text-link);
  }

  .run-head > a.is-sorted {
    color: var(--text-accent);
  }

  .sort-caret {
    font-size: 9px;
    line-height: 1;
  }

  .run-card {
    padding: 10px 16px;
  }

  /* The three qualifier cells stop being one line and become grid items of
     the row itself. */
  .run-card .run-meta {
    display: contents;
  }

  .run-card .run-kind::before,
  .run-card .run-stage::before {
    content: none;
  }

  .run-card .run-where,
  .run-card .run-type,
  .run-card .run-kind,
  .run-card .run-stage,
  .run-card .run-len,
  .run-card .run-progress {
    grid-row: 1;
    margin-top: 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .run-card .run-where { grid-column: 1; }
  .run-card .run-type  { grid-column: 2; font-size: 13px; color: var(--text-secondary); }
  .run-card .run-kind  { grid-column: 3; font-size: 12px; color: var(--text-muted); }
  .run-card .run-stage { grid-column: 4; font-size: 12px; color: var(--text-muted); }
  .run-card .run-len   { grid-column: 5; }
  .run-card .run-act   { grid-column: 6; }
  .run-card .run-progress { grid-column: 7; }

  /* The testing row has no length column and carries the C/TC number
     first, so each cell sits one track right of the runs list's and the
     ring lands on 6. The cells are placed explicitly: left on the runs
     list's numbers, the row stops lining up with its own header. */
  .test-card .test-ctc    { grid-column: 1; grid-row: 1; }
  .test-card .run-where   { grid-column: 2; }
  .test-card .run-type    { grid-column: 3; }
  .test-card .run-kind    { grid-column: 4; }
  .test-card .run-stage   { grid-column: 5; }
  .test-card .run-progress { grid-column: 6; }

  /* A ring is taller than a line of text, so the row needs less padding of
     its own to keep the same rhythm down the list. */
  .run-card {
    padding: 6px 16px;
  }
}

/* ---- across-projects report -------------------------------------------- *
 * A tile a project, opening to its own stages, and an accuracy section that
 * opens to cable types and then to the runs themselves. The tiles reuse the
 * drum card's shape deliberately: everything in this app that opens is a
 * card with a summary, and a report is no place to invent a second idiom.
 */

/* The include box sits where the drums list puts its pencil: outside the
   <details>, because a control inside a <summary> both acts and toggles the
   card, so ticking a project would flick it open. */
.proj-pick {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius);
  cursor: pointer;
  margin: 0;
}

.proj-pick:hover { background: var(--bg-accent); }

.proj-pick input {
  width: 22px;
  height: 22px;
  margin: 0;
  accent-color: var(--text-success);
  cursor: pointer;
}

.proj-card { margin-bottom: 6px; }

.proj-card.is-off { opacity: 0.5; }

.proj-card.is-off .drum-name { text-decoration: line-through; }

.proj-stage-list { padding: 4px 14px 12px; }

.proj-head { display: none; }

/* On a phone the client, stages and counts read as one muted line, the
   separators drawn rather than written into the markup so the wide layout
   can drop them - the same handling as the drum cards. Literal characters,
   not hex escapes. */
.proj-summary .drum-meta {
  font-size: 12px;
  color: var(--text-muted);
}

.proj-stages::before,
.proj-cables::before,
.proj-metres::before { content: " · "; }

/* The filter icon: a summary that reads as an icon button, the same size and
   shape as the icon buttons beside a search box. */
.filter-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  cursor: pointer;
  color: var(--text-secondary);
  list-style: none;
}

.filter-icon::-webkit-details-marker { display: none; }

.filter-icon:hover {
  border-color: var(--brand-highlight);
  color: var(--text-accent);
}

details[open] > .filter-icon {
  background: var(--bg-accent);
  color: var(--text-accent);
}

/* Equal thirds cut "Running totals" in half. These labels are not the same
   length, so they size to their words and share what is spare. */
#drawSet button,
#groupSet button {
  flex: 1 1 auto;
  padding: 0 10px;
}

.range-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Not input[type="date"]: the picker rewrites it to text, and the rule
   stopped matching the moment the script ran. */
.range-row .dp,
.range-row input {
  flex: 1;
  min-width: 0;
  font-size: 12px;
  padding: 6px 8px;
}

.range-row .dp { padding: 0; }

.range-len {
  flex: none;
  font-size: 11.5px;
  color: var(--text-muted);
  white-space: nowrap;
}

.range-row.is-off { opacity: 0.45; }

/* Stacked rather than a field squeezed beside a button. The menu panel is
   216px wide, which left about 120px for the name - too little to read back
   what you had typed, and worse on a phone. Stacking makes it one control
   that behaves the same on both. */
.saved-name {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  padding: 6px 12px 10px;
  margin: 0;
  min-width: 244px;
}

.saved-name input {
  width: 100%;
  min-width: 0;
  font-size: 13px;
  padding: 8px 10px;
  min-height: var(--tap-min);
}

.saved-name button { width: 100%; }

/* One totals row: a colour, a name, and the two figures. */
.total-row {
  display: grid;
  grid-template-columns: 16px minmax(0, 1fr) 0 auto auto;
  gap: 0 12px;
  align-items: center;
  padding: 9px 14px;
}

.total-swatch {
  width: 14px;
  height: 3px;
  border-radius: 2px;
}

.total-name {
  font-weight: 600;
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.total-client { display: none; }

.total-cables,
.total-metres {
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
}

.total-metres { text-align: right; }

.total-row.is-total {
  background: var(--bg-accent);
  border-color: var(--brand-highlight);
}

.total-row.is-total .total-name { font-weight: 700; }

.total-row.is-total .total-cables,
.total-row.is-total .total-metres {
  font-weight: 600;
  color: var(--text-primary);
}

/* Closed, the accuracy section is the only thing between the reader and a
   section they may not want, so it has to read as a control rather than a
   heading someone forgot to fill in: a card row with a chevron, the same
   shape as everything else on the screen that opens. */
.acc-section {
  margin-top: 18px;
  padding: 0;
}

.acc-section > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 11px 14px;
  min-height: var(--tap-min);
  border-radius: inherit;
}

.acc-section > summary::-webkit-details-marker { display: none; }

.acc-section > summary:hover { background: var(--bg-accent); }

.acc-section > summary h2 {
  font-size: 14px;
  margin: 0;
}

.acc-section-words { margin-right: auto; }

.acc-section-words {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  margin-right: auto;
}

.acc-section-hint {
  font-size: 11.5px;
  color: var(--text-muted);
}

.acc-section-chev {
  flex: none;
  color: var(--text-secondary);
  transition: transform 0.15s ease;
}

.acc-section[open] > summary { border-bottom: 0.5px solid var(--border); }

.acc-section[open] .acc-section-chev { transform: rotate(180deg); }

.acc-legend {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  font-size: 11.5px;
  color: var(--text-secondary);
  margin-left: auto;
}

/* The header wraps before the key gets squeezed: on a phone the title and
   its hint take the width, and the key drops underneath them rather than
   ellipsising into nothing. */
.acc-section > summary { flex-wrap: wrap; }

@media (max-width: 560px) {
  .acc-legend { margin-left: 0; }
}

/* The single-project dashboard caps its charts at 900px and its chart head
   matches them. This report's charts are full width, so its head has to be
   too, or the filter icon stops short of the card edge with a band of empty
   card beside it. */
.chart-head-wide { max-width: none; }

.acc-swatch {
  display: inline-block;
  width: 16px;
  height: 7px;
  border-radius: 2px;
  vertical-align: middle;
}

.acc-rows {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 8px 12px 12px;
}

.acc-card {
  padding: 0;
  overflow: hidden;
}

.acc-summary,
.acc-type > summary {
  list-style: none;
  cursor: pointer;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  gap: 0 10px;
  align-items: center;
}

.acc-summary {
  padding: 7px 12px;
  min-height: var(--tap-min);
}

.acc-summary::-webkit-details-marker,
.acc-type > summary::-webkit-details-marker { display: none; }

.acc-summary-flat { cursor: default; }

.acc-name {
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.acc-cover {
  font-size: 11px;
  color: var(--text-muted);
  white-space: nowrap;
}

.acc-bias {
  font-size: 12px;
  text-align: right;
  min-width: 98px;
}

.acc-card details[open] > .acc-summary { border-bottom: 0.5px solid var(--border); }

.acc-types { padding: 2px 0 4px; }

.acc-type > summary {
  padding: 5px 12px 5px 22px;
  min-height: 36px;
  font-size: 12px;
}

.acc-type > summary:hover { background: var(--bg-accent); }

.acc-type .acc-name {
  font-size: 12px;
  color: var(--text-secondary);
}

.acc-type .cable-rows {
  padding: 2px 12px 8px 30px;
  max-height: 260px;
  overflow-y: auto;
}

/* Already indented, and it scrolls — an absolutely positioned rule would
   scroll away from the rows it belongs to. */
.acc-type .cable-rows::before { display: none; }

.acc-type .cable-row { font-size: 12px; }

.acc-card.is-total { background: var(--bg-accent); }

/* ---- across-projects, wide ---------------------------------------------- *
 * Tiles flowing as cards read well on a phone and waste a desktop, the same
 * wall the cable runs list had. Above 860px each project is one row with its
 * detail across the page, under a header.
 */
@media (min-width: 860px) {
  .proj-head {
    display: grid;
    /* The client had a fixed 96px, which cut "V/Line (demonstration data)"
       to "V/Line (demon…" and left it floating in the middle of the name
       column's spare width. Both columns share the space now. */
    grid-template-columns: minmax(0, 1.3fr) minmax(0, 0.9fr) 1fr 132px 104px;
    gap: 0 14px;
    padding: 0 52px 4px 17px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  .proj-head span:last-child { text-align: right; }

  .proj-summary {
    display: grid;
    grid-template-columns: minmax(0, 1.3fr) minmax(0, 0.9fr) 1fr 132px 104px;
    gap: 0 14px;
    align-items: center;
  }

  .proj-summary .drum-meta { display: contents; }

  .proj-stages::before,
  .proj-cables::before,
  .proj-metres::before { content: none; }

  .proj-summary .drum-meta > span {
    font-size: 12px;
    color: var(--text-muted);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .proj-summary .proj-metres { text-align: right; }

  .proj-stage-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 8px;
  }

  .total-row { grid-template-columns: 16px minmax(0, 1.3fr) minmax(0, 0.9fr) 148px 158px; }

  .total-client {
    display: block;
    font-size: 12px;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* ---- no cable for this run ---------------------------------------------- *
 * The progress indicator on the cable runs list carries a second fact: not
 * how far this run has got, but whether there is cable on the drums to pull
 * it with. They belong together because they are asked together — "what can
 * we do tomorrow" is progress and stock in one question, and the stock panel
 * that already knows the answer lives on a screen nobody opens while
 * planning a day's work.
 *
 * The ring keeps its progress colour underneath; the alert is drawn over it,
 * so a run that is short of cable still reads as not-installed rather than
 * losing its place in the sequence.
 */

/* Steady red: the cable type is at or under 130% of what is still to pull
   on it. Every outstanding run of that type carries it, which is why it does
   not move — on a list of two hundred runs a pulse on every row is not a
   warning, it is a screen nobody can read. */
.pdonut.stock-low .pdonut-track,
.pdonut.stock-short .pdonut-track {
  stroke: var(--text-danger);
}

/* The progress arc, where there is one, draws in currentColor — so the
   whole ring goes red together rather than half of it staying green. A run
   not yet installed has no arc at all, which is exactly the common case
   here: the red track is the whole ring. */
.pdonut.stock-low,
.pdonut.stock-short { color: var(--text-danger); }

.pdonut.stock-low .pdonut-label,
.pdonut.stock-short .pdonut-label,
.progress-label.is-stock {
  color: var(--text-danger) !important;
  font-weight: 600;
}

.progress.stock-low .seg,
.progress.stock-short .seg {
  border-color: var(--text-danger);
}

/* A slow pulse for the run that cannot be pulled at all: the drums of its
   type do not hold its length. Same 1.6s as the stock cards on the reporting
   screen — anything faster than three cycles a second can trigger
   photosensitive seizures (WCAG 2.3.1), and this is a screen people have
   open all day. The colour never leaves, so the row still reads correctly in
   a screenshot or at the bottom of the cycle. */
@keyframes run-stock-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.38; }
}

.pdonut.stock-short svg,
.progress.stock-short .progress-track {
  animation: run-stock-pulse 1.6s ease-in-out infinite;
}

/* Honour a system setting that asks for no motion: the ring keeps its red
   and stops moving. */
@media (prefers-reduced-motion: reduce) {
  .pdonut.stock-short svg,
  .progress.stock-short .progress-track { animation: none; }
}

/* ---- column filters: tick boxes, not one-at-a-time links ---------------- *
 * Sorting is one choice and filtering is several, so the menu holds both
 * shapes: the directions stay links, the values become the same check rows
 * used on every form in the app. Ticking does not reload on each box —
 * pick what you want, then Apply, which is the whole point of being able to
 * pick more than one.
 */
.col-filter {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 6px 8px 8px;
  margin: 0;
  min-width: 210px;
}

/* Enough for a project with thirty cable types without the menu running off
   the bottom of the screen. */
.col-filter-rows {
  max-height: 280px;
  overflow-y: auto;
  gap: 6px;
}

/* Tighter than a form's check rows: this is a menu, and a full 44px row
   thirty times over is a scroll for no reason. Still a whole-row target. */
.col-filter .check-row {
  padding: 7px 10px;
  min-height: 38px;
  gap: 10px;
}

.col-filter .check-row input[type="checkbox"] {
  width: 20px;
  height: 20px;
}

.col-filter .check-label { font-size: 12.5px; }

.col-filter-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

.col-filter-actions button { flex: 1; }

.col-filter-actions a {
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
}

/* How many values a column is ticked to, on the heading itself: a filtered
   column otherwise looks exactly like an unfiltered one until it is
   opened. */
.col-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 15px;
  height: 15px;
  padding: 0 4px;
  margin-left: 3px;
  border-radius: 8px;
  background: var(--fill-accent);
  color: var(--on-accent);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0;
}

/* ---- test readiness, wide ----------------------------------------------- *
 * Locations tiling into 400px columns is the same wall the cable runs list
 * had: three columns of stacked cards and no line for the eye to follow.
 * Above 860px each location is one row with its state and its two dials
 * across the page, under a header. The phone keeps the cards as they were.
 */
@media (min-width: 860px) {
  .rd-head {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(110px, 1fr) 104px 104px;
    gap: 0 16px;
    padding: 0 14px 6px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  /* The dial columns centre their heading over the dial, the way they did
     as plain labels. .col-head is a flex row, so it is justify-content
     rather than text-align that moves it. */
  .rd-head > *:nth-child(3),
  .rd-head > *:nth-child(4) { justify-content: center; }

  /* The last heading's menu would hang off the right edge of the page. */
  .rd-head > *:last-child .col-menu .menu-panel {
    left: auto;
    right: 0;
  }

  .rd-list {
    grid-template-columns: 1fr;
    gap: 4px;
  }

  .rd-summary {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(110px, 1fr) 104px 104px;
    gap: 0 16px;
    align-items: center;
    padding: 8px 14px;
  }

  /* The name and its badge, and the two dials, become columns of the row
     rather than two stacked groups — the same display:contents trick the
     drum and pit rows use, so the markup does not differ between widths. */
  .rd-summary .rd-text,
  .rd-summary .rd-dials { display: contents; }

  .rd-summary .rd-badge { justify-self: start; }
}

/* Setting a stage's work package in place. Stages arrive from imports, so
   this has to be editable on one that already exists — a field only
   settable at add time would be a field nobody could use. */
.stage-cwp {
  display: flex;
  gap: 6px;
  align-items: center;
  min-width: 0;
}

.stage-cwp input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 34px;
  font-size: 12px;
}

.stage-cwp button { flex: none; }
