/* ==========================================================================
   Roadframe — "Yard Tag" design system
   ---------------------------------------------------------------------
   An internal ops tool, not a marketing page: dense tables, forms, and
   status at a glance win over whitespace. The identity is drawn from the
   fleet yard itself — asphalt + safety amber, condensed industrial type,
   and status rendered like a tag clipped to a truck's key rather than a
   generic UI pill. Built as an override layer on top of Bootstrap 5.3
   (loaded from the CDN in base.html) — no build step, so overrides use
   Bootstrap's own CSS custom properties where possible instead of forking
   its source.
   ========================================================================== */

:root {
  /* ---- Palette --------------------------------------------------------- */
  --ink:        #1B1F23;   /* asphalt — nav, headings, primary text */
  --ink-soft:   #4B525A;   /* secondary text */
  --paper:      #EEF0F0;   /* page background — steel-tinted, not stark white */
  --panel:      #FFFFFF;   /* card / table surface */
  --line:       #DBDFE1;   /* hairline borders */
  --line-soft:  #E8EBEC;

  --amber:      #D98A0F;   /* primary accent — caution-flag amber */
  --amber-deep: #B4720A;
  --amber-tint: #FBEEDA;

  --route:      #24587F;   /* secondary accent / links — highway-sign blue */
  --route-deep: #1B4360;
  --route-tint: #E4ECF1;

  --go:         #2E7D4E;   /* active / paid / success */
  --go-tint:    #E3F1E8;
  --stop:       #B23A2E;   /* down / unpaid / danger */
  --stop-tint:  #F8E7E4;
  --idle:       #6B7280;   /* retired / neutral */
  --idle-tint:  #EAEBEC;

  /* Component identity (modular build). Used for identity only — slot
     borders, tags, meter fills — never for buttons or alerts, which stay
     amber/route/go/stop so the action language doesn't fragment. */
  --chassis:    #6B4E8C;
  --body:       #1F6F72;
  --reefer:     #24587F;   /* aliases --route deliberately */

  /* ---- Type -------------------------------------------------------------*/
  --font-display: "Big Shoulders Display", "Arial Narrow", sans-serif;
  --font-body: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  --radius: 5px;
  --shadow-sm: 0 1px 2px rgba(27, 31, 35, .07);
  --shadow-md: 0 6px 20px rgba(27, 31, 35, .10);

  /* Re-point Bootstrap's own contextual variables at our palette so every
     utility class built on them (bg-success, text-danger, border-warning,
     .badge bg-*, etc.) re-themes without touching template markup. */
  --bs-success-rgb: 46, 125, 78;
  --bs-danger-rgb: 178, 58, 46;
  --bs-warning-rgb: 217, 138, 15;
  --bs-secondary-rgb: 107, 114, 128;
  --bs-dark-rgb: 27, 31, 35;
  --bs-primary-rgb: 36, 88, 127;
  --bs-body-font-family: var(--font-body);
  --bs-link-color: var(--route);
  --bs-link-hover-color: var(--route-deep);
  --bs-border-color: var(--line);
}

/* ---- Base ---------------------------------------------------------------*/

body {
  background-color: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: .95rem;
}

.app-skip-link {
  position: fixed;
  top: .5rem;
  left: .75rem;
  z-index: 1100;
  padding: .5rem .75rem;
  border: 2px solid var(--ink);
  border-radius: var(--radius);
  background: var(--amber);
  color: var(--ink);
  font-weight: 700;
  box-shadow: var(--shadow-sm);
}

.app-skip-link:focus {
  color: var(--ink);
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: .01em;
  color: var(--ink);
}

.h3 { font-size: 1.7rem; text-transform: uppercase; letter-spacing: .03em; }

a { color: var(--route); text-decoration-color: rgba(36, 88, 127, .35); }
a:hover, a:focus { color: var(--route-deep); }

code {
  font-family: var(--font-mono);
  color: var(--route-deep);
  background: var(--route-tint);
  padding: .1em .35em;
  border-radius: 3px;
  font-size: .85em;
}

::selection { background: var(--amber-tint); color: var(--ink); }

:focus-visible {
  outline: 2px solid var(--amber);
  outline-offset: 2px;
}

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

/* Modular build wizard --------------------------------------------------- */
.build-steps ol {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: .5rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
.build-steps a,
.build-steps li > span,
.build-steps button {
  align-items: center;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--ink-soft);
  display: flex;
  font-size: .78rem;
  font-weight: 700;
  gap: .45rem;
  min-height: 42px;
  padding: .45rem .55rem;
  text-decoration: none;
  width: 100%;
}
.build-steps button { cursor: not-allowed; opacity: .55; }
.build-steps .n {
  align-items: center;
  background: var(--idle-tint);
  border-radius: 50%;
  display: inline-flex;
  flex: 0 0 24px;
  height: 24px;
  justify-content: center;
}
.build-steps .is-complete .n { background: var(--go); color: #fff; }
.build-steps .is-current > span { background: var(--ink); color: #fff; }
.build-steps .is-current .n { background: var(--amber); color: var(--ink); }

.pick-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: .75rem;
}
.pick-card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  gap: .35rem;
  min-height: 138px;
  padding: .85rem .9rem;
  position: relative;
  transition: border-color 120ms ease, box-shadow 120ms ease;
}
.pick-card:hover { border-color: var(--amber); box-shadow: 0 2px 8px rgba(27,31,35,.08); }
.pick-card:has(input:focus-visible) { outline: 2px solid var(--amber); outline-offset: 2px; }
.pick-card:has(input:checked) {
  border: 2px solid var(--amber);
  box-shadow: 0 0 0 3px rgba(217,138,15,.15);
  padding: calc(.85rem - 1px) calc(.9rem - 1px);
}
.pick-card.pick-chassis { border-top: 4px solid var(--chassis); }
.pick-card.pick-body { border-top: 4px solid var(--body); }
.pick-card.pick-reefer { border-top: 4px solid var(--reefer); }
.pick-card .truck-no { font-size: 1.45rem; }
.pick-card .c-serial { font-size: 1rem; }
.pick-meta { color: var(--ink-soft); font-size: .78rem; }
.pick-radio {
  border: 2px solid var(--idle);
  border-radius: 50%;
  height: 17px;
  position: absolute;
  right: .8rem;
  top: .8rem;
  width: 17px;
}
.pick-card:has(input:checked) .pick-radio { border: 5px solid var(--amber); }
.registration-panel { display: none; }
.registration-panel.is-open { display: block; }
.c-serial-input { border: 2px solid var(--amber); font-family: var(--font-mono); }
.truck-no-input { font-family: var(--font-display); font-size: 1.15rem; font-weight: 700; }
.build-summary dt { color: var(--ink-soft); font-size: .75rem; text-transform: uppercase; }
.component-disposition {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: .9rem;
}
.component-disposition legend { float: none; margin-bottom: .75rem; width: auto; }

