/* Field Scheduling application styles — follows Intricate Solutions CSS conventions */

/* ── Typography override ──────────────────────────────────────────────────
   CSS.md calls for Lato/Lato Light everywhere -- new-style.css's
   `html { font-family: 'Lato'; font-size: 12px; }` and `h3`'s
   `font-family: 'Lato Light';` are that base case's default, but neither
   named a fallback, and the Lato webfont they point at (local
   /webfonts/Lato-*.ttf) was never actually part of this repo, so both
   404 -- every element without its own font-family (h1/h2/most body
   text) was silently dropping to the browser's default serif face. Fixed
   at the source (base.html/base_bare.html now load real Lato from Google
   Fonts) rather than switched to a different family, but still needs a
   safe fallback here in case that ever fails to load, and Google Fonts
   has no family literally named "Lato Light" (it serves one 'Lato'
   family across weights) -- 'Lato Light' is font-weight: 300 there, so h3
   needs that instead of the nonexistent family name. Loaded after
   new-style.css, so these rules win on the tied specificity without
   needing !important. Root size bumped 12px -> 13px, which nudges small
   rem-sized labels by about a px and headings by a couple px, without the
   layouts tuned around a ~12px root (calendar day-column widths, span bar
   heights, etc.) overflowing the way a bigger jump risked. */
html {
  font-family: 'Lato', Arial, sans-serif;
  font-size: 13px;
}
h3 {
  font-family: 'Lato', Arial, sans-serif;
  font-weight: 300;
}

/* ── Sidebar ──────────────────────────────────────────────────────────── */
.sidebar-new {
  position: fixed;
  top: 0;
  left: 0;
  height: 100vh;
  width: 27rem;
  background: var(--primary);
  color: #fff;
  display: flex;
  flex-direction: column;
  z-index: 10;
  transition: width 0.3s ease;
  overflow: hidden;
}
.sidebar-new.collapsed {
  width: 5.66rem;
}
.sidebar-new.collapsed .sidebar-label,
.sidebar-new.collapsed .sidebar-section-label {
  display: none;
}
.sidebar-toggle-row {
  display: flex;
  align-items: center;
  height: 4.5rem;
  padding: 0 var(--size-5);
  border-bottom: 1px solid rgba(255,255,255,0.15);
}
/* THE MENU BUTTON WAS INVISIBLE, and specificity is why. It carries
   .button.transparent, which sets color: var(--primary) -- a blue, on the teal
   sidebar. Two classes outrank the one class this rule used to have, so the
   blue won and the only white thing here was the rule nobody saw applied.
   .sidebar-toggle-row is added to the selector to win that, rather than
   !important, which would just move the argument.

   It also reads as a control now instead of a bare glyph: a faint chip behind
   it, so there is something to aim at. */
/* What SHIFT stands for, beside the name. Normal weight and the muted grey so
   it reads as a subtitle; the bar is white now, so this is grey-on-white rather
   than the white-on-grey everything here used to be. Hidden on a narrow window,
   where the nav needs the room more than the strapline does. */
.app-title-expansion {
  font-weight: 400;
  font-size: 0.82rem;
  color: var(--accent);
  margin-left: 0.5rem;
  letter-spacing: 0;
  white-space: nowrap;
}
@media (max-width: 900px) { .app-title-expansion { display: none; } }

.sidebar-toggle-row .sidebar-toggle-btn.button.transparent {
  color: #fff;
  font-size: 1.35rem;
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 0.3rem;
  padding: 0.3rem 0.55rem;
  line-height: 1;
  transition: background 0.15s, color 0.15s;
}
.sidebar-toggle-row .sidebar-toggle-btn.button.transparent:hover {
  color: #FFDB9F;
  background: rgba(255, 255, 255, 0.22);
}
.sidebar-nav-list {
  list-style: none;
  padding: 0;
  margin: 0;
  flex: 1;
  overflow-y: auto;
}
.sidebar-nav-item a,
.sidebar-nav-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem var(--size-6);
  color: #fff;
  text-decoration: none;
  font-size: 0.95rem;
  transition: background 0.2s, color 0.2s;
  cursor: pointer;
  border-left: 3px solid transparent;
}
.sidebar-nav-item a:hover,
.sidebar-nav-item:hover {
  background: rgba(255,255,255,0.12);
  color: #FFDB9F;
}
.sidebar-nav-item.active a,
.sidebar-nav-item.active {
  border-left-color: #FFDB9F;
  background: rgba(255,255,255,0.15);
  color: #FFDB9F;
}
.sidebar-nav-item i { font-size: 1.1rem; min-width: 1.2rem; text-align: center; }
.sidebar-section-label {
  padding: 0.75rem var(--size-6) 0.25rem;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: rgba(255,255,255,0.5);
}
.sidebar-footer {
  border-top: 1px solid rgba(255,255,255,0.15);
  padding: var(--size-4) 0;
}

/* Sidebar offset -- applied ONCE, on .content only. .wrapper is .content's
   own parent (see base.html: <div class="wrapper"><header>...<div
   class="content">), so if .wrapper ALSO carried this margin (it used to),
   the two stacked: .content ended up shifted by the sidebar's width TWICE
   (e.g. 54rem instead of 27rem when expanded). Base-case's own .wrapper
   rule (new-style.css) never had a left offset at all -- only .content did
   -- so this was a duplication bug introduced when sidebar support was
   added here, not something the base template ever intended. Harmless-
   looking on capped-width pages (content just sat further right inside
   still-plenty-of-room), but on this page's uncapped width it silently ate
   a full sidebar-width of the space this feature exists to give back. */
.content.sidebar-collapsed { margin-left: 5.66rem; transition: margin-left 0.3s; }
.content.sidebar-expanded  { margin-left: 27rem;   transition: margin-left 0.3s; }

/* ── Header overrides ─────────────────────────────────────────────────── */
/* WHITE, not the old #878a91 grey. Everything sitting on this bar was white or
   near-white to show up against that grey, so each of those is flipped below --
   changing only the background would have left white text on white. The border
   is doing the work the grey used to: against a near-white page a white bar has
   no edge of its own. */
.new-ui-topheader {
  background: #fff;
  border-bottom: 1px solid var(--accent-faded);
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 4.5rem;
  padding: 0 var(--size-6);
  position: fixed;
  top: 0;
  z-index: 9;
  width: 100%;
  box-sizing: border-box;
}
.new-ui-topheader.sidebar-collapsed { padding-left: calc(5.66rem + var(--size-6)); transition: padding-left 0.3s; }
.new-ui-topheader.sidebar-expanded  { padding-left: calc(27rem + var(--size-6)); transition: padding-left 0.3s; }
.new-logo { height: 2.5rem; }
.app-title-block { display: flex; align-items: center; height: 100%; }
.logo-text-fallback {
  display: none; align-items: center; gap: 0.6rem;
  color: var(--dark); font-weight: 600; font-size: 1.25rem; letter-spacing: 0.02em;
}
.logo-text-fallback i { font-size: 1.4rem; color: var(--primary); }
#new-toprightnav { display: flex; align-items: center; gap: var(--size-5); }
.header-user-name { color: var(--accent); font-size: 0.9rem; }
.header-dialog-button { border:none; background:none; font-size:2rem; color:var(--accent); transition:color 0.3s; cursor:pointer; }
.header-dialog-button:hover { color:var(--primary); }
/* The hover disc was a light grey meant to lift off a darker bar; on white it
   has to go the other way or it disappears. */