@media (max-width: 991px) {
  .build-steps ol { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 575.98px) {
  .build-steps ol { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pick-grid { grid-template-columns: 1fr; }
}

/* ---- Navbar ---------------------------------------------------------- */

.navbar {
  background: var(--ink) !important;
  border-bottom: 3px solid var(--amber);
  padding-top: .55rem;
  padding-bottom: .55rem;
  box-shadow: var(--shadow-sm);
  position: relative;
  z-index: 1030;
}

.navbar-brand {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.55rem;
  text-transform: uppercase;
  letter-spacing: .02em;
  color: #fff !important;
  display: flex;
  align-items: center;
  gap: .4rem;
}

.navbar .nav-link {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: .8rem;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: rgba(255, 255, 255, .68) !important;
  padding: .55rem .85rem !important;
  border-bottom: 2px solid transparent;
  transition: color .15s ease, border-color .15s ease;
}

.navbar .nav-link:hover,
.navbar .nav-link:focus-visible {
  color: #fff !important;
  border-bottom-color: rgba(217, 138, 15, .55);
}

.navbar .nav-link.is-active {
  color: #fff !important;
  border-bottom-color: var(--amber);
}

.navbar-toggler {
  border-color: rgba(255, 255, 255, .25);
}
.navbar-toggler:focus { box-shadow: 0 0 0 .2rem rgba(217, 138, 15, .4); }

.navbar-brand-word { line-height: 1; }

/* ---- Footer ------------------------------------------------------------ */

.app-footer {
  margin-top: 2.5rem;
  padding: .9rem 0 1.6rem;
  border-top: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: .74rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
}

.app-footer-sep {
  color: var(--line);
  margin: 0 .3em;
}

/* ---- Buttons ----------------------------------------------------------- */

.btn {
  font-weight: 600;
  font-size: .85rem;
  border-radius: var(--radius);
  letter-spacing: .01em;
}

.btn-primary {
  --bs-btn-color: var(--ink);
  --bs-btn-bg: var(--amber);
  --bs-btn-border-color: var(--amber);
  --bs-btn-hover-color: var(--ink);
  --bs-btn-hover-bg: #C67F0C;
  --bs-btn-hover-border-color: #C67F0C;
  --bs-btn-active-color: var(--ink);
  --bs-btn-active-bg: var(--amber-deep);
  --bs-btn-active-border-color: var(--amber-deep);
  --bs-btn-focus-shadow-rgb: 217, 138, 15;
  --bs-btn-disabled-bg: var(--amber);
  --bs-btn-disabled-border-color: var(--amber);
}

.btn-success {
  --bs-btn-bg: var(--go);
  --bs-btn-border-color: var(--go);
  --bs-btn-hover-bg: #256841;
  --bs-btn-hover-border-color: #256841;
  --bs-btn-active-bg: #235c3a;
  --bs-btn-active-border-color: #235c3a;
  --bs-btn-focus-shadow-rgb: 46, 125, 78;
}

.btn-outline-primary {
  --bs-btn-color: var(--route);
  --bs-btn-border-color: var(--route);
  --bs-btn-hover-bg: var(--route);
  --bs-btn-hover-border-color: var(--route);
  --bs-btn-active-bg: var(--route-deep);
  --bs-btn-active-border-color: var(--route-deep);
  --bs-btn-focus-shadow-rgb: 36, 88, 127;
}

.btn-outline-secondary {
  --bs-btn-color: var(--ink-soft);
  --bs-btn-border-color: var(--line);
  --bs-btn-hover-color: #fff;
  --bs-btn-hover-bg: var(--ink-soft);
  --bs-btn-hover-border-color: var(--ink-soft);
  --bs-btn-active-bg: var(--ink);
  --bs-btn-active-border-color: var(--ink);
  --bs-btn-focus-shadow-rgb: 75, 82, 90;
}

.btn-outline-danger {
  --bs-btn-color: var(--stop);
  --bs-btn-border-color: var(--stop);
  --bs-btn-hover-bg: var(--stop);
  --bs-btn-hover-border-color: var(--stop);
  --bs-btn-active-bg: #8f2f25;
  --bs-btn-active-border-color: #8f2f25;
  --bs-btn-focus-shadow-rgb: 178, 58, 46;
}

.btn-link {
  --bs-btn-color: var(--route);
  --bs-btn-hover-color: var(--route-deep);
  font-weight: 600;
  text-decoration: none;
}
.btn-link:hover { text-decoration: underline; }
.btn-link.text-danger { --bs-btn-color: var(--stop); --bs-btn-hover-color: #8f2f25; }
.btn-link.text-secondary { --bs-btn-color: var(--ink-soft); --bs-btn-hover-color: var(--ink); }

/* ---- Badges & status tags ------------------------------------------------
   The signature element: status reads like a tag clipped to a key, not a
   generic pill — rectangular, uppercase, tight tracking, printed-edge feel.
   Color comes from the root --bs-*-rgb overrides above; this block only
   changes shape/type. */

.badge {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: .68rem;
  text-transform: uppercase;
  letter-spacing: .05em;
  border-radius: 3px;
  padding: .32em .55em;
  vertical-align: middle;
}

.status-badge-active,
.status-badge-down,
.status-badge-retired {
  border-radius: 3px;
  padding: .32em .6em;
  font-weight: 700;
  font-size: .68rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  border: 1px solid rgba(0, 0, 0, .12);
}

.status-badge-active  { background-color: var(--go);   color: #fff; }
.status-badge-down    { background-color: var(--stop); color: #fff; }
.status-badge-retired { background-color: var(--idle); color: #fff; }

/* Preventative maintenance, in the same tag language as vehicle status.
   "unknown" gets the amber caution treatment rather than a neutral grey: a
   schedule nobody has configured is a gap to close, not a quiet default, and
   greying it out is how it stays unnoticed. */
.pm-badge-ok,
.pm-badge-due_soon,
.pm-badge-overdue,
.pm-badge-unknown,
.pm-badge-not_applicable {
  border-radius: 3px;
  padding: .32em .6em;
  font-weight: 700;
  font-size: .68rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  border: 1px solid rgba(0, 0, 0, .12);
}

.pm-badge-ok             { background-color: var(--go);        color: #fff; }
.pm-badge-due_soon       { background-color: var(--amber);     color: var(--ink); }
.pm-badge-overdue        { background-color: var(--stop);      color: #fff; }
.pm-badge-unknown        { background-color: var(--amber-tint); color: var(--amber-deep); }
.pm-badge-not_applicable { background-color: var(--idle-tint); color: var(--ink-soft); }

/* Warranty, in the same tag language again, but deliberately quieter: it is
   an outline rather than a fill, and a size down. Most of these read "active"
   most of the time, and a wall of solid green competing with the PM badges
   would train people to stop seeing all of them. The colour still carries the
   state, and the text beside it always says the same thing in words -- nobody
   should have to distinguish this green from this amber to know where they
   stand. "unknown" stays amber for the reason given above. */
.warranty-badge-active,
.warranty-badge-expiring_soon,
.warranty-badge-expired,
.warranty-badge-unknown,
.warranty-badge-not_applicable {
  border-radius: 3px;
  padding: .2em .45em;
  font-weight: 600;
  font-size: .62rem;
  text-transform: uppercase;
  letter-spacing: .05em;
  background-color: transparent;
  border: 1px solid currentColor;
}

.warranty-badge-active         { color: var(--go); }
.warranty-badge-expiring_soon  { color: var(--amber-deep); }
.warranty-badge-expired        { color: var(--stop); }
.warranty-badge-unknown        { color: var(--amber-deep); background-color: var(--amber-tint); }
.warranty-badge-not_applicable { color: var(--ink-soft); border-color: var(--line); }

/* ---- Cards --------------------------------------------------------------*/

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

.card-header {
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: .78rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-soft);
  padding: .6rem .9rem;
}

.card-stat .display-6 {
  font-family: var(--font-display);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.alert {
  border-radius: var(--radius);
  border-width: 1px;
  font-size: .9rem;
}
.alert-info {
  background: var(--route-tint);
  border-color: rgba(36, 88, 127, .25);
  color: var(--route-deep);
}
.alert-danger { background: var(--stop-tint); border-color: rgba(178, 58, 46, .3); color: #7a281f; }
.alert-success { background: var(--go-tint); border-color: rgba(46, 125, 78, .3); color: #1f5735; }
.alert-warning { background: var(--amber-tint); border-color: rgba(217, 138, 15, .3); color: #7a5308; }

/* ---- Tables ---------------------------------------------------------- */

.table-responsive {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  isolation: isolate;
}

/* The vehicle overview is wider than a phone viewport. Make horizontal
   touch panning explicit and keep iOS momentum scrolling enabled. */
.vehicle-table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-x pan-y;
}

.table-responsive[role="region"]:focus-visible {
  outline: 3px solid rgba(217, 138, 15, .4);
  outline-offset: 2px;
}

.mobile-scroll-cue {
  display: none;
}

.table {
  margin-bottom: 0;
  font-variant-numeric: tabular-nums;
  --bs-table-hover-bg: var(--amber-tint);
}

.table > thead {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-soft);
}

.table > thead > tr > th {
  border-bottom: 2px solid var(--line);
  background: var(--paper);
}

.table > :not(caption) > * > * {
  border-bottom-color: var(--line-soft);
}

/* .table-responsive's overflow-x:auto only ever kicks in if a cell is
   willing to overflow instead of wrapping -- by default a free-text column
   (invoice/rental "description"/"reason", a document's "notes", etc.) wraps
   onto as many lines as it takes to fit the available width instead,
   producing rows that grow to match a single long entry (confirmed: a
   364-character description produced a 906px-tall row) rather than the
   table just scrolling sideways like the rest of its columns already do.
   Every table in the app sits inside a .table-responsive wrapper, so this
   is safe to apply generally rather than patching each affected column. */
.table-responsive .table > :not(caption) > * > * {
  white-space: nowrap;
}

/* nowrap above stops a long free-text cell from inflating its own row, but
   left uncapped that column's width is still shared across every row in the
   table (plain HTML table layout), so one long description drags an
   otherwise-short-entry table thousands of pixels wide -- confirmed: a
   single 364-character description forced a 2635px-wide column onto every
   row. Applied to whichever cell holds a free-text field (description,
   reason, notes); the full value remains one tap away since these rows
   already link through to the edit/detail page. */
.cell-truncate {
  max-width: 260px;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Full-row-click table rows. The real link in the identifying cell remains
   keyboard and screen-reader accessible; base.html delegates pointer clicks
   from the rest of the row to it. Bootstrap's stretched-link pseudo-element
   is deliberately disabled here because absolutely positioned descendants
   of <tr> are not contained reliably by mobile browsers after scrolling and
   can otherwise sit invisibly over the navbar. */
.table tbody tr.row-link {
  cursor: pointer;
}

.table tbody tr.row-link .stretched-link::after {
  content: none;
}

.table tbody tr.row-link:hover {
  box-shadow: inset 3px 0 0 var(--amber);
}

/* Any other link/button/form in a row-link row (e.g. "+ Invoice", "View",
   "Excluded" toggle) needs to sit above the stretched-link's full-row
   overlay to stay independently clickable -- the standard caveat from
   Bootstrap's own stretched-link docs, applied to table rows instead of
   cards. */
.table tbody tr.row-link a:not(.stretched-link),
.table tbody tr.row-link button,
.table tbody tr.row-link form {
  position: relative;
  z-index: 2;
}

.table tfoot td,
.table .table-light {
  background: var(--paper) !important;
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: .02em;
}

.expiring-soon {
  color: var(--stop);
  font-weight: 700;
}

/* ---- Invoice attachment preview ----------------------------------------
   PDF via the browser's own built-in viewer in an iframe; images directly.
   Both load through download_upload(view=True) -- see that route and
   auth.py's download_url() for why an inline (not forced-download)
   response is a separate opt-in rather than the default. */

/* The preview grows to whatever height the details card beside it sets, so
   the two columns end level instead of the viewer picking its own size. The
   min-height keeps a page of the PDF legible when the details card is short. */
.invoice-attachment-body {
  display: flex;
  flex-direction: column;
}

.invoice-attachment-preview {
  width: 100%;
  flex: 1 1 auto;
  min-height: 420px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.invoice-attachment-frame {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
  justify-content: center;
}

.invoice-attachment-preview-img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

/* Create/edit uses the available browser window as a workbench: fields stay
   at a comfortable reading width while the paper remains visible beside
   them. The split editor then gets the full card width below instead of
   inheriting the PDF column's scale. */
.invoice-form {
  width: 100%;
  max-width: 1600px;
}

.invoice-form-workspace {
  display: grid;
  grid-template-columns: minmax(560px, 1fr) minmax(420px, .82fr);
  gap: 1.25rem;
  align-items: start;
}

/* A grid item defaults to min-width:auto, so it refuses to shrink below its
   widest content. The split editor puts #lineTable (min-width 1080px) in this
   column, which without this blows the column past its track and scrolls the
   whole page sideways -- taking the sticky viewer off-screen -- instead of
   scrolling inside the table's own .table-responsive wrapper. */
.invoice-form-fields {
  min-width: 0;
}

.invoice-form-preview-column {
  min-width: 0;
  position: sticky;
  top: 1rem;
}

.invoice-form-preview-column .invoice-attachment-preview {
  height: clamp(520px, calc(100vh - 230px), 800px);
  min-height: 0;
  flex: 0 0 auto;
}

/* The seven sized columns below come to 1052px. min-width has to clear that
   by enough to leave the one auto column -- Description, the widest thing
   anybody types -- something readable, because table-layout:fixed takes its
   widths from the colgroup and ignores the min-width on the <th>. At 1080px
   Description got the 28px that happened to be left over and rendered as a
   sliver with its header spilling across Vehicle's, which is what it looked
   like on a phone. 1340 = 1052 + 18rem for Description. */
#lineTable {
  min-width: 1340px;
  table-layout: fixed;
}

#lineTable .line-col-reference { width: 10rem; }
#lineTable .line-col-description { width: auto; }
#lineTable .line-col-vehicle { width: 12rem; }
#lineTable .line-col-shared { width: 5.5rem; }
#lineTable .line-col-owner { width: 12rem; }
#lineTable .line-col-work { width: 14rem; }
#lineTable .line-col-amount { width: 9rem; }
#lineTable .line-col-actions { width: 3.25rem; }

@media (max-width: 1199.98px) {
  .invoice-form-workspace {
    grid-template-columns: minmax(0, 1fr);
  }

  .invoice-form-preview-column {
    position: static;
  }

  .invoice-form-preview-column .invoice-attachment-preview {
    height: min(65vh, 680px);
  }
}

/* Stacked on phones there is no neighbour to match, so cap the height instead
   of letting a tall PDF push the rest of the page off-screen. */
@media (max-width: 767.98px) {
  .invoice-attachment-preview {
    height: 60vh;
    min-height: 0;
    flex: 0 0 auto;
  }

  .invoice-attachment-preview-img {
    max-height: 60vh;
  }
}

/* ---- Forms ------------------------------------------------------------- */

.form-label {
  font-weight: 600;
  font-size: .82rem;
  color: var(--ink-soft);
}

.form-control, .form-select {
  border-color: var(--line);
  border-radius: var(--radius);
  font-size: .9rem;
}

.form-control:focus, .form-select:focus {
  border-color: var(--amber);
  box-shadow: 0 0 0 .2rem rgba(217, 138, 15, .22);
}

.form-control.is-invalid,
.form-select.is-invalid {
  border-color: var(--stop);
}

.form-control.is-invalid:focus,
.form-select.is-invalid:focus {
  border-color: var(--stop);
  box-shadow: 0 0 0 .2rem rgba(217, 138, 15, .22);
}

.form-check-input:checked {
  background-color: var(--route);
  border-color: var(--route);
}
.form-check-input:focus {
  border-color: var(--amber);
  box-shadow: 0 0 0 .2rem rgba(217, 138, 15, .22);
}

.form-text { color: var(--ink-soft); font-size: .8rem; }

form.card {
  box-shadow: var(--shadow-md);
}

/* ---- Modals -------------------------------------------------------------*/

.modal-content {
  border-radius: var(--radius);
  border: 1px solid var(--line);
}

.modal-header {
  background: var(--paper);
  border-bottom: 1px solid var(--line);
}

.modal-title {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .02em;
  font-size: 1.15rem;
}

/* ---- Dashboard ----------------------------------------------------------*/

.dashboard-page .navbar {
  margin-bottom: .75rem !important;
}

.dashboard-summary {
  display: grid;
  /* Let the summary choose fewer tracks when enlarged text makes a value
     wider than a six-column tile. At the default size 10rem keeps the
     established six-up desktop strip; the phone override below keeps the
     established two-up layout. With larger text the same intrinsic minimum
     lets the cards reflow before their decision-relevant values are clipped. */
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: .75rem;
}

.dashboard-summary .card {
  border-left: 4px solid var(--line);
  overflow: hidden;
}

.dashboard-summary-item:nth-child(1) .card { border-left-color: var(--go); }
.dashboard-summary-item:nth-child(2) .card { border-left-color: var(--stop); }
.dashboard-summary-item:nth-child(3) .card { border-left-color: var(--amber); }
.dashboard-summary-item:nth-child(4) .card,
.dashboard-summary-item:nth-child(5) .card,
.dashboard-summary-item:nth-child(6) .card { border-left-color: var(--route); }

.dashboard-summary .card-body {
  padding: .65rem .5rem;
}

.dashboard-summary .display-6 {
  font-size: 1.5rem;
  line-height: 1.1;
  font-family: var(--font-display);
}

/* #299: phone .display-6 { white-space: nowrap } plus card overflow:hidden
   clipped the month-over-month % past the Spend This Month card edge. Keep
   amount and delta as unbroken tokens, but let the pair wrap inside the card. */
.dashboard-summary .spend-month-figure {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: .35rem;
  row-gap: .1rem;
  min-width: 0;
  white-space: normal;
}
.dashboard-summary .spend-month-amount,
.dashboard-summary .spend-month-delta {
  white-space: nowrap;
}
.dashboard-summary .spend-month-delta {
  margin-left: 0;
  line-height: 1.2;
}

/* Tile label ("Active", "Spend This Month") — always paired with .small in
   the markup. The longer explanatory captions some tiles carry underneath
   (e.g. "Includes uninvoiced rental base minimums") reuse .text-muted
   without .small, so they're deliberately excluded from the tracked-caps
   treatment below or they'd wrap into dense, hard-to-scan blocks. */
.dashboard-summary .text-muted.small {
  line-height: 1.15;
  font-weight: 600;
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .05em;
}

.dashboard-summary .card-body > .text-muted:not(.small) {
  font-weight: 400;
  line-height: 1.3;
  text-transform: none;
  letter-spacing: normal;
  margin-top: .15rem;
}

.dashboard-summary-link {
  color: inherit;
  display: block;
  height: 100%;
  text-decoration: none;
}

.dashboard-summary-link .card {
  transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease;
}

.dashboard-summary-link:hover .card,
.dashboard-summary-link:focus-visible .card {
  box-shadow: var(--shadow-md);
  transform: translateY(-1px);
}

.dashboard-panel .card-header {
  padding: .5rem .8rem;
}

.dashboard-panel .table > :not(caption) > * > * {
  padding: .3rem .55rem;
}

.dashboard-panel .table {
  font-size: .85rem;
}

/* Fill the viewport, no page scroll: desktop only (min-width matches the
   col-lg-6 breakpoint the two panel rows already use). Below that, the six
   stat tiles alone can run tall enough that cramming both panel rows into
   whatever's left would leave them unusably short, so mobile/tablet keeps
   the normal stacked, page-scrolls layout instead. The two table panels
   split the remaining height evenly and scroll internally
   (.dashboard-panel .table-responsive) if their own content overflows,
   rather than growing the page. */
@media (min-width: 992px) {
  body.dashboard-page {
    height: 100vh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }

  body.dashboard-page .navbar {
    flex: 0 0 auto;
  }

  /* A measure, not a container query. The dashboard is prose and tables --
     summary labels, panel copy, invoice and vehicle rows -- and those stop
     being easier to read somewhere well before 3440px. _dashboard_summary
     already caps its own paragraph at 58rem for this reason; this is the
     same decision applied to the page instead of to one element, so the
     hero, the summary strip and the panels share one measure rather than
     each finding its own. Below 2240px it does nothing.
     Live Status is deliberately NOT capped: there the width is the payload. */
  body.dashboard-page > .container-fluid.px-4 {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    padding-bottom: .5rem;
    overflow: hidden;
    max-width: 140rem;
    margin-inline: auto;
  }

  body.dashboard-page .dashboard-summary {
    flex: 0 0 auto;
  }

  /* Bootstrap's .row (flex + flex-wrap:wrap) doesn't reliably stretch a
     column to the row's own height once the column's content wants to be
     taller than that — confirmed directly (col height tracked its content,
     not the row, until content was pinned to an explicit pixel height).
     Grid's stretch doesn't have that failure mode, so the two panels use
     grid here instead of Bootstrap's column classes; gutter margin/padding
     that .row/.col-lg-6 still apply are zeroed in favor of grid's own gap
     so they don't stack on top of it. */
  body.dashboard-page .dashboard-panels {
    display: grid;
    /* auto-fit rather than a fixed two: the number of panels is not fixed and
       neither is the window. A hard `repeat(2, ...)` means an odd panel takes
       one cell and leaves the other visibly empty -- which is exactly what a
       third panel did to this row, reading as a gap under its neighbour rather
       than as a panel that had wrapped. Fitting as many ~26rem tracks as the
       width allows lets a row hold three on a wide screen, two on a laptop and
       one on a phone, and a lone panel stretch the full width, with no
       breakpoint to keep in step with the panel count.
       min(100%, 26rem) keeps the track from overflowing a viewport narrower
       than the minimum itself.

       The 30% floor is the ceiling on that: auto-fit will keep adding tracks
       for as long as the width allows, so on an ultrawide the six panels all
       land in one row and the page gets SHORTER as the screen gets wider --
       measured at 610px of content under a 1440px-tall viewport at 3440px,
       against 790px at 1920px. A track that must be at least 30% of the row
       can never be the fourth, whatever the width, so extra width now goes
       into panel width and a second row instead of into more columns.
       max() rather than a breakpoint because the floor has to track the
       container, not the viewport: this grid is also the narrow layout. */
    grid-template-columns: repeat(auto-fit, minmax(min(100%, max(26rem, 30%)), 1fr));
    /* Cards size to their own content rather than to the tallest card in the
       row. A compact summary is deliberately ~109px against a populated
       panel's ~261px; stretching it to match left 152px of empty card under
       four of six panels at 3440px. */
    align-items: start;
    align-content: start;
    gap: 1rem;
    margin-left: 0;
    margin-right: 0;
    flex: 1 1 0;
    min-height: 0;
  }

  /* The column is also made a flex container (rather than relying on the
     card's .h-100 percentage height) for the same reason: a percentage
     height on the card doesn't reliably resolve against a grid-stretched
     column once the card's own content is tall, so the card fills it via
     flex:1 below instead. */
  /* Both .col-12 panels here are exception-only -- "Needs attention" and
     "Reported engine diagnostics" render at all only when something is
     wrong. The template says full width and the narrow layout gives them
     full width; grid was quietly ignoring the class and seating them in one
     track beside the routine panels, so an exception looked like a peer of
     "Recent Invoices". Grid needs to be told in its own terms. */
  body.dashboard-page .dashboard-panels > .col-12 {
    grid-column: 1 / -1;
  }

  body.dashboard-page .dashboard-panels > [class*="col-"] {
    display: flex;
    flex-direction: column;
    min-height: 0;
    padding-left: 0;
    padding-right: 0;
    /* col-lg-6's own width:50%/max-width:50% (left over from Bootstrap's
       percentage-width flex column system) still apply here regardless of
       the parent's display — left in place, they re-halve the already-
       50%-wide grid track (50% of a 688px track = 344px, with a dead gap
       where the other 344px should be). */
    width: auto;
    max-width: none;
  }

  body.dashboard-page .dashboard-panel {
    flex: 1 1 auto;
    min-height: 0;
  }

  body.dashboard-page .dashboard-panel .table-responsive {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
  }

  body.dashboard-page .dashboard-panel thead th {
    position: sticky;
    top: 0;
    background: var(--panel);
    box-shadow: inset 0 -1px 0 var(--line);
    z-index: 1;
  }

  body.dashboard-page .app-footer {
    flex: 0 0 auto;
    margin-top: 0;
    padding: .35rem 0 .5rem;
  }
}

@media (max-width: 991.98px) {
  .dashboard-summary {
    grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  }
}

.vehicle-header-actions {
  display: flex;
  flex: 0 0 auto;
  gap: .5rem;
}

.vehicle-action-menu {
  min-width: 13rem;
}

.vehicle-action-menu form {
  margin: 0;
}

.pm-answer {
  display: none;
}

@media (max-width: 575.98px) {
  .dashboard-summary {
    /* Keep the established two-up phone strip at the normal root size, but
       allow the track to collapse to one column when enlarged text makes
       that the only width that can hold the preserved currency token. */
    grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  }
  .navbar-brand { font-size: 1.3rem; }

  /* Stat figures (cost/mile, total cost, etc.) default to Bootstrap's 2.5rem
     display-6 — fine in a full-width column, but it breaks mid-number
     ("$11,690.02") once two stats share a col-6 row on a phone. Shrink and
     keep each figure on one line instead. */
  .display-6 {
    font-size: 1.65rem;
    white-space: nowrap;
  }

  .mobile-scroll-cue {
    position: sticky;
    left: 0;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: .45rem;
    width: 100%;
    min-height: 1.8rem;
    padding: .3rem .65rem;
    border-bottom: 1px solid rgba(217, 138, 15, .28);
    background: var(--amber-tint);
    box-shadow: inset 3px 0 0 var(--amber);
    color: var(--ink);
    font-size: .75rem;
    font-weight: 650;
  }

  .mobile-scroll-region {
    scroll-margin-top: calc(var(--org-state-offset, 0rem) + 4rem);
  }

  .mobile-scroll-cue > span {
    font-family: var(--font-mono);
    font-size: .9rem;
  }

  .recent-invoice-category {
    display: none;
  }

  .vehicle-detail-header {
    flex-wrap: wrap;
    row-gap: .6rem;
  }

  .vehicle-header-actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    width: 100%;
  }

  .vehicle-header-actions .dropdown > .btn {
    width: 100%;
  }

  .pm-panel + .pm-panel {
    padding-top: 1rem;
    border-top: 1px solid var(--line-soft);
  }

  .pm-answer {
    display: block;
    margin: 0 0 .55rem;
    color: var(--route-deep);
    font-size: .9rem;
  }

  .pm-answer strong {
    font-family: var(--font-mono);
    font-size: 1.05rem;
  }

  .pm-answer.text-danger {
    color: var(--stop) !important;
  }

  .pm-facts {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: .35rem 1rem;
  }

  .pm-facts > dt,
  .pm-facts > dd {
    width: auto;
    margin: 0;
    padding: 0;
  }

  .pm-facts > dt {
    color: var(--ink-soft);
    font-weight: 600;
  }

  .pm-facts > dd {
    text-align: right;
    white-space: nowrap;
  }

  .pm-facts > .pm-fact-mobile-hidden {
    display: none;
  }
}

/* ---- Login page -----------------------------------------------------
   The one page in the app with no nav (base.html hides the nav links and
   toggler entirely when there's no current_user) -- so this owns its own
   centered layout rather than sitting inside the usual page container. */

.login-page .container-fluid {
  padding-top: 0;
  padding-bottom: 0;
}

.login-wrap {
  display: flex;
  justify-content: center;
  padding: 3.5rem 1rem;
}

.login-card {
  width: 100%;
  max-width: 380px;
  box-shadow: var(--shadow-md);
}

.login-card .card-body {
  padding: 2rem 1.75rem;
}

.login-mark {
  display: flex;
  align-items: center;
  gap: .55rem;
  margin-bottom: 1.5rem;
}

.login-wordmark {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.4rem;
  text-transform: uppercase;
  letter-spacing: .02em;
  color: var(--ink);
}

/* ---- Account / self-service page -------------------------------------*/

.account-role-tag {
  display: inline-block;
  margin-bottom: 1rem;
}

/* ---- Frontline field UI (iPad-first PWA phase 2) ------------------------
   Touch-target and layout rules specific to templates/frontline/*.html.
   Not applied globally -- the rest of the app stays desktop/admin-oriented
   by design; this is where the phase-2 roadmap's "optimize touch targets,
   numeric keyboards, portrait/landscape" item actually lives. 2.75rem
   (~44px) matches Apple's minimum recommended touch target. */

.frontline-search {
  font-size: 1.05rem;
  padding: .65rem .9rem;
  min-height: 2.75rem;
}

.frontline-unit-list {
  display: flex;
  flex-direction: column;
  gap: .5rem;
}

.frontline-unit-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  padding: .85rem 1rem;
  min-height: 3.25rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: inherit;
  text-decoration: none;
}
.frontline-unit-row:hover,
.frontline-unit-row:focus-visible {
  border-color: var(--amber);
  background: var(--amber-tint);
}
.frontline-unit-row .unit-number {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.15rem;
  color: var(--ink);
}

.frontline-action-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1rem;
}

.frontline-action-card .card-body {
  padding: 1.1rem;
}

.frontline-action-card .btn,
.frontline-action-card .form-control,
.frontline-action-card .form-select {
  min-height: 2.75rem;
}

.frontline-action-card textarea.form-control {
  min-height: 4.5rem;
}

@media (max-width: 575.98px) {
  .frontline-action-grid { grid-template-columns: 1fr; }
}

/* ---- Line-note: what a proposed line covers ------------------------------
   A charge can cover two trucks, and the reader has to decide how it divides.
   The note is a strip under its own line, held in the margin by a rule, so it
   reads as an annotation on the line above rather than a billed line of its
   own.

   Route blue, not amber. Amber is this system's caution colour and is spent
   on PM due-soon and failed states; an unassigned multi-truck line is neither
   wrong nor urgent, it is a signpost saying which way to go next -- which is
   what highway-sign blue is for here. The one genuine caution, a unit this
   fleet does not have, is the only thing that takes the stop colour. */

/* Selector deliberately mirrors the nowrap rule's own shape above. That rule
   is `.table-responsive .table > :not(caption) > * > *`, which outranks a
   plain `.line-note-row > td` -- matching its wrapper classes is what lets
   this one win. */
.table-responsive .table > tbody > tr.line-note-row > td {
  border-top: 0;
  padding-top: 0;
  padding-bottom: .55rem;
  background: transparent;
  /* Data cells in this table are nowrap on purpose -- a figure or a unit
     should not fold in half, the table scrolls instead. This cell is a
     sentence, and the rule has to be lifted or it runs off the side of the
     scroll container and is never read. */
  white-space: normal;
}

/* inline-flex, not flex: the cell spans all eight columns so the note sits
   under its whole line, but the tinted strip should hug the few words it
   holds. Stretched to the full table width it becomes a wide empty field of
   colour that outweighs the line it is annotating. */
.line-note {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: .3rem .5rem;
  margin: 0;
  padding: .4rem .6rem;
  border-left: 3px solid var(--route);
  background: var(--route-tint);
  border-radius: 0 var(--radius) var(--radius) 0;
  /* The cell spans a table that is wider than its scroll container, so the
     note has to be capped against the viewport rather than its parent -- a
     percentage here resolves against the table and overflows exactly as if
     there were no cap. A long basis is otherwise cut off mid-sentence at the
     container edge, which is the half that says to check the figure. */
  max-width: calc(100vw - 5rem);
}

.line-note-label {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: .66rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--route-deep);
}

.line-note-text {
  font-size: .8rem;
  color: var(--ink-soft);
  /* A flex item will not shrink below its own min-content width unless told
     to, so without this the sentence keeps its full single-line width and
     runs out past the capped strip on a phone instead of wrapping inside it. */
  min-width: 0;
}

/* A unit number is how everyone here names a truck, so it is set as a tag
   clipped to a key -- the same idiom as the status badges -- and in the mono
   face, because it is an identifier and not a word. Two tags side by side say
   "one charge, two trucks" faster than the sentence beside them. */
.unit-tag {
  font-family: var(--font-mono);
  font-size: .72rem;
  font-weight: 600;
  line-height: 1.2;
  padding: .16em .4em;
  border-radius: 3px;
  background: var(--panel);
  color: var(--route-deep);
  border: 1px solid rgba(36, 88, 127, .35);
}

.unit-tag-missing {
  color: var(--stop);
  border-color: rgba(178, 58, 46, .45);
  border-style: dashed;
  background: var(--stop-tint);
}

/* A shared line keeps one amount field and records its confirmed recipients
   in place. The helper is deliberately quiet until somebody selects fewer
   than two vehicles; that is a review state, not a validation crash. */
.line-allocation {
  max-width: 15rem;
}

.line-allocation .line-shared-vehicles {
  min-height: 5.25rem;
}

.line-allocation-help {
  display: block;
  max-width: 15rem;
}

/* On a phone the split table scrolls sideways inside its own container, and
   an unwrapped note is wider than the screen -- putting the sentence that
   explains the row off to the right, where somebody scanning the invoice will
   never see it. Cap it so it wraps and stays where reading starts. */
@media (max-width: 575.98px) {
  .line-note { max-width: 16rem; }
}

/* An estimated split is a figure this app worked out, not one printed on the
   invoice, and it must not read like the rest of the row. Amber, because it
   is the system's "check this" colour -- the same one PM due-soon uses -- and
   because a proposed number that looks settled is the one nobody verifies. */
.line-note-estimated {
  border-left-color: var(--amber);
  background: var(--amber-tint);
}

.line-note-estimated .line-note-label {
  color: var(--amber-deep);
}

/* ---- Component register (modular build) ---------------------------------
   Component statuses get their own tag vocabulary, deliberately not reusing
   the truck words (active/down/retired). A part is "in yard" or "in shop",
   never "down" — sharing the words makes the pool read as a second fleet
   list. Same rectangular printed-edge shape as .status-badge-* so they still
   look like one system. */

.t-inservice,
.t-yard,
.t-shop,
.t-scrapped,
.t-unrecorded {
  display: inline-block;
  border-radius: 3px;
  padding: .32em .6em;
  font-weight: 700;
  font-size: .68rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  border: 1px solid rgba(0, 0, 0, .12);
}

.t-inservice { background-color: var(--go-tint);    color: #1f5735; }
.t-yard      { background-color: var(--amber-tint); color: #7a5308; }
.t-shop      { background-color: var(--stop-tint);  color: #7a281f; }
.t-scrapped  { background-color: var(--idle-tint);  color: var(--ink-soft); }

/* A backfilled placeholder serial. Amber rather than grey on purpose: an
   unrecorded serial is a gap somebody should close on their next walk of the
   yard, and greying it out is how it stays unnoticed for a year. */
.t-unrecorded {
  background-color: var(--amber-tint);
  color: var(--amber-deep);
  margin-left: .4rem;
}

/* Serials are always mono, truck numbers always the display face. A reader
   should be able to tell which kind of identifier they're looking at without
   reading the column header. */
.c-serial {
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: 1rem;
  display: inline-block;
  max-width: 24ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}

.truck-no {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.15rem;
  letter-spacing: .01em;
}

/* Service-life meter. Rendered only when there is something to measure —
   see the life_meter macro, which emits nothing rather than an empty track,
   because a 0%-wide bar reads as "nothing used yet" instead of "unknown". */
.life-meter { min-width: 150px; }

.life-track {
  height: 6px;
  border-radius: 3px;
  background: var(--line-soft);
  overflow: hidden;
}

.life-fill {
  height: 100%;
  border-radius: 3px;
  background: var(--idle);
}

.life-fill.life-body   { background: var(--body); }
.life-fill.life-reefer { background: var(--reefer); }

/* At 70% of expected life the fill turns red — but the caption underneath
   always states the figures too, so the warning never rides on colour alone. */
.life-fill.is-spent { background: var(--stop); }

.life-caption {
  display: flex;
  justify-content: space-between;
  gap: .5rem;
  margin-top: .25rem;
  font-size: .68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--ink-soft);
}

.life-caption-alone { margin-top: 0; }

/* Serial entry is the one field on these forms that must be typed exactly,
   so it gets the amber emphasis and the mono face it will be displayed in. */
.serial-input {
  font-family: var(--font-mono);
  font-size: 1rem;
  border: 2px solid var(--amber);
}

.serial-input:focus {
  border-color: var(--amber);
  box-shadow: 0 0 0 3px rgba(217, 138, 15, .15);
}

/* ---- Assembly card (truck detail) ---------------------------------------
   Three slots, fixed order, always all three even when one is empty — an
   empty slot is information, and a card that quietly renders two reads as a
   complete build. */

/* "Make default" sits with Remove under each frame: both are decisions about
   this one photo, made where the photo is. The current default says so as
   plain text rather than a button, because it is a state, not an action. */
.photo-default { display: inline; }
.photo-default.is-default {
  color: var(--amber-deep);
  font-family: var(--font-display);
  font-size: .72rem;
  letter-spacing: .04em;
  text-transform: uppercase;
}

/* The build: one photo of the assembled truck beside the parts it is made of.
   Paired rather than stacked because they are the same statement -- this is the
   truck, and this is what is under it. The photo is a link to the rest of the
   photography, which used to occupy roughly a third of this page as three
   galleries with an upload form each.

   auto-fit keeps the pairing honest at any width: below about 46rem there is no
   room for a photo beside three slots, so it drops above them rather than
   squeezing both. */
.build {
  display: grid;
  grid-template-columns: minmax(min(100%, 15rem), 18rem) minmax(0, 1fr);
  gap: 1rem;
  align-items: start;
}

.build-photo {
  position: relative;
  display: block;
  aspect-ratio: 4 / 3;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--idle-tint);
  text-decoration: none;
}

.build-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.build-photo-empty {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--ink-soft);
  font-size: .8rem;
}

/* Sits on the image rather than under it: the count is a way in to the rest of
   the photos, not a caption, and a line of text below would push the slots out
   of alignment with the frame. */
.build-photo-count {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: .3rem .55rem;
  background: linear-gradient(to top, rgba(27, 31, 35, .82), rgba(27, 31, 35, 0));
  color: #fff;
  font-family: var(--font-display);
  font-size: .78rem;
  letter-spacing: .04em;
  text-transform: uppercase;
}

.build-photo:hover { border-color: var(--amber); }
.build-photo:hover .build-photo-count { background: linear-gradient(to top, rgba(217, 138, 15, .9), rgba(217, 138, 15, 0)); color: var(--ink); }

@media (max-width: 735px) {
  .build { grid-template-columns: 1fr; }
  .build-photo { aspect-ratio: 16 / 9; }
}

.assembly {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0;
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.assembly .slot {
  padding: .85rem .9rem;
  border-top: 4px solid var(--line);
  border-right: 1px solid var(--line-soft);
  background: var(--panel);
}

.assembly .slot:last-child { border-right: none; }

.slot-chassis { border-top-color: var(--chassis); }
.slot-body    { border-top-color: var(--body); }
.slot-reefer  { border-top-color: var(--reefer); }

/* An empty slot is hatched rather than blank: blank space reads as "still
   loading" or "nothing to say here", the hatch reads as "deliberately
   nothing fitted". */
.assembly .slot.is-empty {
  background: repeating-linear-gradient(
    45deg,
    var(--paper),
    var(--paper) 6px,
    transparent 6px,
    transparent 12px
  );
  border-top-color: var(--line);
}

.slot-kind {
  display: flex;
  align-items: center;
  gap: .4rem;
  font-family: var(--font-body);
  font-size: .66rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .09em;
  color: var(--ink-soft);
  margin: 0 0 .35rem;
}

.slot-dot {
  width: 8px;
  height: 8px;
  border-radius: 2px;
  background: var(--line);
  flex: 0 0 auto;
}

.slot-chassis .slot-dot { background: var(--chassis); }
.slot-body    .slot-dot { background: var(--body); }
.slot-reefer  .slot-dot { background: var(--reefer); }

.slot-id { line-height: 1.2; }

/* The chassis slot carries a truck number, not a serial, so it renders in
   the display face even though it sits in a component card — the typography
   is what tells the reader which kind of identifier they're looking at. */
.slot-chassis .slot-id .truck-no { font-size: 1.5rem; }

.slot-meta {
  font-size: .75rem;
  color: var(--ink-soft);
  margin-top: .2rem;
}

.assembly.is-retired .slot { opacity: .6; }

@media (max-width: 899.98px) {
  /* Each slot keeps its coloured top border as the separator when stacked. */
  .assembly { grid-template-columns: 1fr; }
  .assembly .slot { border-right: none; }
}

/* ---- Inherited invoices -------------------------------------------------
   Spend billed to a part now fitted to this truck but logged under another.
   Tinted so it is never mistaken for this build's own cost, and shown by
   default — hiding it would recreate the blind spot the modular build exists
   to close. Never summed into cost/mile. */

tr.inherited > td,
tr.inherited > th {
  background: var(--route-tint);
}

.t-prior {
  display: inline-block;
  margin-left: .4rem;
  border-radius: 3px;
  padding: .22em .45em;
  font-size: .62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  background: var(--panel);
  color: var(--route-deep);
  border: 1px solid rgba(36, 88, 127, .3);
}

/* ---- "Billed to" consequence panel (invoice form) -----------------------
   States the outcome of the current choice in words. Route-tinted rather
   than amber: this is information about what will happen, not a warning
   that something is wrong. */

.billed-to-panel {
  background: var(--route-tint);
  border-left: 3px solid var(--route);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: .55rem .75rem;
  font-size: .82rem;
  color: var(--ink);
  min-height: 2.6rem;
}

/* --- Condition galleries -------------------------------------------------
   A scroll-snap strip, not a carousel. Native swipe on a phone, no JS, and
   no state to get out of sync with the DOM. Component colours are identity
   only here, same rule as the assembly slots: they tint the card edge so a
   body photo cannot be mistaken for a refrigeration photo at a glance. */
.photo-strip {
  display: flex;
  gap: .75rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  list-style: none;
  margin: 0;
  padding: 0 0 .5rem;
  scrollbar-width: thin;
}

.photo-strip:focus-visible {
  outline: 2px solid var(--amber);
  outline-offset: 2px;
}

.photo-card {
  flex: 0 0 auto;
  width: 220px;
  scroll-snap-align: start;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.photo-card img {
  display: block;
  width: 100%;
  height: 165px;
  object-fit: cover;
  background: var(--paper);
}

.photo-meta {
  padding: .5rem .6rem;
  font-size: .72rem;
  color: var(--ink-soft);
  display: flex;
  flex-direction: column;
  gap: .15rem;
}

.photo-date {
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .04em;
}

.photo-caption {
  color: var(--ink);
  overflow-wrap: anywhere;
}

.photo-delete .btn-link {
  color: var(--stop);
  text-decoration: none;
  font-size: .72rem;
}

.photo-delete .btn-link:hover { text-decoration: underline; }

/* Full-size photos stay inside the application instead of opening a raw image
   in an auxiliary tab. Bootstrap owns modal focus, Escape, and restoration
   when JavaScript runs. The :target:not(.show) rules expose the same viewer and
   return link as an ordinary in-flow section if JavaScript is unavailable. */
.photo-viewer-dialog {
  width: 100%;
  max-width: none;
  min-height: 100dvh;
  margin: 0;
}

.photo-viewer-content {
  min-height: 100dvh;
  border: 0;
  border-radius: 0;
}

.photo-viewer-header {
  gap: 1rem;
  padding-top: max(1rem, env(safe-area-inset-top));
  padding-right: max(1rem, env(safe-area-inset-right));
  padding-left: max(1rem, env(safe-area-inset-left));
}

.photo-viewer-back {
  flex: 0 0 auto;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

.photo-viewer-back-js {
  display: none;
}

.photo-viewer.show .photo-viewer-back-js {
  display: inline-flex;
}

.photo-viewer.show .photo-viewer-back-fallback {
  display: none;
}

.photo-viewer-body {
  min-width: 0;
  overflow: auto;
  padding-right: max(1rem, env(safe-area-inset-right));
  padding-bottom: max(1rem, env(safe-area-inset-bottom));
  padding-left: max(1rem, env(safe-area-inset-left));
}

.photo-viewer-figure {
  min-height: calc(100dvh - 6rem);
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .75rem;
}

.photo-viewer-figure img {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: calc(100dvh - 10rem - env(safe-area-inset-top) - env(safe-area-inset-bottom));
  object-fit: contain;
  background: var(--paper);
}

.photo-viewer-caption {
  width: min(100%, 60rem);
  color: var(--ink-soft);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: .35rem .75rem;
  overflow-wrap: anywhere;
}

.photo-viewer:target {
  display: block;
  opacity: 1;
  background: var(--panel);
}

.photo-viewer:target .modal-dialog {
  transform: none;
}

.photo-viewer:target:not(.show) {
  position: static;
}

.photo-viewer:target:not(.show) .photo-viewer-dialog {
  min-height: auto;
}

.photo-viewer:target:not(.show) .photo-viewer-content {
  min-height: auto;
}

.photo-viewer:target:not(.show) .photo-viewer-body {
  overflow: visible;
  min-height: 0;
}

.photo-viewer:target:not(.show) .photo-viewer-figure {
  min-height: 0;
}

/* The strip is already horizontally scrollable, so it needs no breakpoint of
   its own -- only the cards shrink, to fit more than one on a narrow phone. */
@media (max-width: 575.98px) {
  .photo-card { width: 165px; }
  .photo-card img { height: 124px; }
}

/* ---- Dense records on narrow workspaces ---------------------------------
   Vehicle and invoice tables remain tables on desktop. At phone and split-
   view widths, each record becomes a compact field group so the operator can
   finish the task in place instead of steering a thousand-pixel canvas. */

.vehicle-register-label,
.vehicle-register-action,
.line-field-label > span,
.line-mobile-label,
.line-remove-label,
.line-shared-label > span,
.account-tier-label {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.line-field-label {
  display: block;
}

.line-shared-label {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

@media (max-width: 1199.98px) {
  .table-responsive.vehicle-register-region,
  .table-responsive.invoice-line-items-region {
    overflow: visible;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    background: transparent;
  }

  .vehicle-register-table,
  .vehicle-register-table > tbody,
  #lineTable,
  #lineTable > tbody {
    display: block;
    width: 100%;
  }

  .vehicle-register-table > thead,
  #lineTable > thead {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }

  .vehicle-register-table > tbody,
  #lineTable > tbody {
    display: grid;
    gap: .75rem;
  }

  .vehicle-register-table > tbody > tr.vehicle-register-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
  }

  .vehicle-register-table > tbody > tr.vehicle-register-row > td {
    display: block;
    min-width: 0;
    padding: .55rem .75rem;
    border: 0;
    border-bottom: 1px solid var(--line-soft);
    text-align: left !important;
    white-space: normal;
    overflow-wrap: anywhere;
  }

  .vehicle-register-table .vehicle-register-identity {
    grid-column: 1;
    grid-row: 1;
  }

  .vehicle-register-table .vehicle-register-status {
    grid-column: 2;
    grid-row: 1;
  }

  .vehicle-register-table .vehicle-register-model {
    grid-column: 1 / -1;
    grid-row: 2;
  }

  .vehicle-register-label,
  .line-field-label > span,
  .line-mobile-label,
  .line-shared-label > span {
    position: static;
    display: block;
    width: auto;
    height: auto;
    margin: 0 0 .2rem;
    overflow: visible;
    clip: auto;
    white-space: normal;
    color: var(--ink-soft);
    font-size: .72rem;
    font-weight: 600;
    line-height: 1.2;
  }

  .vehicle-register-link {
    display: inline-flex;
    align-items: baseline;
    gap: .5rem;
  }

  .vehicle-register-action {
    position: static;
    display: inline;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip: auto;
    white-space: normal;
    color: var(--route);
    font-size: .75rem;
    font-weight: 600;
  }

  .vehicle-register-table > tbody > tr.vehicle-register-empty {
    display: block;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
  }

  .vehicle-register-table > tbody > tr.vehicle-register-empty > td {
    display: block;
    white-space: normal;
  }

  #lineTable {
    min-width: 0;
    max-width: 100%;
    table-layout: auto;
    background: transparent;
  }

  #lineTable > colgroup {
    display: none;
  }

  #lineTable > tbody > tr.line-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: .75rem;
    padding: .85rem;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
  }

  #lineTable > tbody > tr.line-row > td.line-field {
    display: block;
    min-width: 0;
    padding: 0;
    border: 0;
    text-align: left !important;
    white-space: normal;
  }

  #lineTable .line-field-reference { grid-column: 1; grid-row: 1; }
  #lineTable .line-field-amount { grid-column: 2; grid-row: 1; }
  #lineTable .line-field-description { grid-column: 1 / -1; grid-row: 2; }
  #lineTable .line-field-vehicle { grid-column: 1; grid-row: 3; }
  #lineTable .line-field-shared { grid-column: 2; grid-row: 3; }
  #lineTable .line-field-owner { grid-column: 1; grid-row: 4; }
  #lineTable .line-field-work { grid-column: 2; grid-row: 4; }
  #lineTable .line-field-actions { grid-column: 1 / -1; grid-row: 5; }

  #lineTable .form-control,
  #lineTable .form-select,
  #lineTable .btn {
    max-width: 100%;
    min-height: 2.75rem;
  }

  #lineTable .line-shared-label {
    justify-content: flex-start;
    gap: .5rem;
    min-height: 2.75rem;
  }

  #lineTable .line-shared-label > span {
    margin: 0;
    color: var(--ink);
    font-size: .875rem;
    font-weight: 400;
  }

  #lineTable .line-owner-summary {
    min-height: 2.75rem;
    padding: .45rem .6rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--paper);
  }

  #lineTable .line-owner-summary .line-mobile-label {
    margin-bottom: .1rem;
  }

  #lineTable .line-field-actions .remove-row {
    width: 100%;
  }

  #lineTable .line-remove-symbol {
    display: none;
  }

  #lineTable .line-remove-label {
    position: static;
    display: inline;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip: auto;
    white-space: normal;
  }

  #lineTable > tbody > tr.line-note-row {
    display: block;
    margin-top: -.5rem;
  }

  .table-responsive #lineTable > tbody > tr.line-note-row > td {
    display: block;
    padding: 0 .85rem;
  }

  #lineTable .line-note {
    width: 100%;
    max-width: none;
  }
}