.header-icon-background { opacity: 0; color: var(--accent-faded); transition: opacity 0.3s; }
.header-dialog-button:hover .header-icon-background { opacity: 1; }
.header-icon { color: #EFEFEF; }

/* Breadcrumb */
div.breadcrumb {
  background: #eee;
  font-style: italic;
  font-size: 1rem;
  color: #7F7F7F;
  height: 2.8rem;
  border-bottom: 0.075rem solid #ccc;
  display: flex;
  align-items: center;
  position: fixed;
  top: 4.5rem;
  left: 0;
  right: 0;
  z-index: 8;
  padding-left: 5.66rem;
}
div.breadcrumb a { color: var(--primary); }

/* Content area — wrapper's margin-top already clears header + breadcrumb.
   The base-case .content rule (new-style.css) is written for a narrow,
   centered page body and sets text-align:center + margin:auto -- that's
   wrong here: this app's .content starts wherever the sidebar ends and
   fills the rest of the viewport, so inherited center-alignment made every
   page's loose text (paragraphs, section labels, form labels not already
   inside a table cell) drift toward the middle of that wide remaining
   space -- reading as "leaning right" of the actual page, not "centered."
   Left-align it here; anything that genuinely wants centering sets that
   itself. */
.content {
  padding-top: var(--size-6);
  min-height: calc(100vh - 7.3rem);
  text-align: left;
}

/* Alert overlays */
.alert-overlay {
  position: fixed;
  bottom: var(--size-6);
  left: 50%;
  transform: translateX(-50%);
  z-index: 100;
  padding: var(--size-5) var(--size-7);
  border-radius: 0.4rem;
  box-shadow: var(--shadow);
  background: #eee;
  max-width: 36rem;
  width: 90%;
}
.alert-overlay.error { background: #fee; color: #f44; }
.alert-overlay.warning { background: #878a91; color: #fff400; }
.alert-overlay-inner { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }

/* ── Status badges ─────────────────────────────────────────────────────── */
.status-badge {
  display: inline-block;
  padding: 0.2rem 0.55rem;
  border-radius: 0.25rem;
  font-size: 0.85rem;
  font-weight: bold;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: #fff;
  /* Every status badge reads as the same size regardless of label length --
     "Backlogged" and "Date is not Flexible" otherwise size purely to their own
     text, which looks inconsistent side by side in a table column. Sized to
     the longest label ("Date is not Flexible"/"Missing Information") plus
     the uppercase+letter-spacing stretch; shorter labels just center in it. */
  min-width: 9.5rem;
  text-align: center;
}
.status-backlog       { background: var(--accent); }
/* Requested is the one status that means somebody is WAITING, and in grey
   it read as "inactive" beside the real greys (cancelled, archived). Light
   purple carries no temperature -- it is not a warning and not a success --
   which is right for "arrived, not yet looked at". */
.status-requested     { background: #8b7fc7; }
.status-to_be_scoped  { background: #8a5a0e; }
.status-unconfirmed   { background: #b7950b; }
.status-confirmed     { background: var(--primary); }
/* Confirmed, and the date cannot move. Red rather than a shade of Confirmed:
   what matters on a board is spotting the dates you CAN'T move, and a darker
   teal read as just another confirmed job. Same red as .status-out_of_service
   below -- both mean "this one is not available to you". */
.status-date_not_flexible { background: #c0392b; }
/* Still used: job_shifts have their own in_progress and share these badges. */
.status-in_progress   { background: var(--secondary); }
.status-completed     { background: #1e8449; }
.status-cancelled     { background: #444; }

.status-available          { background: #1e8449; }
.status-due_for_inspection { background: #b7950b; }
.status-out_of_service     { background: #c0392b; }

.role-badge { display:inline-block; padding:0.2rem 0.55rem; border-radius:0.25rem; font-size: 0.85rem; font-weight:bold; }
.role-admin        { background:#fdf2e9; color:var(--secondary); }
.role-director     { background:#e8f4f8; color:var(--primary); }
.role-scheduler    { background:#eafaf1; color:#1e8449; }
.role-field_tech   { background:#fef9e7; color:#b7950b; }
.role-consultant   { background:#f4f4f4; color:var(--accent); }
.role-finance      { background:#eafaf1; color:#1e8449; }

/* ── Status messages ──────────────────────────────────────────────────── */
.error_message   { color:#f00; background:#fee; border:1px solid #fcc; }
.success_message { color:#0a0; background:#efe; border:1px solid #cfc; }
.in_progress_message { color:#880; background:#ffe; border:1px solid #cc0; }

/* ── Print ────────────────────────────────────────────────────────────── */
.print-only { display: none; }
@media print {
  .no-print, .alert-overlay, .overlay, header, .sidebar-new { display: none; }
  .print-only { display: block; }
  .wrapper, .content { margin: 0 auto !important; padding-top: 0 !important; }
  table.tablesorter, table.tablesorter thead tr th { font-size: 9pt; }
}

/* ── Responsive (Field Tech / mobile) ────────────────────────────────── */
@media only screen and (max-width: 850px) {
  /* THE NAV NEEDS THE HEADER'S OWN OFFSET. It lives outside #mainWrapper,
     whose inline margin-top: 7.3rem is what reserves room for the fixed
     header -- so in normal flow the nav starts at y=0, under the header band,
     and its first rows (the collapse toggle, then Dashboard) are unclickable
     whichever way the z-index points. Raising the header merely moved which
     of the two was buried. 7.3rem is the same number #mainWrapper and
     .sched-toolbar.pinned both use for the header's height; if that ever
     changes, all three move together. */
  .sidebar-new { width: 100%; height: auto; position: relative;
                 margin-top: 7.3rem; }
  /* AND THE WRAPPER GIVES ITS UP, or the offset is applied twice and leaves a
     95px band of nothing between the menu and the page -- the nav is above it
     in flow now and has already pushed it down. !important because that
     margin is an inline style on the element; there is no class to target. */
  .wrapper { margin-top: 0 !important; }
  /* AND THE HEADER STILL OUTRANKS THE NAV, for the scroll. The offset above
     is what stops them sharing pixels at rest; this is for what happens
     afterwards -- the header is position:fixed and the nav is now in flow, so
     the nav scrolls UP underneath it, and at the design system's z-index 2
     the header would be painted over on the way past. Scoped to this block,
     so the desktop stacking is untouched: there an expanded sidebar is meant
     to sit over the header's left edge.

     RAISED ON header#header-bar, NOT ON THE .new-ui-topheader INSIDE IT.
     That was the first attempt and it did nothing at all. The design system
     gives the bare `header` element `position: fixed; z-index: 2`, which
     makes it a STACKING CONTEXT -- so a z-index on its child only orders that
     child against its siblings, and the whole subtree still competes with the
     sidebar at 2. The rule was present, correct-looking, and completely inert:
     a browser reported the raised value of 11 on the div while the nav went
     on swallowing every click. A z-index has to go on the element that
     establishes the context, not on the one you want to see. */
  header#header-bar { z-index: 11; }
  .sidebar-new.collapsed { width: 100%; }
  .wrapper.sidebar-collapsed, .wrapper.sidebar-expanded,
  .content.sidebar-collapsed, .content.sidebar-expanded { margin-left: 0; }
  .new-ui-topheader.sidebar-collapsed, .new-ui-topheader.sidebar-expanded { padding-left: var(--size-6); }
  div.breadcrumb { padding-left: var(--size-6); }
}
@media only screen and (max-width: 600px) {
  .job-package-card { padding: var(--size-5) !important; }
  .job-package-card .button { width: 100%; margin: 0.3rem 0; }
  .status-tile-grid, .health-tile-grid, .kpi-row { grid-template-columns: 1fr !important; }
}
/* ── Work Cards -- shared tile component for the Dashboard's working list
   and the Calendar's kanban board, so both read as the same interaction
   pattern (collapsed one-liner by default, click to expand for detail). ── */
.work-card {
  background: #fff; border: 1px solid var(--accent-faded); border-left: 4px solid var(--primary);
  border-radius: 0.4rem; padding: 0.5rem 0.7rem; margin-bottom: 0.5rem; box-shadow: var(--shadow);
  transition: box-shadow 0.15s ease;
}
.work-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.14); }
.work-card.collapsed .work-card-details { display: none; }
.work-card-header { display: flex; justify-content: space-between; align-items: flex-start; cursor: pointer; gap: 0.5rem; }
.work-card-title { font-weight: 600; font-size: 0.9rem; }
.work-card-meta { font-size: 0.85rem; color: var(--accent); margin-top: 0.15rem; }
.work-card-details { margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px dashed var(--accent-faded); font-size: 0.85rem; }
.work-card-actions { margin-top: 0.5rem; display: flex; gap: 0.4rem; flex-wrap: wrap; }
.work-card.severity-critical { border-left-color: var(--secondary); }
.work-card.severity-warning { border-left-color: #d9a441; }
.work-card-toggle { color: var(--accent); font-size: 0.85rem; flex-shrink: 0; }

/* ── Scheduling Board -- Unscheduled Work bin / calendar / Resources bin ──
   Flexbox, not a fixed grid -- no column is pinned to a hardcoded width.
   Unscheduled/Resources default to a comfortable width but don't grow;
   Calendar is the only one that grows, so it eats whatever's left instead
   of leaving it empty. Every .board-panel is independently resizable
   (both dimensions), and a manual resize converts that panel from
   "auto-sized" to "fixed at whatever you dragged it to" -- see the
   ResizeObserver in board.html, which is what makes an override stick
   instead of snapping back to the default the next time flex recalculates. */
.scheduling-board-layout {
  display: flex;
  flex-wrap: wrap;
  gap: var(--size-6);
  align-items: flex-start;
}
.scheduling-board-layout > .board-panel { min-width: 0; }
.scheduling-board-layout > .scheduling-bin { flex: 0 0 13rem; height: 44rem; }
/* Same bounded-height-with-internal-scroll treatment as the bins above --
   without it this panel just grows to fit every lane × every day (weeks of
   people × 7 days easily runs to hundreds of rows), pushing the whole page
   to an unreasonable height instead of scrolling in place. The panel's own
   resize handle can still make it bigger/smaller from here same as before. */
.scheduling-board-layout > .scheduling-calendar { flex: 1 1 40rem; height: 44rem; }
.scheduling-board-layout > .scheduling-calendar > .board-panel-body { overflow: hidden; }
@media only screen and (max-width: 1200px) {
  .scheduling-board-layout > .board-panel { flex: 1 1 100%; }
}
.scheduling-bin-body {
  overflow-y: auto;
  overflow-x: hidden;
  padding-right: 0.3rem;
}
/* The BU/Client Resources panel stacks two of these (Technicians, Trucks) in
   one shared panel body -- each needs its OWN bounded scroll, since the
   outer panel's resize now governs the panel as a whole, not either sub-box
   individually. */
.resource-group-box .scheduling-bin-body { max-height: 18rem; }
.scheduling-bin-body h4 {
  text-transform: uppercase; color: var(--accent); font-size: 0.85rem;
  margin: var(--size-5) 0 var(--size-4);
}
.scheduling-bin-body h4:first-child { margin-top: 0; }
.draggable-unscheduled { cursor: grab; }

/* ── Board "windows" -- drag to reorder, minimize, resize, and save named
   layout presets. Default DOM order/CSS is today's fixed arrangement;
   nothing here changes unless a scheduler actually customizes it. ────── */
.board-panel-header { justify-content: space-between; cursor: default; }
.board-panel-drag-handle { display: flex; align-items: center; gap: 0.5rem; cursor: move; }
.board-panel:not(.panel-draggable) .board-panel-drag-handle { cursor: default; }
.panel-minimize-toggle { cursor: pointer; color: var(--accent); font-size: 0.85rem; padding: 0.2rem; }
.panel-minimize-toggle:hover { color: var(--primary); }
.board-panel.minimized .board-panel-body,
.board-panel.minimized .board-panel-title-extra { display: none; }
.board-panel.minimized .board-panel-header {
  margin-bottom: 0; padding-bottom: 0; border-bottom: none;
}
.board-panel-placeholder {
  border: 2px dashed var(--primary); border-radius: 0.5rem; background: var(--primary-faded);
}
.layout-controls select, .layout-controls .button { font-size: 0.85rem; }

/* The resize handle lives on the panel itself (not just its inner body) so
   dragging it actually changes the flex item's width -- resizing only the
   scrollable inner content wouldn't move anything else in the row. */
.scheduling-board-layout > .board-panel {
  display: flex;
  flex-direction: column;
  resize: both;
  overflow: hidden;
}
.scheduling-board-layout > .board-panel.minimized {
  resize: none;
  overflow: visible;
  height: auto !important;
}
/* Header never scrolls away; only the body beneath it does. */
.scheduling-board-layout > .board-panel > .board-panel-header { flex: 0 0 auto; }
.scheduling-board-layout > .board-panel > .board-panel-body {
  flex: 1 1 auto;
  overflow-y: auto;
  min-height: 0;
}
.resource-token {
  background: #fff; border: 1px solid var(--accent-faded); border-radius: 0.4rem;
  padding: 0.4rem 0.6rem; margin-bottom: 0.4rem; font-size: 0.85rem; cursor: grab;
}
.resource-token i { color: var(--primary); margin-right: 0.3rem; }
.resource-token-meta { font-size: 0.85rem; color: var(--accent); margin-top: 0.15rem; }

.scheduling-grid { display: grid; gap: 0.3rem; min-width: max-content; }
/* The per-lane band behind a technician's columns -- see _board_body.html.
   Alternating so two neighbouring lanes are told apart at a glance, which
   also answers "where does this person's lane end", the thing a wide lane
   made impossible to see. Deliberately unpositioned: it must paint UNDER the
   day-cells and bars, and tree order does that on its own -- giving it
   position/z-index would lift it over the grey cells instead. */
/* Every lane is outlined, so one person's columns are visibly separate from
   the next person's. --accent-faded is too pale to read as a divider against
   these near-white fills, so the line weight is set here in explicit greys:
   dark enough to see, well short of black, which would turn 39 lanes into a
   cage. */
.scheduling-lane-band {
  background: #fcfcfd;
  border: 1px solid #ccd1d9;
  border-radius: 0.4rem;
  pointer-events: none;
}
.scheduling-lane-band.alt { background: #f3f4f6; }
/* The box for a multi-column lane: a step darker again, because this is the
   one that has to carry "all of this width is one person" on its own. Squared
   off at the top where the header sits on it, so the two read as one
   container. */
.scheduling-lane-band.wide {
  background: #eef0f4;
  border: 1px solid #aab2bf;
  border-radius: 0 0 0.4rem 0.4rem;
}
.scheduling-grid-corner { position: sticky; top: 0; left: 0; background: #fff; z-index: 3; }
.scheduling-grid-daylabel {
  position: sticky; top: 0; background: #fff;
  font-weight: bold; font-size: 0.85rem; text-align: center; padding: 0.3rem 0;
  border-bottom: 2px solid var(--accent-faded); z-index: 2;
}
.scheduling-grid-daylabel.today { color: var(--primary); border-bottom-color: var(--primary); }
.scheduling-lane-label {
  font-weight: 600; font-size: 0.85rem; display: flex; align-items: center; justify-content: space-between;
  position: sticky; left: 0; background: #fff; padding-right: 0.5rem; z-index: 1;
}
.lane-label-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mark-day-off-btn { flex: 0 0 auto; font-size: 0.75rem; padding: 0.15rem 0.3rem; color: var(--accent); }
.mark-day-off-btn:hover { color: #c0392b; }

/* Vertical orientation (technicians as columns, dates as rows, the default) --
   swaps which axis freezes: the date column stays put while scrolling through
   technicians, and technician names stay put while scrolling through dates.
   Everything else (span bars, empty cells, the day-off overlay) is already
   axis-agnostic -- _board_body.html decides per-element whether a value goes
   into grid-column or grid-row, so no other CSS needs to change here. */
.scheduling-grid.vertical .scheduling-grid-daylabel { top: auto; left: 0; text-align: left; padding: 0.3rem 0.5rem; }
/* CENTRED OVER THE LANE'S OWN BOX. A lane is as many columns wide as the most
   jobs that person holds on any one day -- Benjamin Parry is three wide
   because of one day in October with three concurrent proves. Centring the
   name across that width only works if the width is VISIBLE, which it was not:
   the empty day-cell is drawn in the lane's first column only, so the other
   two columns rendered nothing at all and the name looked like it belonged to
   the column next door. The band behind the lane draws the box; this centres
   the name over it and caps it. */
.scheduling-grid.vertical .scheduling-lane-label {
  left: auto; top: 0;
  justify-content: center; text-align: center;
  gap: 0.3rem; padding: 0.25rem 0.4rem;
}
/* A lane that needs more than one column gets the box drawn properly, header
   included, so "this whole width is one person" is something you can see
   rather than infer. */
.scheduling-grid.vertical .scheduling-lane-label.wide {
  background: #eef0f4;
  border: 1px solid #aab2bf;
  border-bottom: none;
  border-radius: 0.4rem 0.4rem 0 0;
}
/* Jobs (Gantt) lane labels are job names, not short person/truck names --
   long ones (e.g. "Annual Project including Inline and Bench") would
   otherwise wrap and blow out every row's height. Truncated with an
   ellipsis at a smaller size; the full name is in the title tooltip and on
   the bar itself once scrolled into the day columns. */
.scheduling-grid-jobs .scheduling-lane-label {
  display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  font-size: 0.85rem; line-height: 1.3; padding-top: 0.3rem;
}
.scheduling-grid-jobs .scheduling-lane-label span { display: block; overflow: hidden; text-overflow: ellipsis; }
.work-card.kanban-card.locked { opacity: 0.85; border-left-color: var(--accent); cursor: not-allowed; }
.lock-toggle { color: var(--accent); font-size: 0.85rem; cursor: pointer; }
.lock-toggle:hover { color: var(--primary); }

/* Resources bin: BU/Client view shows both technicians and trucks as two
   visually separate boxes (not one merged scrolling list) since neither is
   the lane dimension there; People/Trucks view only ever renders one of
   these boxes (the "other" resource type), so no extra wrapper needed. */
.resource-group-box {
  border: 1px solid var(--accent-faded); border-radius: 0.4rem;
  padding: var(--size-5); margin-bottom: var(--size-5);
}
.resource-group-box:last-child { margin-bottom: 0; }
.resource-group-box h4 { margin-top: 0; }

/* Card secondary area -- the "other" resource type for a card's job+date
   (trucks on a People-view card, technicians on a Trucks-view card). Also a
   drop target for adding one. */
.card-secondary {
  margin-top: 0.5rem; padding: 0.4rem 0.5rem; background: #f7f7f8;
  border-radius: 0.3rem; border: 1px dashed var(--accent-faded); min-height: 1.6rem;
}
.card-secondary-label {
  font-size: 0.85rem; text-transform: uppercase; color: var(--accent); margin-bottom: 0.25rem;
}
.resource-badge {
  display: inline-flex; align-items: center; gap: 0.3rem; background: #fff;
  border: 1px solid var(--accent-faded); border-radius: 0.3rem; padding: 0.2rem 0.5rem;
  font-size: 0.85rem; margin: 0 0.3rem 0.3rem 0;
}
.resource-badge i.fa-user-hard-hat, .resource-badge i.fa-truck { color: var(--primary); }
.remove-badge { color: var(--secondary); cursor: pointer; font-size: 0.85rem; margin-left: 0.1rem; }
.resource-badge-empty { font-size: 0.85rem; color: var(--accent); font-style: italic; }
/* A field whose value is a computed sum (e.g. Estimated Value once Billing
   line items exist) rather than something the user can type over directly. */
.readonly-derived {
  background: #eee !important; color: var(--accent); cursor: not-allowed;
}

/* Billing line items: "Hide All"/per-row eye toggle collapses everything
   except Source/Description/Total, see toggleAllBillingRowsBtn/
   toggle-billing-row-btn in jobs/form.html. */
.billing-row.collapsed .billing-detail-cell { display: none; }

.quick-assign-trigger {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.4rem; height: 1.4rem; min-height: 0; padding: 0; vertical-align: middle;
  border: 1px dashed var(--accent-faded); border-radius: 0.3rem; color: var(--primary);
}

/* Floating popover opened by .quick-assign-trigger -- a lightweight
   click-based alternative to dragging a resource token onto a card, see
   openQuickAssignPopover() in calendar/board.html. */
.quick-assign-popover {
  z-index: 1100; background: #fff; border: 1px solid var(--accent-faded);
  border-radius: 0.4rem; box-shadow: 0 0.3rem 1rem rgba(0,0,0,0.15);
  padding: 0.6rem; min-width: 14rem;
}
.quick-assign-popover-label { font-weight: bold; font-size: 0.85rem; margin-bottom: 0.4rem; }
.quick-assign-popover-actions { display: flex; gap: 0.4rem; margin-top: 0.5rem; }
.quick-assign-popover-actions .button { font-size: 0.85rem; padding: 0.3rem 0.6rem; }

/* Floating read-only notes popover -- see SCHED.showNotesPopover in sched.js. */
.notes-popover {
  z-index: 1100; background: #fff; border: 1px solid var(--accent-faded);
  border-radius: 0.4rem; box-shadow: 0 0.3rem 1rem rgba(0,0,0,0.15);
  padding: 0.6rem; width: 18rem; max-width: 80vw;
}
.notes-popover-label { font-weight: bold; font-size: 0.85rem; margin-bottom: 0.4rem; }
.notes-popover-text { font-size: 0.85rem; white-space: pre-wrap; max-height: 12rem; overflow-y: auto; }
.notes-popover-trigger { cursor: pointer; color: var(--accent); }
.notes-popover-trigger:hover { color: var(--primary); }

/* Jobs list quick-edit-dates icon -- see openReschedulePopover in jobs/list.html. */
.reschedule-trigger { cursor: pointer; color: var(--accent); margin-left: 0.3rem; }
.reschedule-trigger:hover { color: var(--primary); }

/* ── Span bars (People/Trucks board -- one bar per contiguous multi-day
   block, see _build_lane_spans) -- explicit grid-column/row placed, so this
   sits alongside .kanban-lane's implicit-flow single-day cells rather than
   inside one. */
.span-grid { align-items: stretch; }
.span-bar {
  position: relative; display: flex; align-items: center; justify-content: space-between;
  gap: 0.4rem; padding: 0.3rem 1.1rem; margin: 0.15rem 0; overflow: hidden;
}
.span-bar-drag-handle { flex: 1 1 auto; min-width: 0; cursor: grab; }
.span-bar-drag-handle .work-card-title, .span-bar-drag-handle .work-card-meta {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.span-bar-drag-handle .work-card-title { font-size: 0.85rem; }
.span-bar-drag-handle .work-card-meta { font-size: 0.85rem; }
.span-bar-actions { flex: 0 0 auto; display: flex; align-items: center; gap: 0.35rem; }
/* The job's billing value, sat with the block's other actions. Styled as a
   quiet pill rather than a button so a row of blocks reads as a schedule with
   figures on it, not a row of buttons. */
.span-bar-value {
  flex: 0 0 auto; font-size: 0.8rem; font-weight: 600; white-space: nowrap;
  padding: 0.1rem 0.4rem; border-radius: 0.25rem;
  background: #eafaf1; color: #1e8449; text-decoration: none;
}
.span-bar-value:hover { background: #d6f5e3; color: #14663a; }
.span-bar-view { color: var(--accent); font-size: 0.85rem; }
.span-bar-view:hover { color: var(--primary); }
.span-bar.locked { opacity: 0.85; border-left-color: var(--accent); }
.span-bar.dragging { opacity: 0.6; }
/* Mid-resize preview: the bar is showing where it WILL land, not where it is. */
.span-bar.resizing { opacity: 0.75; outline: 2px dashed var(--primary); }
.span-resize-handle {
  position: absolute; top: 0; bottom: 0; width: 0.6rem; cursor: ew-resize;
}
.span-resize-left { left: 0; }
.span-resize-right { right: 0; }
/* Vertical orientation: a span's "start"/"end" edges are its top/bottom
   (dates are rows), not its left/right -- same handles, transposed. */
.scheduling-grid.vertical .span-resize-handle { top: auto; bottom: auto; left: 0; right: 0; width: auto; height: 0.6rem; cursor: ns-resize; }
.scheduling-grid.vertical .span-resize-left { top: 0; }
.scheduling-grid.vertical .span-resize-right { bottom: 0; }
.scheduling-grid.vertical .span-bar { flex-direction: column; align-items: stretch; }
.span-resize-handle:hover { background: rgba(0, 0, 0, 0.08); }

/* ── Sticky toolbar (page header w/ Prior/Next + Filters panel) -- opt-in
   via the pin button, since some schedulers would rather have the extra
   vertical room and just scroll back up. Personal pref, localStorage only
   (see board.html), same as the rest of this page's layout state. top:7.3rem
   matches #mainWrapper's own margin-top so this sticks directly under the
   app's fixed header instead of underneath it. ─────────────────────────── */
.sched-toolbar.pinned {
  position: sticky;
  top: 7.3rem;
  z-index: 6;
  background: #fff;
  padding-top: 0.5rem;
  padding-bottom: var(--size-5);
  box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
#pinToolbarBtn.active { color: var(--primary); }
#pinToolbarBtn.active i { transform: rotate(-45deg); }

/* Each filter (Service Line / BU / Client / Technician) collapses on its
   own, independent of the whole Filters panel's own minimize toggle and
   independent of whether the toolbar above is pinned -- a plain class
   toggle on a plain block, unaffected by either. */
.filters-panel-body { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-start; }
.filter-group { flex: 1; min-width: 14rem; }
.filter-group-header {
  display: flex; justify-content: space-between; align-items: center;
  cursor: pointer; margin-bottom: 0.2rem;
}
.filter-group-header label { font-size: 0.85rem; color: var(--accent); margin: 0; }
/* .filter-group-toggle went with the chevrons -- a typeahead is one line
   tall and there is nothing left to collapse. */
.filter-group.collapsed .filter-group-body { display: none; }

/* Shown at the SAME grid cell as any span bar that might occupy that date --
   a day off is never invisible just because something's scheduled on it
   (see the day-off-marker comment in _board_body.html). Drawn as a red
   outline + X via two rotated pseudo-elements, on top of whatever's in the
   cell but never blocking clicks/drag-drop on it. */
/* THE TAG TAKES POINTER EVENTS BACK, and nothing else does. The marker covers
   the whole cell and must stay transparent to clicks so assigning a job to a
   day somebody is off still works.

   AND IT HAS TO BE BIG ENOUGH TO GRAB. At the base chip size this measured
   41 x 8 CSS pixels on the live board. Because the marker around it is
   pointer-events:none by design, a miss of four pixels vertically lands on the
   cell underneath and does nothing at all -- no cursor change, no helper, no
   error. An 8px-tall handle is not a handle, it is a coin toss, and it read as
   the feature being broken.

   Padded to ~24px tall and ~62px wide: still a small chip floating in the
   middle of the cell, so everything around it stays available for
   click-to-assign, but now a deliberate target. Every pixel this gains is a
   pixel the cell loses, which is why it is sized to the smallest thing a
   person can reliably hit rather than to the marker. Measured on the live
   board afterwards: about 11px of vertical slop from the centre, against 4px
   before.

   IN PIXELS, DELIBERATELY. This page sets html { font-size: 13px }, so the
   first version of this rule -- written in rem against an assumed 16px root --
   came out at 81% of its intended size and rendered the label at 8px, which is
   barely legible. A hit target is a physical thing a hand has to land on, not
   a typographic one that should scale with the page's text, so it is stated in
   the unit it is actually reasoned about in.

   AND IT STILL HAS TO LOOK LIKE A LABEL. The base tag is a translucent BLACK
   pill, which is unobtrusive at 41 x 8 and a heavy black box at 62 x 24 -- the
   first enlargement kept the fill and just scaled it up, and the result read
   as a black box dropped on the cell. So the grown chip is filled in the
   marker's own hue instead, with a hairline white edge: the same object as the
   border and the hatch around it, at a size you can hit. */
.day-off-tag.draggable-day-off {
  pointer-events: auto; cursor: grab;
  display: flex; align-items: center; justify-content: center;
  /* 20px still leaves about +/-9px of slop from the centre -- a normal mouse
     target -- while taking noticeably less of the cell than 24 did. */
  min-height: 20px; min-width: 56px;
  padding: 2px 7px;
  font-size: 10px;
  letter-spacing: 0.03em;
  background: var(--day-off-colour, #c0392b);
  border: 1px solid rgba(255, 255, 255, 0.8);
  border-radius: 3px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
  /* Or a touch drag scrolls the board instead of moving the day off. */
  touch-action: none;
}
.day-off-tag.draggable-day-off:active { cursor: grabbing; }

/* THE EXTEND GRIPS. The marker is pointer-events:none so the cell underneath
   keeps click-to-assign; these take it back, like the chip does, but they sit
   ON the cell's edge rather than in the middle of it -- which is the one strip
   where taking the pointer costs the cell nothing anybody aims for.

   Which edge is which depends on the layout, because the date axis does: in
   the vertical board dates run DOWN the rows, so a block ends at its bottom,
   and in the horizontal one they run ACROSS, so it ends at its right. Stated
   in pixels for the same reason the chip is -- this page's root is 13px, and a
   grab strip is a physical target. */
.day-off-resize {
  position: absolute;
  pointer-events: auto;
  z-index: 3;
  background: rgba(255, 255, 255, 0.35);
}
.day-off-resize:hover, .day-off-resize.resizing {
  background: #fff;
  box-shadow: 0 0 3px rgba(0, 0, 0, 0.4);
}
/* Dates ACROSS the columns: the ends are left and right. */
.day-off-resize { top: 3px; bottom: 3px; width: 8px; cursor: ew-resize; }
.day-off-resize-start { left: 0; border-radius: 2px 0 0 2px; }
.day-off-resize-end { right: 0; border-radius: 0 2px 2px 0; }
/* Dates DOWN the rows: the ends are top and bottom. */
.vertical .day-off-resize {
  top: auto; bottom: auto; left: 3px; right: 3px;
  width: auto; height: 8px; cursor: ns-resize;
}
.vertical .day-off-resize-start { top: 0; border-radius: 2px 2px 0 0; }
.vertical .day-off-resize-end { bottom: 0; border-radius: 0 0 2px 2px; }
/* While dragging, the whole marker reads as the thing being changed. */
.day-off-marker.resizing { outline: 2px solid #fff; outline-offset: -2px; }
.day-off-tag.ui-draggable-dragging { cursor: grabbing; z-index: 1000; }

.day-off-marker {
  position: relative; pointer-events: none; border: 2px solid var(--day-off-colour, #c0392b);
  border-radius: 0.3rem; margin: 0.15rem; z-index: 2;
  /* A repeating gradient, NOT two rotated bars. The bars were sized as a
     percentage of the cell and then rotated 45deg, which is fine in a roughly
     square cell and catastrophic in a wide one: a 45deg-rotated wide rectangle
     reaches far outside its own box, and nothing clipped it, so one person's
     day off drew diagonals clean across the whole board. A gradient is painted
     within the element by definition, so it cannot escape at any cell shape. */
  background-image: repeating-linear-gradient(
    45deg,
    var(--day-off-colour, #c0392b) 0, var(--day-off-colour, #c0392b) 1px,
    transparent 1px, transparent 7px);
  overflow: hidden;
}

/* ── Quick-create Job popup (right-click a calendar cell) -- same
   fixed-overlay-centered-panel convention as the Reassign Crew/Truck modal
   on jobs/detail.html, just generalized into reusable classes since this one
   holds an <iframe> (the real New Job form running in embed mode) rather
   than inline form fields. ─────────────────────────────────────────────── */
.modal-overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 50;
  align-items: center; justify-content: center;
}
/* THE TWO SHARED DIALOGS OUTRANK A PAGE'S OWN MODAL, always. They ask ABOUT an
   action, and that action is often started from inside another modal -- the
   project merge panel is the live example. Every .modal-overlay used to carry
   the same z-index, so the tie was broken by document order, and these two are
   declared in base.html near the top of <body>: they lost to every page modal
   on the site. The confirmation opened centred behind the merge panel, both
   buttons unclickable, and Merge looked like a dead button.

   window.confirm never had this problem because the browser draws it. Putting
   the dialog in the page is what makes stacking ours to get right. */
#confirmOverrideModal, #promptTextModal { z-index: 60; }

.modal-panel {
  background: #fff; border-radius: 0.5rem; width: 90%; max-width: 34rem;
  max-height: 85vh; display: flex; flex-direction: column; overflow: hidden;
  /* Without this the card sits flat on the dim layer and reads as part of the
     page rather than over it -- which is most of what made the old dialog look
     wrong, alongside the header that was never styled at all. */
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
}
.modal-panel-wide { max-width: 92vw; width: 92vw; height: 90vh; }
.modal-panel-fullscreen { max-width: 96vw; width: 96vw; height: 94vh; max-height: 94vh; }
.modal-panel-header {
  display: flex; justify-content: space-between; align-items: center;
  padding: var(--size-6) var(--size-7); border-bottom: 1px solid var(--accent-faded);
  font-weight: bold; flex: 0 0 auto;
}
.modal-panel iframe { flex: 1 1 auto; width: 100%; border: none; }

/* ── Calendar month-grid view (read-only overview; see _month_body.html) ── */
.month-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 0.4rem; }
.month-daycol-header {
  font-weight: bold; font-size: 0.85rem; text-align: center; color: var(--accent);
  text-transform: uppercase; padding-bottom: 0.3rem;
}
.month-day-cell {
  background: #f7f7f8; border-radius: 0.4rem; padding: 0.4rem; min-height: 7.5rem;
  display: flex; flex-direction: column; gap: 0.25rem;
}
.month-day-cell.other-month { opacity: 0.5; }
.month-day-cell.month-day-today { outline: 2px solid var(--primary); outline-offset: -2px; }
.month-day-number-row { display: flex; justify-content: space-between; align-items: center; }
.month-day-number { font-weight: 600; font-size: 0.85rem; }
.month-day-link { color: var(--accent); font-size: 0.85rem; }
.month-day-link:hover { color: var(--primary); }
.month-job-chip {
  display: block; background: #fff; border: 1px solid var(--accent-faded); border-radius: 0.3rem;
  padding: 0.15rem 0.4rem; font-size: 0.85rem; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; color: inherit;
}
.month-job-chip:hover { border-color: var(--primary); color: var(--primary); }
.month-job-chip-more { font-size: 0.85rem; color: var(--accent); }
.month-job-chip-more:hover { color: var(--primary); }

/* ── Calendar kanban board ───────────────────────────────────────────── */
.kanban-day-col { min-width: 15.5rem; }
.kanban-lane {
  background: #f7f7f8; border-radius: 0.4rem; padding: 0.4rem; margin-bottom: 0.6rem; min-height: 3rem;
  /* Lanes are CSS grid cells (see .scheduling-grid) with the default
     align-self: stretch, which fights the native resize handle on every
     reflow -- a manual per-lane drag-resize never reliably stuck. Use the
     zoom control (board.html) to scale the whole grid instead; a lane with
     more cards than fits just scrolls internally. */
  overflow-y: auto;
}

/* The same cell on a day that already has work. Present only so a job can be
   dropped onto an occupied day -- the span is drawn over it, so it must take
   no space and show nothing of its own, or every booked day would carry a grey
   slab behind the card and a 0.6rem gap under it. */
.kanban-lane.occupied {
  background: transparent;
  margin-bottom: 0;
  padding: 0;
  /* min-height is NOT zeroed. These are grid items with the default
     align-self: stretch, so they fill their track -- but a track whose only
     other content is a span bar still needs the cell to have a box for
     jQuery UI droppable to intersect. Collapsing it to nothing is what made
     the first attempt at this look right and still refuse the drop. */
  min-height: 2rem;
  pointer-events: auto;
}
/* Only while something is being dragged: show the free slots beside a job so
   it is obvious where a second booking can land. Without this the target is
   real but invisible, which is the same problem in a different disguise. */
body.dragging-job .kanban-lane.occupied {
  background: rgba(63, 124, 172, 0.07);
  outline: 1px dashed rgba(63, 124, 172, 0.45);
  outline-offset: -2px;
}
/* Horizontal scroll for the wide day-by-day grid, and vertical scroll as a
   fallback -- the panel's OWN resize handle (.board-panel, both dimensions)
   and the zoom control both change the calendar's effective size now, so
   this doesn't need a resize handle of its own. */
.scheduling-calendar-scroll {
  overflow: auto; min-height: 22rem; height: 100%; border-radius: 0.3rem;
}
/* Mirrors the main grid's horizontal scrollbar at the top of the panel too --
   with a tall day x lane grid, the real scrollbar (bottom of a possibly very
   long scroll area) can be a long way from the day labels you're trying to
   navigate by. Kept in sync with the real scroll position via JS
   (board.html's initCalendarTopScroll). */
.scheduling-calendar-top-scroll {
  overflow-x: auto; overflow-y: hidden; height: 1rem; margin-bottom: 0.4rem;
}
.scheduling-calendar-top-scroll-inner { height: 1px; }
.sched-zoom-controls { display: flex; align-items: center; gap: 0.3rem; margin-right: 0.5rem; }
.sched-zoom-controls .button { width: 1.75rem; height: 1.75rem; min-width: 0; padding: 0; font-size: 0.85rem; }
.sched-zoom-level { font-size: 0.85rem; color: var(--accent); min-width: 2.8rem; text-align: center; }
.kanban-lane-header {
  font-weight: bold; font-size: 0.85rem; text-transform: uppercase; color: var(--accent);
  padding: 0.15rem 0.3rem 0.4rem; display: flex; justify-content: space-between;
}
.kanban-lane.drag-over { background: var(--primary-faded); outline: 2px dashed var(--primary); outline-offset: -2px; }
.work-card.kanban-card { cursor: grab; margin-bottom: 0.4rem; }
.work-card.kanban-card.dragging { opacity: 0.35; }
.work-card.kanban-card:last-child { margin-bottom: 0; }

/* ── Page layout components -- shared card/section/banner pattern used
   across every page so section grouping, spacing, and headings read the
   same everywhere (established on admin_master_data/index.html). ──────── */
.page-wrap {
  padding: var(--size-7);
  max-width: 75rem;
}
.page-wrap.wide { max-width: none; }

.page-header {
  margin-bottom: var(--size-7);
}
.page-header h2 {
  margin: 0 0 0.2rem;
}
.page-header .page-subtitle {
  color: var(--accent);
  font-size: 0.95rem;
}

.page-section {
  background: #fff;
  border: 1px solid var(--accent-faded);
  border-radius: 0.5rem;
  padding: var(--size-6);
  margin-bottom: var(--size-8);
  box-shadow: var(--shadow);
}
.page-section:last-child { margin-bottom: 0; }

.page-section-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 var(--size-6);
  padding-bottom: var(--size-4);
  border-bottom: 1px solid var(--accent-faded);
  font-size: 1.05rem;
}
.page-section-title i { color: var(--primary); }

.page-section-table-wrap { overflow-x: auto; }

.info-banner {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  background: #f0f6f8;
  border: 1px solid var(--primary-faded);
  border-radius: 0.4rem;
  padding: var(--size-5) var(--size-6);
  margin-bottom: var(--size-7);
  font-size: 0.9rem;
  color: var(--dark);
}
.info-banner i { color: var(--primary); margin-top: 0.15rem; }
.info-banner:last-child { margin-bottom: 0; }

/* ── Dashboard working-list columns -- fixed 4-across grid (not auto-fit,
   which can leave a column short and wrap it below unpredictably) so
   Requested/Unconfirmed/Overdue/Upcoming always sit side by side on a normal
   desktop width. Each column scrolls its OWN body past a height cap and can
   collapse independently, so a bucket with 150 items grows a scrollbar
   instead of stretching the whole page. ──────────────────────────────────── */
.working-list-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--size-6);
  align-items: start;
}
@media only screen and (max-width: 1100px) {
  .working-list-grid { grid-template-columns: repeat(2, 1fr); }
}
@media only screen and (max-width: 650px) {
  .working-list-grid { grid-template-columns: 1fr; }
}
.working-list-col-header {
  display: flex; justify-content: space-between; align-items: center; gap: 0.5rem;
  cursor: pointer; text-transform: uppercase; color: var(--accent); font-size: 0.85rem;
  margin-bottom: var(--size-4); padding-bottom: 0.3rem; border-bottom: 1px solid var(--accent-faded);
}
.working-list-col-header:hover { color: var(--primary); }
.working-list-col-toggle { transition: transform 0.15s ease; flex-shrink: 0; }
.working-list-col.collapsed .working-list-col-toggle { transform: rotate(-90deg); }
.working-list-col.collapsed .working-list-col-body { display: none; }
.working-list-col-body {
  max-height: 34rem;
  overflow-y: auto;
  padding-right: 0.4rem;
}

/* ── Searchable select / multiselect (type-ahead combobox) ───────────── */
.searchable-select-wrap, .searchable-multiselect-wrap { position: relative; display: block; }
.searchable-select-hidden { position: absolute; opacity: 0; height: 0; width: 0; pointer-events: none; }
/* The wrap above is a block, not a flex container, so .text-input's flex:1
   does nothing here -- without an explicit width the visible input falls
   back to native sizing (~20 characters) and looks short inside whatever
   column/cell its wrap actually occupies. */
.searchable-select-input { width: 100%; box-sizing: border-box; }
.searchable-select-list {
  display: none; position: absolute; top: 100%; left: 0; right: 0; z-index: 50;
  background: #fff; border: 1px solid var(--accent-faded); border-radius: 0.3rem;
  max-height: 14rem; overflow-y: auto; box-shadow: 0 2px 8px rgba(0,0,0,0.12); margin-top: 2px;
}
.searchable-select-item { padding: 0.4rem 0.6rem; cursor: pointer; font-size: 0.9rem; }
.searchable-select-item:hover, .searchable-select-item.active { background: #f0f4f8; }
.searchable-select-empty { padding: 0.4rem 0.6rem; font-size: 0.85rem; color: var(--accent); }
.searchable-select-create { color: var(--primary); border-top: 1px solid var(--accent-faded); }
.searchable-select-create i { margin-right: 0.3rem; }
.searchable-multiselect-chips { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-bottom: 0.3rem; }
.searchable-multiselect-chip {
  display: inline-flex; align-items: center; gap: 0.3rem; background: var(--primary-faded);
  color: var(--primary); border-radius: 1rem; padding: 0.2rem 0.55rem 0.2rem 0.75rem; font-size: 0.85rem;
}
.searchable-multiselect-chip-remove { cursor: pointer; font-size: 0.85rem; }
.searchable-multiselect-chip-remove:hover { color: var(--secondary); }

@media only screen and (max-width: 480px) {
  h2 { font-size: 1.4rem; }
  table.tablesorter th, table.tablesorter td { padding: 0.4rem 0.3rem; font-size: 0.85rem; }
}


/* ── Job board quick-view tabs ───────────────────────────────────────────────
   Plain links, not JS tabs: each is a real URL, so a particular view is
   bookmarkable and the back button behaves. The count sits in the tab because
   the useful question is usually "is there anything in there", and opening each
   one to find out is the slow way to answer it. */
.job-view-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin-bottom: var(--size-6);
  border-bottom: 1px solid var(--accent-faded);
}
.job-view-tab {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.9rem;
  color: var(--accent);
  text-decoration: none;
  font-size: 0.9rem;
  border: 1px solid transparent;
  border-bottom: none;
  border-radius: 0.3rem 0.3rem 0 0;
  margin-bottom: -1px;
}
.job-view-tab:hover {
  color: var(--primary);
  background: #f2f5f6;
}
.job-view-tab.active {
  color: var(--primary);
  font-weight: 600;
  background: #fff;
  border-color: var(--accent-faded);
  border-bottom: 1px solid #fff;
}
/* Readable on its own, unlike --accent-faded which is #e7e7e7 and would put
   near-white digits on a near-white tab. */
.job-view-count {
  font-size: 0.75rem;
  font-weight: 600;
  color: #55595e;
  background: #e4e7e9;
  border-radius: 0.75rem;
  padding: 0.05rem 0.45rem;
  min-width: 1.5rem;
  text-align: center;
}
.job-view-tab.active .job-view-count {
  color: #fff;
  background: var(--primary);
}

/* Status on a calendar card. Deliberately tiny and inline with the meta line:
   a span bar can be a single grid cell wide, and a second line would push the
   site label out of view on every one-day job. Reuses the .status-* background
   colours so a status reads the same here as on the job board. */
.span-bar-status {
  display: inline-block;
  padding: 0 0.3rem;
  border-radius: 0.2rem;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: #fff;
  vertical-align: 0.05rem;
}

/* A related pair of toolbar controls, boxed so they read as one thing. The
   toolbar is a flat flex row, so without this the bin's mode select sat exactly
   as close to the Trucks toggle beside it as to the Job Bin toggle it actually
   belongs to. */
.toolbar-group {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--accent-faded);
  border-radius: 0.3rem;
  background: #f7f9fa;
  overflow: hidden;
}
.toolbar-group > .button { border-radius: 0; margin: 0; }
.toolbar-group-divider { width: 1px; background: var(--accent-faded); }

/* What the bin is listing. Borderless inside the group -- the group draws the
   frame, and a second border around the select would box a box. */
.bin-pool-select {
  font-size: 0.85rem;
  padding: 0.35rem 0.5rem;
  border: none;
  border-radius: 0;
  background: transparent;
  color: #55595e;
  cursor: pointer;
}
.bin-pool-select:hover { color: var(--primary); background: #eef4f6; }
.bin-pool-select:focus { outline: 2px solid var(--primary); outline-offset: -2px; }



/* ── Unavailability markers, by kind ─────────────────────────────────────────
   Time off, travel and blocked all mean "not available for job work", so they
   share the marker; the colour and the little tag say which. Distinct hues
   rather than shades of one, because the useful glance is "why is this person
   not free", not "how unavailable are they". */
.day-off-marker { position: relative; }
.day-off-tag {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  font-size: 0.55rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #fff;
  background: rgba(0, 0, 0, 0.55);
  border-radius: 0.15rem;
  padding: 0 0.25rem;
  pointer-events: none;
  white-space: nowrap;
}
/* Kind only changes the hue -- the border and hatch both read it. */
.day-off-travel  { --day-off-colour: #1f6f8b; }
.day-off-blocked { --day-off-colour: #6c4675; }


/* ── Right-click menu on a calendar cell ─────────────────────────────────────
   Everything reachable from a cell in one place. Right-click used to go
   straight into Quick Create, so making a job was the only thing the gesture
   could do, and booking time off was an icon in the lane label that was easy to
   miss entirely. */
.calendar-context-menu {
  /* fixed, positioned from the cell's getBoundingClientRect() -- absolute would
     be relative to whichever ancestor happens to be positioned, and the board
     is inside a CSS-zoomed scroll container. */
  position: fixed;
  z-index: 1400;
  min-width: 15rem;
  padding: 0.25rem 0;
  background: #fff;
  border: 1px solid var(--accent-faded);
  border-radius: 0.35rem;
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.18);
}
.calendar-context-header {
  padding: 0.35rem 0.75rem 0.4rem;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: #7b8085;
  border-bottom: 1px solid #eef0f1;
}
.calendar-context-item {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  width: 100%;
  padding: 0.45rem 0.75rem;
  border: none;
  background: none;
  text-align: left;
  font-size: 0.85rem;
  color: #33383d;
  cursor: pointer;
}
.calendar-context-item:hover { background: #eef4f6; color: var(--primary); }
.calendar-context-item i { width: 1rem; text-align: center; color: #7b8085; }
.calendar-context-item:hover i { color: var(--primary); }
.calendar-context-item.danger:hover { background: #fdeeec; color: #b03a2e; }
.calendar-context-item.danger:hover i { color: #b03a2e; }
.ctx-hint { color: #9aa0a6; font-size: 0.75rem; }
.calendar-context-sep { height: 1px; margin: 0.25rem 0; background: #eef0f1; }

/* ────────────────────────────────────────────────────────────
   Job Gantt (Capacity Plan). One row per job, one continuous bar across the
   days it runs -- the thing the old board lens never did, since it drew a
   separate badge per day. Header, empty cells and bar all share one grid
   (gantt_cols, built in routes/capacity.py) so they cannot drift apart.
   ──────────────────────────────────────────────────────────── */
.gantt { min-width: 100%; }
.gantt-head,
.gantt-row { display: grid; align-items: stretch; }
.gantt-head { position: sticky; top: 0; z-index: 2; background: #fff; }
.gantt-day {
  text-align: center; font-size: 0.75rem; padding: 0.3rem 0.1rem;
  border-bottom: 2px solid var(--primary); color: var(--accent); line-height: 1.2;
}
.gantt-row { position: relative; border-bottom: 1px solid var(--accent-faded); min-height: 2.4rem; }
.gantt-label-cell {
  grid-column: 1; padding: 0.35rem 0.5rem; font-size: 0.85rem;
  position: sticky; left: 0; background: #fff; z-index: 1;
  border-right: 1px solid var(--accent-faded); overflow: hidden;
}
.gantt-sub { color: var(--accent); font-size: 0.75rem; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
.gantt-cell { border-left: 1px solid var(--accent-faded); }
.gantt-weekend { background: #fafafa; }
/* Same row as its cells, laid over them -- grid-row 1 because each .gantt-row
   is its own grid. Colour comes from the shared .status-* classes above, so a
   bar matches the job's badge everywhere else. */
.gantt-bar {
  grid-row: 1; align-self: center; margin: 0.2rem 0;
  border-radius: 0.2rem; padding: 0.2rem 0.4rem;
  font-size: 0.75rem; color: #fff; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}

/* ────────────────────────────────────────────────────────────
   Month view (Calendar of Work). Seven equal columns, whole weeks -- the point
   is the SHAPE of a month, which the board's 30-day range preset cannot give
   because it stretches one timeline sideways.
   ──────────────────────────────────────────────────────────── */
.month-grid {
  display: grid; grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 1px; background: var(--accent-faded); border: 1px solid var(--accent-faded);
}
.month-dow {
  background: #fff; padding: 0.4rem; text-align: center;
  font-size: 0.8rem; text-transform: uppercase; color: var(--accent); font-weight: 600;
}
.month-cell {
  background: #fff; min-height: 7rem; padding: 0.3rem;
  display: flex; flex-direction: column; gap: 0.15rem;
}
.month-weekend { background: #fafafa; }
/* Padding days from the neighbouring month: present so every row is a full
   week, dimmed so they are not mistaken for this month's work. */
.month-outside { background: #f4f4f4; }
.month-outside .month-date { color: #b9b9b9; }
.month-today { box-shadow: inset 0 0 0 2px var(--primary); }
.month-date { font-size: 0.8rem; color: var(--accent); font-weight: 600; }
.month-chip {
  display: block; border-radius: 0.2rem; padding: 0.1rem 0.3rem;
  font-size: 0.72rem; color: #fff; text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.month-chip:hover { filter: brightness(1.12); color: #fff; }
.month-more { font-size: 0.7rem; color: var(--accent); padding-left: 0.2rem; }

/* Who else is on this job, under the block's own meta line. Muted and small:
   it answers "am I alone on this?" at a glance without competing with the job
   name. A job renders once per person, so without it a two-hander reads as two
   unrelated single-person jobs sitting in different lanes. */
.span-bar-crew {
  font-size: 0.7rem;
  /* WAS WHITE, ON A WHITE CARD. .span-bar inherits .work-card's #fff
     background, so this line has been invisible since it was added -- and the
     BU/service-line tint that sits behind it now is a 12%-alpha wash, which is
     still light. Uses the same muted grey as .work-card-meta directly above
     it, so the two meta lines read as one block rather than two weights. */
  color: var(--accent);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 0.1rem;
}
.span-bar-crew i { opacity: 0.8; margin-right: 0.15rem; }

/* Fuzzy contact suggestions under the onsite-contact name box. Anchored to the
   input's own wrapper so it tracks the field in the form grid. */
.onsite-suggest {
  position: absolute; z-index: 60; top: 100%; left: 0; right: 0;
  background: #fff; border: 1px solid var(--accent-faded); border-top: none;
  border-radius: 0 0 0.3rem 0.3rem; box-shadow: 0 0.3rem 0.8rem rgba(0,0,0,0.12);
  max-height: 14rem; overflow-y: auto;
}
.onsite-suggest-item { padding: 0.35rem 0.5rem; cursor: pointer; font-size: 0.85rem; }
.onsite-suggest-item:hover, .onsite-suggest-item.active { background: #eef4f6; }
.onsite-suggest-item .osi-name { font-weight: 600; }
.onsite-suggest-item .osi-meta { color: var(--accent); font-size: 0.78rem; }

/* The Filters panel as a popup, opened from the Filter button in the toolbar.
   Absolutely positioned so opening it does not push the board down -- the
   point of the change was to give the grid back the vertical space the panel
   used to hold permanently. z-index clears the sticky day-label header
   (z-index 2) and the lane labels (1). */
.filters-popup {
  position: absolute;
  z-index: 20;
  right: 0;
  min-width: min(52rem, 95vw);
  max-width: 95vw;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
  border: 1px solid var(--accent-faded);
  background: #fff;
}
/* The toolbar is the positioning context, so the popup hangs under the
   button rather than off the page corner. */
.sched-toolbar { position: relative; }
#filtersToggleBtn.active { background: var(--primary); color: #fff; }

/* ---------------------------------------------------------------------------
   Jobs list: column filters and in-place editing
   --------------------------------------------------------------------------- */
/* The filter row sits inside <thead>, under the labels. It is deliberately
   quieter than the header above it -- it is a control strip, not a second set
   of headings -- and its cells drop the header's sort cursor and padding. */
#jobsTable thead tr.column-filters th {
  padding: 0.25rem 0.4rem 0.4rem;
  background: var(--panel, #fff);
  border-top: 0;
  cursor: default;
  font-weight: normal;
  text-transform: none;
  letter-spacing: 0;
}
#jobsTable thead tr.column-filters select,
#jobsTable thead tr.column-filters input[type="search"],
#jobsTable thead tr.column-filters input[type="date"] {
  width: 100%;
  min-width: 0;          /* or a long client name forces the column wide */
  box-sizing: border-box;
  font-size: 0.78rem;
  padding: 0.2rem 0.3rem;
  border: 1px solid var(--border, #d7dbe0);
  border-radius: 3px;
  background: #fff;
  color: inherit;
}

/* The scheduled and client-preferred dates used to be wrapped in <em>, which
   made them the only italic text on the page. They inherit the table's font
   now; this just stops them wrapping mid-date, which is what made the single
   combined column hard to read. */
#jobsTable td.sched-date {
  white-space: nowrap;
  font-style: normal;
}

/* The three in-place editors. Dotted underline rather than a button: the cell
   still reads as data, with a hint that it can be changed. */
#jobsTable .inline-status,
#jobsTable .inline-crew {
  cursor: pointer;
}
#jobsTable .inline-crew {
  border-bottom: 1px dotted var(--accent, #8a9199);
}
#jobsTable .inline-crew:hover,
#jobsTable .inline-status:hover {
  filter: brightness(0.94);
}
/* Always visible, never hover-only. Hidden until hover, the dates read as
   plain text and the only way to find out they could be edited was to happen
   to move the pointer over the row. It carries the link colour so it reads as
   something you can act on. */
#jobsTable .reschedule-trigger {
  cursor: pointer;
  margin-left: 0.4rem;
  color: var(--primary, #3f7cac);
  opacity: 0.75;
  transition: opacity 0.12s;
}
#jobsTable .reschedule-trigger:hover { opacity: 1; }

.inline-edit-popover { min-width: 14rem; }

/* ---------------------------------------------------------------------------
   Requester form grid
   --------------------------------------------------------------------------- */
/* A <select> takes its intrinsic width from its LONGEST option, and a grid
   item defaults to min-width:auto, which means "never shrink below the
   content". Together those pushed the Service Type dropdown out through the
   right-hand edge of its panel -- the longest service line name is
   "SCVF (Surface Casing Vent Flow) & Gas Migration". min-width:0 lets the
   column shrink to its track, and the control then fills that track rather
   than defining it. */
.request-grid > div { min-width: 0; }
.request-grid select,
.request-grid input,
.request-grid textarea {
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
}
/* A long option still has to go somewhere once the control is narrow. Ellipsis
   beats a horizontal scrollbar inside a dropdown. */
.request-grid select {
  text-overflow: ellipsis;
}

/* .text-input is flex:1, which does nothing unless it is inside a flex
   .text-input-container -- the same trap already noted above for
   .searchable-select-input. The Additional Information textarea has the class
   but no container, so it fell back to the browser native size (~20 characters
   wide) and the only way to see more was to drag it taller. Full width of the
   panel, and resizing stays vertical because widening it past the panel is
   never what is wanted. */
#requestForm textarea.text-input {
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
  resize: vertical;
  min-height: 6rem;
}

/* ---------------------------------------------------------------------------
   Job form: the action row stays put
   --------------------------------------------------------------------------- */
/* The form runs to 69 fields across 32 sections, so Save was a long scroll from
   most of it. Sticky rather than fixed: it keeps its place in the flow, so it
   cannot cover the last field and needs no spacer under it. The negative margins
   let the bar span the full panel width while its buttons stay aligned with the
   fields above. */
.job-form-actions {
  display: flex;
  gap: var(--size-5);
  align-items: center;
  flex-wrap: wrap;
  position: sticky;
  bottom: 0;
  z-index: 8;                    /* under the header (9), over the form */
  background: #fff;
  border-top: 1px solid var(--accent-faded);
  padding: var(--size-5) var(--size-6);
  margin: var(--size-6) calc(var(--size-6) * -1) 0;
  box-shadow: 0 -0.2rem 0.6rem rgba(0, 0, 0, 0.06);
}

/* A block of time off that runs past the edge of the window is drawn square
   on that side, so a clipped bar does not read as one that ends there. The
   handle is withheld on the same condition -- dragging an edge you cannot see
   would move a boundary somewhere off screen. */
.day-off-marker.day-off-clipped-start { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.day-off-marker.day-off-clipped-end { border-top-right-radius: 0; border-bottom-right-radius: 0; }


/* ── Client Schedule -- an MS Project Level 1 view ─────────────────────────
   Three columns on one CSS grid: the task table (client / work), what we are
   telling the client about timing, and the chart. Prefixed cs- because the
   capacity planner above already owns .gantt and four of its children.

   The gridlines are NOT grid items -- they are one absolutely positioned
   overlay inset past the two fixed columns, so a month line is continuous
   down the whole chart instead of being redrawn in every row and never quite
   lining up. That is also why columns 1 and 2 are fixed widths: the overlay's
   left edge is calc()'d from them, and a fractional column would leave it
   guessing.

   EVERY COLOUR HERE IS A MEANING, not decoration -- solid is committed,
   hatched is a window that may move, grey is finished -- so all of it is
   forced through print-color-adjust. A client schedule printed without its
   hatching says the opposite of what it means. */
.cs-scroll { overflow-x: auto; }
.cs-chart {
  --gc1: 21rem;
  --gc2: 11rem;
  position: relative;
  display: grid;
  grid-template-columns: var(--gc1) var(--gc2) minmax(22rem, 1fr);
  min-width: 68rem;
  font-size: 0.9rem;
}
.cs-grid {
  position: absolute; top: 0; bottom: 0;
  left: calc(var(--gc1) + var(--gc2)); right: 0;
  pointer-events: none; z-index: 0;
}
.cs-gridline { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--accent-faded); }
.cs-today { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--secondary); opacity: 0.55; }

.cs-head {
  position: relative; height: 2.4rem;
  border-bottom: 2px solid var(--primary);
  font-weight: 700; color: var(--primary-darker);
  display: flex; align-items: flex-end; padding-bottom: 0.25rem;
}
.cs-month {
  position: absolute; bottom: 0.2rem;
  font-size: 0.82rem; font-weight: 400; color: var(--accent);
  /* BORDER-BOX. As a content box the padding was added to the width the
     template had already computed as a percentage of the track, so every
     label was ~4px wider than its month and the last one hung past the
     right-hand edge -- a permanent 4px scrollbar inside .cs-scroll. */
  box-sizing: border-box;
  padding-left: 0.2rem;
  border-left: 1px solid transparent;
  white-space: nowrap; overflow: hidden;
  line-height: 1.15;
}
.cs-month.year-start { font-weight: 700; color: var(--primary-darker); }
.cs-month small { display: block; font-size: 0.7rem; line-height: 1; opacity: 0.85; }
.cs-head { align-items: flex-end; }

/* Hidden by default, revealed inside @media print. ORDER IS THE WHOLE
   RULE: this and the print-media rule have equal specificity, so whichever
   comes last wins in both media. Written the other way round it was
   display:none while printing, the forced page breaks generated no boxes,
   and fifteen of the sixteen PDF sheets came out with no date axis on them.
   Do not move this below the @media print block. */
.cs-pagehead { display: none; }

.cs-cell {
  position: relative; z-index: 1;
  border-bottom: 1px solid var(--accent-faded);
  padding: 0.3rem 0.4rem; min-height: 2rem;
}
.cs-c3.cs-cell { padding-left: 0; padding-right: 0; }

/* Level 1: the client. Its row is the summary, the jobs under it are the
   detail, and the indent on .cs-task is the only thing saying so. */
.cs-group {
  background: var(--primary-faded);
  font-weight: 700; color: var(--primary-darker);
  border-bottom: 1px solid var(--primary);
}
.cs-count { font-weight: 400; color: var(--accent); font-size: 0.8rem; margin-left: 0.4rem; }
.cs-task { padding-left: 1.4rem; }
.cs-task-name { font-weight: 600; display: block; }
.cs-sites, .cs-desc, .cs-dates {
  display: block; color: var(--accent); font-size: 0.8rem; line-height: 1.3;
}
.cs-desc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cs-nobar {
  display: inline-block; margin-left: 0.4rem;
  color: var(--accent); font-size: 0.8rem; font-style: italic;
}

/* The summary bracket -- open at the bottom, the way MS Project draws a
   rolled-up parent, so it never reads as a fourth kind of bar. */
.cs-summary {
  /* THE INSETS ARE CAPPED AT A SHARE OF THE WIDTH, not fixed. A bracket can
     be as narrow as the 3px floor, and a flat 0.32rem inset is 4.16px --
     wider than the whole box, so `calc(100% - 0.32rem)` went NEGATIVE and
     the two inner vertices crossed over each other. The taper rendered as a
     forked swallowtail on every single-job group. */
  --cs-taper: min(0.32rem, 22%);
  --cs-arrow: min(0.35rem, 30%);
  position: absolute; top: 0.45rem; height: 0.85rem;
  min-width: 3px;
  background: var(--primary-darker);
  /* The MS Project summary shape -- a bar that tapers down at each end --
     cut out of the element's own box. It was two ::after triangles hung off
     the ends, which on a 2px bar flared 7.8px out each side and rendered as
     a downward-pointing anchor rather than a bracket. */
  clip-path: polygon(0 0, 100% 0, 100% 100%,
                     calc(100% - var(--cs-taper)) 45%,
                     var(--cs-taper) 45%, 0 100%);
}
/* A bracket whose work runs past the edge keeps the left taper and gains an
   arrowhead in place of the right one. Two classes, so it beats the
   single-class .cs-continues.
   SEVEN VERTICES, NOT SIX. The first version went straight from the bottom
   of the arrowhead to the LEFT inner vertex, with no right inner vertex at
   all -- so the underside was one diagonal running the entire length of the
   bar, and the widest bracket on the page read as a long wedge rather than
   as a summary. The vertex that comes back up to the band is the fix. */
.cs-summary.cs-continues {
  clip-path: polygon(0 0, calc(100% - var(--cs-arrow)) 0, 100% 50%,
                     calc(100% - var(--cs-arrow)) 100%,
                     calc(100% - var(--cs-arrow)) 45%,
                     var(--cs-taper) 45%, 0 100%);
}

.cs-bar {
  position: absolute; top: 0.45rem; height: 0.95rem;
  /* A one-day job is 1.34px at the capped scale. Floored so it reads as a
     bar rather than as a stray tick; the floor is in px because the
     percentage floor in client_presentation cannot know the track's width. */
  border-radius: 0.15rem; min-width: 3px;
}
.cs-bar.kind-confirmed { background: var(--primary); border: 1px solid var(--primary-dark); }
.cs-bar.kind-complete  { background: #b9bcc0; border: 1px solid var(--accent); }
/* Hatched, and deliberately the only patterned bar on the chart: this is the
   one that is NOT a commitment, and it has to look different from across a
   boardroom table rather than on close reading of a legend. */
.cs-bar.kind-tentative {
  background: repeating-linear-gradient(135deg,
      #cfe3ea 0 0.3rem, #ffffff 0.3rem 0.6rem);
  border: 1px dashed var(--primary);
}

/* The axis stops before the last of the work does -- see the horizon cap in
   client_presentation.timeline(). A bar that runs past the edge is cut with
   a notch rather than squared off, so it does not read as a finish date; one
   that starts past the edge has no bar at all, only an arrow. Neither is
   hidden: the dates are spelled out in the Scheduling column either way. */
.cs-continues {
  border-right: none;
  clip-path: polygon(0 0, calc(100% - 0.35rem) 0, 100% 50%,
                     calc(100% - 0.35rem) 100%, 0 100%);
}
.cs-offchart {
  /* Pinned to the right edge, NOT placed at left:99%. The marker has a width
     of its own that a percentage cannot account for, and at 99% every one of
     them hung ~6px past the end of the track. */
  position: absolute; top: 0.3rem; right: 0;
  color: var(--primary); font-size: 1rem; line-height: 1;
}
.cs-beyond-note {
  margin: 0 0 0.6rem; font-size: 0.82rem; color: var(--accent);
}

/* ── Kind pills and legend ───────────────────────────────────────────── */
.client-pill {
  display: inline-block; padding: 0.1rem 0.45rem; border-radius: 0.8rem;
  font-size: 0.78rem; font-weight: 700; white-space: nowrap;
}
.client-pill.kind-confirmed { background: var(--primary); color: #fff; }
.client-pill.kind-tentative { background: var(--primary-faded); color: var(--primary-darker); border: 1px dashed var(--primary); }
.client-pill.kind-complete  { background: #eef0f1; color: var(--accent); }
.client-pill.kind-requested { background: #fdf2e9; color: var(--secondary); }
.client-pill.kind-tbd       { background: #f2f3f4; color: var(--accent); }

.client-legend {
  display: flex; flex-wrap: wrap; gap: 0.4rem 1.4rem;
  margin: 0 0 0.8rem; font-size: 0.82rem; color: var(--accent);
}
.client-legend-item { display: flex; align-items: center; gap: 0.35rem; }
.client-legend strong { color: var(--primary-darker); }
.client-swatch { width: 1.6rem; height: 0.7rem; border-radius: 0.15rem; flex-shrink: 0; }
.client-swatch.kind-confirmed { background: var(--primary); }
.client-swatch.kind-complete  { background: #b9bcc0; }
.client-swatch.kind-tentative {
  background: repeating-linear-gradient(135deg, #cfe3ea 0 0.3rem, #ffffff 0.3rem 0.6rem);
  border: 1px dashed var(--primary);
}
.client-swatch.kind-requested { background: #fdf2e9; border: 1px solid var(--secondary); }
.client-swatch.kind-tbd       { background: #f2f3f4; border: 1px solid var(--accent); }

/* ── The disclaimer ──────────────────────────────────────────────────────
   Not .no-print, unlike every other banner in this app. The whole reason it
   exists is the copy that leaves the building. */
.client-disclaimer {
  display: flex; gap: 0.6rem; align-items: flex-start;
  background: #fdf2e9; border: 1px solid var(--secondary); border-left-width: 4px;
  border-radius: 0.3rem; padding: 0.6rem 0.8rem; margin-bottom: 0.8rem;
  color: #7d3200;
}
.client-disclaimer i { margin-top: 0.15rem; }
.client-disclaimer-sub { font-size: 0.85rem; margin-top: 0.2rem; }
.client-footer-note {
  margin-top: 1rem; padding-top: 0.5rem; border-top: 1px solid var(--accent-faded);
  font-size: 0.8rem; color: var(--accent); font-style: italic;
}
.client-print-head { margin-bottom: 0.6rem; }

@media print {
  /* Without this every bar prints white, and a schedule whose confirmed and
     tentative work look identical is worse than no schedule. */
  .cs-chart, .cs-chart *, .client-pill, .client-swatch, .client-disclaimer {
    -webkit-print-color-adjust: exact; print-color-adjust: exact;
  }
  .cs-scroll { overflow: visible; }
  .cs-chart { min-width: 0; font-size: 8pt; --gc1: 14rem; --gc2: 8rem; }
  .cs-desc { display: none; }            /* room for the chart */
  .client-legend { font-size: 7.5pt; }
  .cs-cell { break-inside: avoid; }

  /* THE RULER ON EVERY SHEET. A CSS grid is not a table, so the browser will
     not repeat a header across printed pages the way a <thead> is repeated:
     the first PDF of this view ran to sixteen sheets and only the first
     carried a month ruler or a column header. The route cuts the rows into
     sheets and emits one of these at the top of each; the page break goes
     here rather than being left to the browser, so the header and the rows
     it labels cannot be separated. */
  .cs-pagehead { display: flex; }
  .cs-pagehead.cs-c1 { break-before: page; }
  .cs-pagehead { break-after: avoid; }
}


@media only screen and (max-width: 850px) {
  .cs-chart { --gc1: 13rem; --gc2: 8rem; }
}

/* Grouped crew: a role that already holds more than one person. Amber rather
   than red -- nothing is wrong and nothing is lost, the view simply cannot
   show both, and the warning is what stops it reading as though one person
   covers the role. */
.crew-role-split-warning {
  /* NOT the 0.75rem the muted hints around it use. At this root size that is
     9.75px -- the smallest text on the form -- and this is the only thing
     between an estimator and quietly replacing somebody. Sized to the
     control it sits under instead. */
  font-size: 0.85rem;
  line-height: 1.3;
  color: #8a5a00;
  background: #fdf2e9;
  border-radius: 0.2rem;
  padding: 0.15rem 0.3rem;
  margin-top: 0.1rem;
}

/* ── Components: crew & duration ──────────────────────────────────────────
   WHAT THIS REPLACES. Every cell, input, chip and slot in this section
   carried its own inline style -- padding and widths repeated on each of
   seven columns, and the crew chips styled from JavaScript with a .css()
   call per chip. Nothing lined up because nothing shared a rule, and a
   column could not be adjusted without editing it in several places.

   The table also sits inside .page-section-table-wrap (overflow-x: auto),
   so anything that makes a column wider than it needs to be pushes the row
   off the side of the card rather than wrapping. The widths below are
   therefore deliberately tight: the numeric fields are sized to the digits
   they hold, not to a round rem figure. */

.component-plan-table { width: 100%; border-collapse: collapse; }

.component-plan-table th {
  text-align: left;
  padding: 0.4rem;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.02rem;
  color: var(--accent);
  border-bottom: 1px solid var(--accent-faded);
  white-space: nowrap;
}
.component-plan-table th:first-child,
.component-plan-table td:first-child { padding-left: 0; }
.component-plan-table th:last-child,
.component-plan-table td:last-child { padding-right: 0; }

.component-plan-table td { padding: 0.35rem 0.4rem; vertical-align: top; }

/* A component and the people on it are ONE thing. The slot row beneath
   carries no top border so the pair reads as a block, and the line under
   the slots closes it. */
.component-plan-row > td { border-top: 1px solid var(--accent-faded); }
.component-plan-table tbody tr:first-child > td { border-top: none; }
.component-slot-row > td { border-top: none; padding: 0 0.4rem 0.6rem 1.2rem; }

.component-plan-name { font-size: 0.9rem; font-weight: 600; }

/* The quoted figures, as one quiet line. Separators come from the rule
   rather than from &middot; entities between every pair, so a missing
   figure cannot leave a dangling dot. */
.component-plan-quoted {
  font-size: 0.75rem;
  color: var(--accent);
  margin-top: 0.1rem;
}
.component-plan-quoted span + span::before {
  content: '·';
  margin: 0 0.35rem;
  opacity: 0.6;
}

/* Sized to the digits. Hours can reach four, crew and days two. */
.component-plan-table input.component-plan-hours { width: 4.5rem; text-align: right; }
.component-plan-table input.component-plan-crew  { width: 3.5rem; text-align: right; }
.component-plan-table input.component-plan-days  { width: 3.75rem; text-align: right; }
.component-plan-table select.component-plan-concurrent { min-width: 9.5rem; }

.component-plan-variance {
  font-size: 0.72rem;
  text-align: right;
  margin-top: 0.15rem;
  min-height: 1em;          /* so a row does not jump when it appears */
}

/* The crew/day shortcuts. A wrapped row of chips, all the same size, laid
   out by gap rather than by a margin on each one -- which is what made the
   old set look ragged when it wrapped. */
.component-plan-options {
  min-width: 11rem;
  max-width: 15rem;
}
.component-plan-chips { display: flex; flex-wrap: wrap; gap: 0.25rem; }

.component-plan-table .component-plan-option {
  padding: 0.1rem 0.45rem;
  border: 1px solid var(--accent-faded);
  border-radius: 1rem;
  font-size: 0.76rem;
  font-weight: 400;
  line-height: 1.5;
  background: transparent;
  white-space: nowrap;
  cursor: pointer;
}
.component-plan-table .component-plan-option:hover {
  border-color: var(--primary);
}
/* The one currently in force. Weight AND fill, because the fill alone is
   too faint to find in a wrapped row of eight. */
.component-plan-table .component-plan-option.is-current {
  font-weight: 700;
  background: var(--primary-faded);
  border-color: var(--primary);
}
.component-plan-options-empty { color: var(--accent); font-size: 0.8rem; }

/* The people, under the component they are on. */
.component-slot-list { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: flex-start; }
.component-slot {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.3rem 0.4rem;
  border: 1px solid var(--accent-faded);
  border-radius: 0.3rem;
}
.component-slot-role { font-size: 0.72rem; color: var(--accent); }
.component-slot-remove { font-size: 0.72rem; }

.component-slot { min-width: 13rem; }
.component-slot-role-name { font-size: 0.82rem; font-weight: 600; }
.component-slot select { min-width: 12rem; font-size: 0.85rem; }
.component-slot-new { border-style: dashed; border-color: var(--accent); }
.component-slot-new input { font-size: 0.8rem; }

/* ── Billing: the component lines under a roll-up ─────────────────────────
   A child is a sub-line of the row above, not a billable row, so it does not
   lay out on the parent's twelve-column grid. It used to: the word
   "component" sat under Type and the actual description in the Description
   column, which is the sixth of twelve on a table that already scrolls
   sideways -- so the thing identifying the line was off the right-hand edge
   and four children read as four near-empty rows. */

.billing-internal-row > td { border: none; }

.billing-internal-cell {
  padding: 0.15rem 0.4rem 0.15rem 1.6rem;   /* indented under its parent */
  font-size: 0.85rem;
  color: var(--accent);
  line-height: 1.5;
}

/* The elbow, drawn rather than set as an icon. At this size a rotated
   arrow glyph reads as a stray mark next to the text. */
.billing-internal-mark {
  display: inline-block;
  width: 0.75rem;
  height: 0.55rem;
  margin-right: 0.4rem;
  border-left: 1px solid var(--accent-faded);
  border-bottom: 1px solid var(--accent-faded);
  vertical-align: 0.15rem;
}

.billing-internal-name { color: var(--text, inherit); }
.billing-internal-plus { color: var(--accent); margin: 0 0.1rem; }

/* The figures after the name, separated by the rule rather than by a
   character between each pair -- so an absent figure cannot leave a
   dangling dot. */
.billing-internal-figures { white-space: nowrap; }
.billing-internal-figures > span::before {
  content: '·';
  margin: 0 0.4rem;
  opacity: 0.6;
}

/* ── Edit-only columns ────────────────────────────────────────────────────
   The job detail page renders this same form inside
   <fieldset class="ro-fieldset" disabled>. The crew shortcuts are an editing
   affordance -- they are populated by script and cannot be clicked when the
   fieldset is disabled -- so on that page the column showed a heading with
   nothing under it, for every row. Same for the remove button.

   Hidden by rule rather than by a flag through the template, because the
   template is included from both pages unchanged and the fieldset is the
   only thing that knows which one it is. */
.ro-fieldset .component-plan-edit-only { display: none; }

/* The quoted figure, shown only where the plan has moved a line off it.
   Italic because it is the thing that is NO LONGER true -- the number beside
   it is. */
.billing-internal-moved { font-style: italic; }

/* What a component is worth, under its name. The figure is the point, so it
   carries the weight; the comparison against the quote is secondary. */
.component-plan-money { margin-top: 0.2rem; font-size: 0.78rem; white-space: nowrap; }
.component-plan-money-now { font-weight: 700; }
.component-plan-money-delta { margin-left: 0.4rem; font-weight: 700; }
.component-plan-money-delta.is-up { color: #1e8449; }
.component-plan-money-delta.is-down { color: #a32218; }
.component-plan-money-was { margin-left: 0.4rem; color: var(--accent); font-style: italic; }
.component-plan-money-locked { color: var(--accent); font-style: italic; }

/* Whether this job's planned hours are a price or a plan. Above the
   components because it governs all of them. */
.component-plan-mode {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: 0.5rem; margin: 0 0 var(--size-5);
}
.component-plan-mode label { font-weight: 700; font-size: 0.85rem; }
/* The mode is stated, not chosen -- the quote decides it. The label keeps the
   weight the <label> had so the row reads the same; the value is plain text
   where a 20rem select used to be. */
.component-plan-mode-label { font-weight: 700; font-size: 0.85rem; }
.component-plan-mode-value { font-size: 0.85rem; }
.component-plan-mode-note { font-size: 0.8rem; color: var(--accent); }

/* The lump-sum figure. Hidden by class rather than removed, so what was
   typed survives a switch away and back -- the fields post in every mode and
   are only acted on in lump_sum. */
.component-plan-lump { display: none; align-items: center; gap: 0.4rem; }
.component-plan-lump.is-shown { display: inline-flex; }
.component-plan-lump input { width: 5rem; text-align: right; }
.component-plan-lump label { font-weight: 700; font-size: 0.85rem; }

/* No dimming in lump-sum mode. The per-component figures are NOT overwritten
   by the job total any more, so they are still live, still edited, and still
   what the schedule is built from -- greying them would say otherwise. */

/* Files sent with a request. A list of links, not a table -- there are
   rarely more than a handful and each one is just a name and a size. */
.job-attachment-list { list-style: none; margin: 0; padding: 0; }
.job-attachment-list li {
  display: flex; align-items: baseline; gap: 0.5rem;
  padding: 0.3rem 0; border-bottom: 1px solid var(--accent-faded);
}
.job-attachment-list li:last-child { border-bottom: none; }
.job-attachment-size { color: var(--accent); font-size: 0.8rem; }

/* The remove button sits at the far end of its row, away from the link, so a
   click meant for the document cannot land on the delete. margin-left:auto
   rather than a float -- the row is a flex container. */
.job-attachment-remove { margin-left: auto; display: inline; }
.btn-link-danger {
  background: none; border: none; cursor: pointer; padding: 0 0.25rem;
  color: var(--accent); font-size: 0.85rem; line-height: 1;
}
.btn-link-danger:hover { color: var(--danger, #c0392b); }

/* Adding a document: a link row and a file row, each collapsing to one column
   on a narrow screen. A link is listed first because it is the one to prefer
   -- the proposal already exists in Quoting and a copy here goes stale. */
.job-attachment-add { margin-top: var(--size-5); }
.job-attachment-add-row {
  display: flex; flex-wrap: wrap; gap: var(--size-4);
  margin-bottom: var(--size-4); align-items: center;
}
.job-attachment-add-row .text-input { flex: 1 1 14rem; min-width: 0; }
.job-attachment-add-row button { flex: 0 0 auto; }

/* ── The sidebar badge sits IN its row ──────────────────────────────────
   REPORTED: "the 3 is fixed position whereas it should be attached to the
   icon, its in place as I scroll and not moving with the requests total".

   `.badge` in css/new-style.css is `position: absolute` with NO top/left --
   written for `.header-dialog-button .badge`, which supplies its own offsets
   through margins. Dropped into the sidebar it has no positioned ancestor to
   resolve against (the nav anchor is `display:flex`, not positioned), so it
   climbs to the nearest one and parks there, staying put while the nav list
   scrolls underneath it.

   Static, not relative-plus-absolute: the anchor is already a flex row, so the
   badge only needs to be a flex child of it. `margin-left:auto` pushes it to
   the trailing edge. It then moves with its row because it IS in its row.

   Applies to every sidebar badge, so Data Conflicts is fixed by the same
   rule -- it had the identical bug and nobody had reported it. */
.sidebar-nav-item .badge {
  position: static;
  margin-left: auto;
  min-width: 1.4rem;
  min-height: 1.4rem;
  line-height: 1.4rem;
  font-size: 0.8rem;
  flex: 0 0 auto;
}
/* The label takes the slack instead of the badge, so badges on different rows
   line up with each other rather than each sitting against its own text. */
.sidebar-nav-item .sidebar-label { flex: 1 1 auto; }
/* Collapsed, there is no room for a number beside a bare icon; the dot still
   says "something is waiting". */
.sidebar-new.collapsed .sidebar-nav-item .badge {
  min-width: 0.6rem; min-height: 0.6rem; line-height: 0;
  font-size: 0; padding: 0; border-radius: 50%;
  margin-left: 0.2rem;
}

/* ── The request queue ──────────────────────────────────────────────────
   Its own screen, not the job board filtered. See templates/jobs/requests.html
   for why. */
.request-table { width: 100%; border-collapse: collapse; }
.request-table th {
  text-align: left;
  padding: 0.5rem var(--size-5);
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--accent);
  border-bottom: 1px solid #ddd8ea;
  white-space: nowrap;
}
.request-table td {
  padding: 0.75rem var(--size-5);
  border-bottom: 1px solid #eeebf5;
  vertical-align: top;
}
.request-row:hover { background: #faf9fd; }

/* Unread carries a left rule in the same light purple a requested job already
   wears on the board, so the two screens agree about what the colour means. */
.request-unread { background: #fbfaff; }
.request-unread td:first-child { box-shadow: inset 3px 0 0 #8b7fc7; }

.request-name { font-weight: bold; text-decoration: none; }
.request-scope {
  margin-top: 0.2rem;
  font-size: 0.85rem;
  color: #555;
  max-width: 44rem;
}
.request-meta { margin-top: 0.25rem; font-size: 0.8rem; color: var(--accent); }
.request-meta-sep { margin: 0 0.35rem; }
.request-when { white-space: nowrap; }
.request-when-time { font-size: 0.8rem; color: var(--accent); }
.request-priority {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: 0.8rem;
  background: #efecf9;
  font-size: 0.8rem;
  white-space: nowrap;
}
.request-due { margin-top: 0.2rem; font-size: 0.8rem; color: var(--secondary); }
.request-read-cell { white-space: nowrap; font-size: 0.85rem; }
.request-unread-dot {
  display: inline-block;
  width: 0.6rem; height: 0.6rem;
  border-radius: 50%;
  background: #8b7fc7;
}
.request-actions { text-align: right; white-space: nowrap; }
.request-count-pill {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 0 0.4rem;
  border-radius: 0.8rem;
  background: rgba(0,0,0,0.12);
  font-size: 0.8rem;
}
.request-empty { padding: var(--size-6); color: var(--accent); }

@media (max-width: 70rem) {
  /* Asked-by and the read stamp are the first things to go: on a narrow screen
     the queue still has to answer "what came in and is it urgent". */
  .request-table th:nth-child(4), .request-table td:nth-child(4),
  .request-table th:nth-child(6), .request-table td:nth-child(6) { display: none; }
  .request-scope { max-width: none; }
}

/* The Requests badge is always in the DOM so script can bring it back from
   zero; this is what "zero" looks like. display:none rather than removing the
   element, for exactly the reason the element is no longer removed. */
.sidebar-nav-item .badge.badge-empty { display: none; }

/* ── Report a Bug dialog: centre it ─────────────────────────────────────────
   REPORTED as "report a bug pops up in the wrong spot" -- the panel's left
   edge sat off screen, so the title read "t a Bug" and the button read "mit".

   new-style.css positions it `position: fixed` with `margin: auto auto 0.5rem`
   and no left or right. On a fixed box with both offsets auto, `margin: auto`
   resolves to zero and the box stays at its STATIC position, which is x=0 --
   underneath the fixed sidebar. The auto margins read like centring and do
   nothing at all.

   Pinning both offsets gives the auto margins something to divide, so the
   panel centres on the viewport. `top` is deliberately untouched: with top
   auto a fixed box keeps its static vertical position, which is what the
   overlay's padding-top sets, so the dialog still hangs below the header
   rather than jumping to the top of the screen.

   HERE AND NOT IN new-style.css, which is the vendored design system copied
   into every repo -- a fix there is one re-sync away from reverting. This
   sheet is loaded after it, so the same selector wins on order. The same bug
   is presumably live in the sibling apps that share that file. */
.header-dialog div.header-dialog-content {
  left: 0;
  right: 0;
  margin-left: auto;
  margin-right: auto;
  max-width: calc(100vw - 2rem);
  /* A MOBILE RULE IN THE DESIGN SYSTEM NUDGES THIS SIDEWAYS. Below 600px it
     sets `top: 12rem; width: 80%; transform: translateX(10%)` -- the top and
     width are wanted, the nudge is not: it shifts the panel by a tenth of its
     own width and fights the auto margins above. Cancelled here rather than
     edited there, and it wins on SPECIFICITY (0,2,1 against that rule's
     0,1,0) because a media query contributes none of its own. */
  transform: none;
}

/* ABOVE THE SIDEBAR. The overlay shipped at z-index 3 while .sidebar-new is
   10, which is invisible at desktop widths -- the sidebar is only 5.66rem
   wide and the dialog sits in the middle. At 850px and below the sidebar
   becomes a full-width block (see the responsive section) and swallowed the
   dialog whole. 50 is the z-index this app already gives .modal-overlay, so
   the bug dialog now stacks like every other modal in it. */
.header-dialog {
  z-index: 50;
}