/* #241: Account tiers on Organization Settings — keep the desktop comparison
   table, but stack each record through tablet widths so identity, scope,
   counts, and Edit stay visible without a nested two-axis scroll. */
@media (max-width: 1199.98px) {
  .table-responsive.account-tier-region {
    overflow: visible;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    background: transparent;
  }

  .account-tier-table,
  .account-tier-table > tbody {
    display: block;
    width: 100%;
  }

  .account-tier-table > thead {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }

  .account-tier-table > tbody {
    display: grid;
    gap: .75rem;
  }

  .account-tier-table > tbody > tr.account-tier-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
  }

  .account-tier-table > tbody > tr.account-tier-row > td {
    display: block;
    min-width: 0;
    padding: .55rem .75rem;
    border: 0;
    border-bottom: 1px solid var(--line-soft);
    text-align: left !important;
    white-space: normal;
    overflow-wrap: anywhere;
  }

  .account-tier-table .account-tier-identity {
    grid-column: 1 / -1;
  }

  .account-tier-table .account-tier-actions {
    grid-column: 1 / -1;
    border-bottom: 0;
  }

  .account-tier-label {
    position: static;
    display: block;
    width: auto;
    height: auto;
    margin: 0 0 .2rem;
    overflow: visible;
    clip: auto;
    white-space: normal;
    color: var(--ink-soft);
    font-size: .72rem;
    font-weight: 600;
    line-height: 1.2;
  }

  .account-tier-description {
    white-space: normal;
    overflow-wrap: anywhere;
  }

  .account-tier-action-group {
    display: flex;
    flex-wrap: wrap;
    gap: .5rem;
    align-items: center;
  }

  .account-tier-action-group .btn-outline-primary {
    order: 0;
  }

  .account-tier-action-group .btn-outline-secondary {
    order: 1;
  }

  .account-tier-table > tbody > tr.account-tier-empty {
    display: block;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
  }

  .account-tier-table > tbody > tr.account-tier-empty > td {
    display: block;
    white-space: normal;
  }
}

@media (max-width: 575.98px) {
  #lineTable > tbody > tr.line-row {
    grid-template-columns: minmax(0, 1fr);
  }

  #lineTable > tbody > tr.line-row > td.line-field {
    grid-column: 1 / -1;
    grid-row: auto;
  }
}
