/* ============================================================================
   Backup Logistics - site styles
   Plain CSS, no build step. Loaded after tokens.css on every page.
   ========================================================================= */

/* ---- reset ------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

img, svg, video { display: block; max-width: 100%; height: auto; }
input, button, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; }
a { color: inherit; }

/* Visible focus everywhere. The old site had no :focus rules at all, so the
   whole thing was unusable by keyboard. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}
:focus:not(:focus-visible) { outline: none; }

::selection { background: var(--accent); color: var(--on-accent); }

.skip-link {
  position: absolute; left: var(--s4); top: -100px;
  background: var(--accent); color: var(--on-accent);
  padding: var(--s3) var(--s5); border-radius: var(--r);
  z-index: 999; font-weight: 600; transition: top var(--fast) var(--ease);
}
.skip-link:focus { top: var(--s4); }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---- layout ------------------------------------------------------------ */
.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--gutter); }
.wrap-wide { width: 100%; max-width: var(--wrap-wide); margin-inline: auto; padding-inline: var(--gutter); }
.section { padding-block: var(--section-y); }
.section-sunk { background: var(--surface-sunk); }
.section-ink { background: var(--ink); color: var(--paper); }
/* .section-ink is NOT an always-dark band. Its background is --ink and its
   text is --paper, and BOTH of those swap in dark mode, so the band inverts
   to a light panel with dark text. Anything tinted inside it must therefore
   be derived from currentColor, never from the fixed-white --on-dark-* scale.
   Measured with the fixed white in place: this lead read 1.09:1 against the
   inverted band. carriers.html and services.html already tint their band copy
   from currentColor; this is the shared rule catching up. */
.section-ink .lead, .section-ink .muted { color: color-mix(in srgb, currentColor 72%, transparent); }

main { min-height: 60vh; }

/* ---- type --------------------------------------------------------------
   The hierarchy is built from size, tracking and leading, because Archivo is
   loaded at 400-700 only and there is no heavier display cut to reach for.
   Every heading level therefore sets tighter and leads tighter than the one
   below it, and the tokens carry the relationship so it stays deliberate. */
h1, h2, h3, h4 { line-height: var(--lh-heading); font-weight: 700; letter-spacing: var(--track-tight); text-wrap: balance; }
h1 { font-size: var(--t-h1); letter-spacing: var(--track-display); line-height: var(--lh-display); }
h2 { font-size: var(--t-h2); }
h3 { font-size: var(--t-h3); line-height: 1.25; letter-spacing: var(--track-snug); }
h4 { letter-spacing: var(--track-snug); }
p  { text-wrap: pretty; }

.hero-title { font-size: var(--t-hero); line-height: var(--lh-display); letter-spacing: var(--track-display); font-weight: 700; }
/* One tier above the hero, for a headline over a full-bleed photograph. */
.display {
  font-size: var(--t-display); line-height: var(--lh-display);
  letter-spacing: var(--track-display); font-weight: 700;
  text-wrap: balance;
}

/* Tight display tracking is a desktop luxury. At 375px, on mobile data, in a
   cab, in daylight, letters that touch are letters that get misread, so the
   display sizes give back most of the negative tracking below 560px. The type
   is smaller there anyway, and small type wants air. */
@media (max-width: 560px) {
  h1, .hero-title, .display { letter-spacing: -.024em; }
}

.lead { font-size: var(--t-lead); color: var(--ink-soft); max-width: var(--measure); line-height: var(--lh-lead); }
.muted { color: var(--ink-soft); }
.faint { color: var(--ink-faint); font-size: var(--t-small); }
.measure { max-width: var(--measure); }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* Used sparingly. Counted against the eyebrow budget: at most one per three
   sections on any page. */
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--accent-ink);
  font-weight: 500;
}

.stack > * + * { margin-top: var(--s4); }
.stack-lg > * + * { margin-top: var(--s6); }

/* ---- buttons ----------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s2);
  padding: 0.8rem 1.4rem;
  border-radius: var(--r);
  font-weight: 600;
  font-size: var(--t-body);
  line-height: 1.2;
  white-space: nowrap;              /* CTA labels never wrap */
  text-decoration: none;
  border: 1.5px solid transparent;
  transition: background var(--fast) var(--ease),
              border-color var(--fast) var(--ease),
              color var(--fast) var(--ease),
              transform var(--fast) var(--ease);
}
.btn:active { transform: translateY(1px); }

.btn-primary { background: var(--accent); color: var(--on-accent); }
.btn-primary:hover { background: var(--accent-press); }

.btn-secondary { background: transparent; color: var(--ink); border-color: var(--rule-strong); }
.btn-secondary:hover { border-color: var(--ink); background: var(--surface); }

/* Same reason as the .lead rule above: the band inverts, so the outline and
   the hover wash are mixed from currentColor and not from fixed white. */
.section-ink .btn-secondary { color: var(--paper); border-color: color-mix(in srgb, currentColor 32%, transparent); }
.section-ink .btn-secondary:hover { border-color: var(--paper); background: color-mix(in srgb, currentColor 8%, transparent); }

.btn-lg { padding: 1rem 1.75rem; font-size: var(--t-lead); }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .5; pointer-events: none; }
.btn-block { width: 100%; }

/* ---- link with a moving rule ------------------------------------------- */
.link {
  color: var(--accent-ink); text-decoration: none; font-weight: 600;
  background-image: linear-gradient(var(--accent-ink), var(--accent-ink));
  background-size: 100% 1.5px; background-repeat: no-repeat;
  background-position: 0 100%;
  transition: background-size var(--med) var(--ease);
}
.link:hover { background-size: 0% 1.5px; background-position: 100% 100%; }

/* ---- nav --------------------------------------------------------------- */
.nav {
  position: sticky; top: 0; z-index: var(--z-nav);
  height: var(--nav-h);
  background: color-mix(in srgb, var(--surface) 86%, transparent);
  backdrop-filter: blur(12px) saturate(140%);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  border-bottom: 1px solid var(--rule);
}
@media (prefers-reduced-transparency: reduce) {
  .nav { background: var(--surface); backdrop-filter: none; -webkit-backdrop-filter: none; }
}
.nav-in {
  height: 100%; display: flex; align-items: center; gap: var(--s5);
  max-width: var(--wrap-wide); margin-inline: auto; padding-inline: var(--gutter);
}
.brand { display: flex; align-items: center; gap: var(--s3); text-decoration: none; margin-right: auto; }
.brand-mark {
  width: 34px; height: 34px; flex: none;
  display: grid; place-items: center;
  background: var(--accent); color: var(--on-accent);
  border-radius: var(--r);
  font-weight: 700; font-size: 0.9rem; letter-spacing: -.02em;
}
.brand-name { font-weight: 700; letter-spacing: -.02em; font-size: 1.05rem; line-height: 1; }
.brand-sub { font-size: 0.66rem; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-faint); line-height: 1; margin-top: 3px; }

.nav-links { display: flex; align-items: center; gap: var(--s5); list-style: none; padding: 0; }
.nav-links a {
  text-decoration: none; font-size: var(--t-small); font-weight: 500;
  color: var(--ink-soft); transition: color var(--fast) var(--ease);
}
.nav-links a:hover, .nav-links a[aria-current="page"] { color: var(--ink); }
.nav-links a[aria-current="page"] { font-weight: 600; }

.nav-actions { display: flex; align-items: center; gap: var(--s3); }

.theme-btn {
  width: 40px; height: 40px; display: grid; place-items: center;
  border-radius: var(--r); border: 1px solid var(--rule);
  color: var(--ink-soft);
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.theme-btn:hover { color: var(--ink); border-color: var(--rule-strong); }
.theme-btn svg { width: 17px; height: 17px; }
.theme-btn .icon-dark { display: none; }
:root[data-theme="dark"] .theme-btn .icon-dark { display: block; }
:root[data-theme="dark"] .theme-btn .icon-light { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-btn .icon-dark { display: block; }
  :root:not([data-theme="light"]) .theme-btn .icon-light { display: none; }
}

.nav-toggle { display: none; width: 40px; height: 40px; place-items: center; border-radius: var(--r); border: 1px solid var(--rule); }
.nav-toggle svg { width: 20px; height: 20px; }

@media (max-width: 900px) {
  .nav-toggle { display: grid; }
  .nav-links {
    position: fixed; inset: var(--nav-h) 0 auto 0;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--surface);
    border-bottom: 1px solid var(--rule);
    padding: var(--s3) var(--gutter) var(--s5);
    transform: translateY(-8px); opacity: 0; pointer-events: none;
    transition: opacity var(--fast) var(--ease), transform var(--fast) var(--ease);
    box-shadow: var(--lift-2);
    max-height: calc(100dvh - var(--nav-h)); overflow-y: auto;
  }
  .nav-links[data-open="true"] { transform: none; opacity: 1; pointer-events: auto; }
  .nav-links a { padding: var(--s4) 0; font-size: 1.05rem; border-bottom: 1px solid var(--rule); }
  .nav-links li:last-child a { border-bottom: 0; }
  .nav .btn-primary { display: none; }
}

/* ---- footer ------------------------------------------------------------ */
/* --footer-bg, not --ink. --ink FLIPS with the theme: in dark mode it is
   #f2f2f0, which made this a near-white slab carrying white text at
   1.12:1. Fixed because the --on-dark-* text on it is fixed. */
.footer { background: var(--footer-bg); color: var(--on-dark-muted); padding-block: var(--s8) var(--s6); }
/* The line that used to sit here was
       :root[data-theme="dark"] .footer { background: var(--surface-sunk); }
   and it was the actual trap. That selector matches ONLY when the visitor has
   pressed the theme toggle. A phone set to system dark, which is most of them,
   never sets data-theme at all - so the footer fell through to the base rule
   and took var(--ink), which the prefers-color-scheme block had already
   flipped to #f2f2f0. It looked correct in every test that used the toggle and
   was invisible on a real phone.
   Removed. --footer-bg is one fixed value for every mode, which is what "the
   footer is dark in both themes" meant in the first place. */
.footer a { color: var(--on-dark-muted); text-decoration: none; transition: color var(--fast) var(--ease); }
.footer a:hover { color: var(--on-dark); }
.footer h4 { color: var(--on-dark); font-size: var(--t-micro); letter-spacing: .14em; text-transform: uppercase; font-weight: 600; margin-bottom: var(--s4); }
/* The brand block is reused from the header, where "Freight dispatch" sits on
   a light surface and --ink-faint is right. In the footer the same span sits
   on --ink, and it measured 3.64:1 at 10.6px, a fail. The footer is dark in
   BOTH themes, so it takes the on-dark scale: 8.9:1. */
.footer .brand-sub { color: var(--on-dark-faint); }
.footer-grid { display: grid; gap: var(--s7); grid-template-columns: 1.6fr 1fr 1fr 1.2fr; }
.footer-list { list-style: none; padding: 0; display: grid; gap: var(--s3); font-size: var(--t-small); }
/* Measured at 375px: footer links rendered 15px tall, well under the 40px
   minimum tap target. Inline links get a real hit area on touch rather than
   relying on the visitor's aim. */
@media (max-width: 900px) {
  .footer-list a, .footer-bottom a { display: inline-block; padding-block: 0.55rem; }
  .footer-list { gap: var(--s1); }
  .footer-bottom nav ul { gap: var(--s2) var(--s5) !important; }
}
.footer-bottom {
  margin-top: var(--s8); padding-top: var(--s5);
  border-top: 1px solid var(--on-dark-rule);
  display: flex; flex-wrap: wrap; gap: var(--s4) var(--s5);
  align-items: center; justify-content: space-between;
  font-size: var(--t-small);
}
@media (max-width: 860px) { .footer-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 520px) { .footer-grid { grid-template-columns: 1fr; gap: var(--s6); } }

/* ---- cards + panels ---------------------------------------------------- */
.panel {
  background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--r-lg); padding: var(--s6);
}
.panel-tint { background: var(--accent-wash); border-color: color-mix(in srgb, var(--accent) 22%, transparent); }

/* ---- forms ------------------------------------------------------------- */
/* Label ABOVE input, always a real <label for>. Placeholder never carries
   meaning. The old site had 24 of 27 fields with no programmatic label. */
/* align-content:start is load-bearing, not tidiness. A .field is a grid box
   sitting in a .form-grid cell, and grid cells stretch by default. Its own
   rows are auto-sized, and CSS Grid resolves the default `align-content:
   normal` to STRETCH - so an auto row grows to fill any leftover height.
   The result: put "Email" (which has a .hint under it) beside "Phone" (which
   does not) and the taller cell forces the shorter field to absorb the
   difference INTO ITS INPUT. Measured live on /carrier-onboarding at 1900px:
   every field with a hint rendered its control at 50.6px, every hintless
   field beside one at 64.6px. Two boxes on the same row, visibly different
   heights, on the page that executes the agreement.
   Starting the content instead keeps every control at its natural height and
   leaves the slack at the bottom of the cell, where nobody can see it. */
.field { display: grid; gap: var(--s2); align-content: start; }
.field > label { font-size: var(--t-small); font-weight: 600; color: var(--ink); }
.field .hint { font-size: var(--t-micro); color: var(--ink-faint); }
.field .err { font-size: var(--t-micro); color: var(--bad); font-weight: 500; }
/* The required marker is aria-hidden and never the only signal, but it is
   still 14px text: --accent measured 4.28:1 on --paper, --accent-ink 6.12:1. */
.req { color: var(--accent-ink); }

.input, .select, .textarea {
  width: 100%;
  padding: 0.72rem 0.85rem;
  background: var(--surface);
  border: 1.5px solid var(--rule-strong);
  border-radius: var(--r);
  color: var(--ink);
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
}
.input::placeholder, .textarea::placeholder { color: var(--ink-faint); }
.input:hover, .select:hover, .textarea:hover { border-color: var(--ink-faint); }
.input:focus, .select:focus, .textarea:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
.input[aria-invalid="true"], .select[aria-invalid="true"], .textarea[aria-invalid="true"] { border-color: var(--bad); }
.textarea { min-height: 130px; resize: vertical; }
.select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%237a7a85' stroke-width='1.6'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 0.8rem center; background-size: 16px;
  padding-right: 2.4rem;
}
/* SAME TRAP AS THE FOOTER. An attribute-only selector matches only after the
   visitor presses the theme toggle. A phone on system dark never sets
   data-theme, so it kept the LIGHT arrow: #55555f on the dark input surface
   measures 2.35:1 and is barely visible on every dropdown in every form.
   Both halves are needed - the media query for system preference, and the
   attribute for an explicit override that disagrees with it. */
:root[data-theme="dark"] .select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2383838d' stroke-width='1.6'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E"); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2383838d' stroke-width='1.6'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E"); }
}

.form-grid { display: grid; gap: var(--s5); grid-template-columns: 1fr 1fr; }
.form-grid .span-2 { grid-column: 1 / -1; }
@media (max-width: 680px) { .form-grid { grid-template-columns: 1fr; } }

.check {
  display: grid; grid-template-columns: auto 1fr; gap: var(--s3);
  align-items: start; cursor: pointer;
  font-size: var(--t-small); line-height: 1.5;
}
.check input {
  width: 20px; height: 20px; margin-top: 1px; flex: none;
  accent-color: var(--accent); cursor: pointer;
}

/* Form status. Announced via aria-live in the markup. */
.notice { padding: var(--s4); border-radius: var(--r); font-size: var(--t-small); font-weight: 500; }
.notice-ok  { background: var(--ok-wash);  color: var(--ok);  border: 1px solid color-mix(in srgb, var(--ok) 26%, transparent); }
.notice-bad { background: var(--bad-wash); color: var(--bad); border: 1px solid color-mix(in srgb, var(--bad) 26%, transparent); }
.notice-warn{ background: var(--warn-wash);color: var(--warn);border: 1px solid color-mix(in srgb, var(--warn) 26%, transparent); }

.spinner {
  width: 15px; height: 15px; flex: none;
  border: 2px solid color-mix(in srgb, currentColor 30%, transparent);
  border-top-color: currentColor; border-radius: 50%;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 2s; } }

/* ---- theme switch: freeze every transition for one frame ----------------
   THIS IS A BUG FIX, NOT A FLOURISH. Do not delete it because it looks like
   an optimisation.

   Nearly every themed component transitions a colour (.btn transitions
   background and color, .footer a transitions color, cards transition
   border-color). Flipping data-theme changes the custom properties those
   colours are built from, and in Chromium the transitioned properties are
   then left holding their OLD resolved value and never recompute. Measured
   on this site before the fix, one click of the header toggle gave:

     .btn-primary   background rgb(210,68,19), color rgb(255,255,255)
                    instead of rgb(255,106,53) / rgb(20,16,14)
     .btn-secondary color rgb(22,22,26) on a rgb(18,18,21) page,
                    which is a contrast ratio of 1.0:1, i.e. invisible

   getAnimations() was empty at that point, so nothing was mid-flight: the
   value was simply stale, permanently, until something else dirtied it.

   So the toggle in site.js adds this class, changes the attribute, forces a
   reflow and drops the class again. With transitions off there is no
   transition to go stale, and the theme lands in one clean repaint. It also
   removes an unasked-for 160ms colour crossfade of the whole page.

   THIS LIVES IN site.css, NOT motion.css, ON PURPOSE. Only five of the eleven
   pages load motion.css; every page loads this file, and every page has the
   toggle in its header. Putting it in motion.css left about, carriers,
   services, privacy, terms and sms-terms still broken, which is how the gap
   was found. */
:root.theme-switching,
:root.theme-switching *,
:root.theme-switching *::before,
:root.theme-switching *::after {
  transition: none !important;
}

/* ---- scroll reveal ----------------------------------------------------- */
/* MOTION_INTENSITY 4: content enters once, nothing loops, nothing parallaxes.
   Driven by IntersectionObserver in site.js, never a scroll listener. */
.reveal { opacity: 0; transform: translateY(18px); }
.reveal.in { opacity: 1; transform: none; transition: opacity .55s var(--ease), transform .55s var(--ease); }
@media (prefers-reduced-motion: reduce) {
  .reveal, .reveal.in { opacity: 1; transform: none; transition: none; }
}

/* ---- legal documents --------------------------------------------------- */
.doc { max-width: 74ch; }
.doc h2 { font-size: var(--t-h3); margin-top: var(--s7); margin-bottom: var(--s3); }
.doc h2:first-of-type { margin-top: var(--s5); }
.doc h3 { font-size: 1.02rem; margin-top: var(--s5); margin-bottom: var(--s2); }
.doc p, .doc li { color: var(--ink-soft); }
.doc p + p { margin-top: var(--s4); }
.doc ul, .doc ol { margin: var(--s4) 0; padding-left: 1.3rem; display: grid; gap: var(--s2); }
.doc strong { color: var(--ink); font-weight: 600; }
.doc-meta { font-size: var(--t-small); color: var(--ink-faint); }
.doc-callout {
  background: var(--accent-wash);
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--r) var(--r) 0;
  padding: var(--s5); margin: var(--s5) 0;
}
.doc-callout p { color: var(--ink); }

/* ---- utility ----------------------------------------------------------- */
.hide { display: none !important; }
.center { text-align: center; }
.row { display: flex; flex-wrap: wrap; gap: var(--s4); align-items: center; }
@media (max-width: 520px) {
  .row-cta { flex-direction: column; align-items: stretch; }
  .row-cta .btn { width: 100%; }
}

/* ============================================================================
   CHARACTER LAYER
   ----------------------------------------------------------------------------
   Everything above this line is structure and contrast, and it is verified:
   every colour clears WCAG AA in both themes. Nothing here changes a single
   one of those values. This layer only adds texture, rhythm and a few
   domain-specific components on top.

   THE IDEA. A generic SaaS layout would work here and would look like every
   other dispatch company. The one thing this business actually promises a
   carrier is "we handle your paperwork" - rate confirmations, BOLs, invoices,
   a signed agreement. So the site borrows the visual language of freight
   documents done properly: ruled lines, monospaced figures that align in a
   column, data plates like the ones riveted to a trailer, and lane markings
   as dividers. It reads as equipment rather than as marketing, which is the
   register the audience already trusts.
   ========================================================================= */

/* ---- ruled paper -------------------------------------------------------
   A manifest is ruled. This is that, at very low contrast: repeating
   hairlines on the sunk band, so the surface reads as a form rather than as
   an empty grey rectangle. Kept under 3% opacity so it never competes with
   text and never affects a contrast measurement. */
.section-sunk {
  background-image: repeating-linear-gradient(
    to bottom,
    transparent 0,
    transparent 39px,
    color-mix(in srgb, var(--ink) 4%, transparent) 39px,
    color-mix(in srgb, var(--ink) 4%, transparent) 40px
  );
  background-attachment: local;
}

/* ---- lane marking ------------------------------------------------------
   The divider between major bands. Dashes in the accent, proportioned like
   the centre line of a road: long mark, long gap. One rule, used at most
   twice on a page, so it stays a punctuation mark and not a pattern. */
.rule-road {
  height: 3px; border: 0; margin: 0;
  background-image: linear-gradient(
    to right,
    var(--accent) 0 44px,
    transparent 44px 88px
  );
  background-size: 88px 3px;
  opacity: .55;
}

/* ---- data plate --------------------------------------------------------
   For a figure that matters: mono, tabular, boxed in a hairline the way a
   serial plate is stamped on equipment. The label sits under the number in
   small caps, because that is the order the eye wants it in.

   Note the border is --rule-strong and the label --ink-soft, both already
   measured against every surface this can sit on. */
.plate {
  display: inline-flex; flex-direction: column; gap: var(--s1);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r);
  background: var(--surface);
}
.plate .fig {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: clamp(1.4rem, 1.1rem + 1.2vw, 2rem);
  font-weight: 500; line-height: 1; letter-spacing: -.02em;
  color: var(--ink);
}
.plate .cap {
  font-size: var(--t-micro); letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-soft); font-weight: 600;
}
/* On the inverting dark band the plate has to derive from currentColor, the
   same rule the audit established for .section-ink. */
.section-ink .plate { background: transparent; border-color: color-mix(in srgb, currentColor 26%, transparent); }
.section-ink .plate .fig { color: currentColor; }
.section-ink .plate .cap { color: color-mix(in srgb, currentColor 72%, transparent); }

/* ---- stamped heading ---------------------------------------------------
   A short accent rule above a section heading, sized to the cap height of the
   text below it. Cheaper than an eyebrow and it costs no words, which is why
   it can appear where the eyebrow budget will not stretch. */
.marked { position: relative; padding-top: var(--s5); }
.marked::before {
  content: ""; position: absolute; top: 0; left: 0;
  width: 44px; height: 3px; background: var(--accent);
}

/* ---- figure column -----------------------------------------------------
   Numbers in a vertical run align on their digits because they are tabular
   mono. A manifest reads down a column; so does this. */
.figures { display: grid; gap: 0; }
.figures > * {
  display: grid; grid-template-columns: 1fr auto; gap: var(--s4);
  align-items: baseline;
  padding-block: var(--s4);
  border-bottom: 1px solid var(--rule);
}
.figures > *:last-child { border-bottom: 0; }
.figures .fig {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-weight: 500; color: var(--accent-ink); white-space: nowrap;
}

/* ============================================================================
   PHOTOGRAPH LAYER
   ----------------------------------------------------------------------------
   Every rival dispatch site runs the same three stock trucks. This one runs the
   operator's own phone photos of his own loads, which is the only thing on the
   page a competitor cannot buy. So the photographs are treated as the argument,
   not as page furniture.

   The problem that comes free with real photography is that it was not shot to
   a brief. Eight frames, eight days, eight white balances, and a 2.3x spread in
   median brightness between the brightest and the darkest daylight frame. Left
   alone they read as a phone camera roll. Three things pull them together:

     the grade      one filter, on every fleet photograph, automatically
     the trim       a per-photo exposure nudge, capped, measured
     the frame      one radius and one hairline, so they sit as a set

   None of it crushes the picture into an effect. A carrier has to be able to
   look at frame 08 and recognise two utility vehicles chained to a gooseneck,
   because recognising it is the point.

   NOTE ON WHERE THIS LIVES. It is in site.css and not motion.css because only
   five of the eleven pages load motion.css, and three of the pages carrying
   photographs - services, carriers, about - are not among them. The same trap
   that left the theme-switch freeze broken on six pages.
   ------------------------------------------------------------------------ */

/* ---- the grade ---------------------------------------------------------
   Applied by source path, not by class, and that is deliberate. A treatment
   that a page has to remember to opt into is a treatment that will be
   inconsistent by the third page. Any file under /assets/img/fleet/ is graded
   wherever it appears, including in pages written after this one. */
.shot > img,
img[src*="/assets/img/fleet/"] {
  filter: var(--photo-grade) brightness(var(--photo-exposure));
}

/* ---- the exposure trim -------------------------------------------------
   Measured with tools/photo-probe.html: each file drawn into a canvas under
   the real grade string, pixels read back, median luminance taken. The trims
   below pull the seven daylight frames toward their own median. Capped at
   +-10%, because past that the correction starts inventing a photograph that
   was never taken.

   Result, measured before and after: the daylight spread closes from 2.28x to
   1.48x. Not uniform, and it should not be - these are real trucks in real
   weather, not a catalogue.

   37 IS DELIBERATELY EXEMPT. It is the night frame, the tarped flatbed under
   work lights, and it sits at a median of 0.02 against the set's 0.24. Pulling
   it up would need +148% and would turn the one photograph that proves
   "dispatch answers at 2am" into an ordinary afternoon. Night is the content.
   Leave it dark. */
img[src*="/fleet/01-"], [data-shot="01"] { --photo-exposure: 1.10; }
img[src*="/fleet/02-"], [data-shot="02"] { --photo-exposure: 0.91; }
img[src*="/fleet/08-"], [data-shot="08"] { --photo-exposure: 1.10; }
img[src*="/fleet/09-"], [data-shot="09"] { --photo-exposure: 1.00; }
img[src*="/fleet/23-"], [data-shot="23"] { --photo-exposure: 0.90; }
img[src*="/fleet/25-"], [data-shot="25"] { --photo-exposure: 0.90; }
img[src*="/fleet/26-"], [data-shot="26"] { --photo-exposure: 1.10; }
img[src*="/fleet/37-"], [data-shot="37"] { --photo-exposure: 1.00; }

/* ---- the frame ---------------------------------------------------------
   One radius, one hairline. The hairline is not decoration: on --paper a
   photograph with a bright edge bleeds into the page and loses its shape, and
   on the dark field the opposite happens. --photo-frame flips between the two
   so the edge is always the one the photograph needs. */
.shot {
  position: relative;
  display: grid;
  overflow: hidden;
  border-radius: var(--photo-radius);
  background: var(--surface-sunk);   /* holds the space before the file lands */
  isolation: isolate;
}
.shot > img {
  grid-area: 1 / 1;
  width: 100%; height: 100%;
  object-fit: cover;
}
.shot::after {
  content: ""; position: absolute; inset: 0; z-index: 3;
  border-radius: inherit; pointer-events: none;
  box-shadow: inset 0 0 0 1px var(--photo-frame);
}
/* Shapes. A phone photo cropped to a shape the layout wanted is still better
   than a stock photo that fitted. 23 of the 38 originals are portrait, which
   is why .shot-tall exists at all. */
.shot-wide  { aspect-ratio: 16 / 10; }
.shot-cine  { aspect-ratio: 21 / 9; }
.shot-tall  { aspect-ratio: 4 / 5; }
.shot-square{ aspect-ratio: 1 / 1; }

/* ---- text on a photograph ----------------------------------------------
   Two layers, and they have to arrive together. The veil is the flat dim over
   the whole frame; .shot-cap is the caption box that carries the rest. Their
   alphas compose to --scrim-total, which is the measured floor, so a caption
   that got the scrim and not the veil would sit at 4.4:1 instead of 4.9:1 -
   under AA, and by an amount nobody would ever catch by eye.

   So the veil is not left to a page agent to remember. :has() puts it there
   the moment a caption exists inside the frame. .shot-text stays as an
   explicit opt-in, for a frame that wants the veil without a caption and for
   the odd engine where :has() is missing; when both match there is still only
   one ::before, so it cannot double up.

   The caption hugs its own content rather than covering the frame, so the
   photograph stays open above it however tall the tile grows, and its scrim
   fades out over exactly --scrim-fade while its padding-top is at least that.
   No line of text can land in the fade. That is the whole trick: the floor is
   a property of the box, not a hope about where the copy ended up. */
.shot-text::before,
.shot:has(> .shot-cap)::before {
  content: ""; grid-area: 1 / 1; z-index: 1;
  background: rgba(var(--scrim-rgb), var(--photo-veil));
}
.shot-cap {
  grid-area: 1 / 1; z-index: 2;
  align-self: end;
  padding: calc(var(--scrim-fade) + var(--s4)) var(--s6) var(--s6);
  color: var(--on-photo);
  background: linear-gradient(to top,
    rgba(var(--scrim-rgb), var(--scrim-a)) 0,
    rgba(var(--scrim-rgb), var(--scrim-a)) calc(100% - var(--scrim-fade)),
    rgba(var(--scrim-rgb), 0) 100%);
}
.shot-cap > * + * { margin-top: var(--s2); }
.shot-cap h2, .shot-cap h3, .shot-cap h4 { color: var(--on-photo); }
.shot-cap p, .shot-cap .lead { color: var(--on-photo-soft); }
.shot-cap .eyebrow { color: var(--on-photo-soft); }

/* A credit or a location line under a photograph, outside the frame. Kept off
   the image on purpose: a caption that has to be scrimmed is a caption that is
   competing with the picture. */
.shot-note { font-size: var(--t-small); color: var(--ink-faint); margin-top: var(--s3); }

/* ============================================================================
   FULL-BLEED PHOTO BAND
   ----------------------------------------------------------------------------
   The site had no way to run a photograph at full width, which for a business
   whose only real differentiator is its own photography was the biggest gap in
   the system. One band, edge to edge, one headline, at most one call to action.

   Structure the page agent writes:

     <section class="photo-band">
       <img src="/assets/img/fleet/37-w1600.jpg"
            srcset="/assets/img/fleet/37-w640.jpg 640w,
                    /assets/img/fleet/37-w1000.jpg 1000w,
                    /assets/img/fleet/37-w1600.jpg 1600w"
            sizes="100vw" alt="..." width="1600" height="1200" loading="lazy">
       <div class="photo-band__body">
         <div class="photo-band__inner">
           <h2 class="display">Six words, no more</h2>
           <p class="lead">One supporting line.</p>
           <a class="btn btn-primary btn-lg" href="/contact">One action</a>
         </div>
       </div>
     </section>

   LCP. sizes="100vw" is mandatory here: without it the browser assumes the
   image is the full viewport width anyway and can still pick w1600 on a phone,
   which is 300KB of mobile data for a 375px screen. With the srcset above a
   phone takes w640. Use loading="lazy" unless the band is the first thing on
   the page, in which case drop lazy and add fetchpriority="high" - one or the
   other, never both.

   The body is full width so its scrim reaches both edges; the inner wrapper is
   what constrains the text. That is why there are two elements and not one.
   ------------------------------------------------------------------------ */
.photo-band {
  position: relative;
  display: grid;
  overflow: hidden;
  isolation: isolate;
  background: var(--surface-sunk);
  /* THE CLEAN STRIP. The body is bottom-anchored and carries the scrim, so the
     only undarkened photograph the reader gets is whatever sits above it. A
     min-height alone cannot guarantee any: measured at 375px with a headline,
     a lead and a button, the body came to 444px inside a 384px minimum and the
     clean strip was zero - a full-bleed photo band showing no photograph.

     Padding on the BAND fixes it structurally. The image is inset:0 on a
     positioned parent, so it covers the padding box, while the grid row that
     holds the body starts below the padding. The strip is therefore reserved
     at every width, by construction, whatever the copy does. Measured after:
     96px clean plus the 80px fade at 375, 160px plus the fade at 1280.

     min-height is now only a floor for a band whose body is a headline and
     nothing else. */
  padding-top: clamp(6rem, 20vw, 10rem);
  min-height: clamp(20rem, 10rem + 26vw, 34rem);
}
/* Taken OUT OF FLOW on purpose. Left as a grid item, the photograph's own
   aspect ratio sizes the row, and a 1600x1200 file in a 1265px band measured
   949px tall - a band that is really a full-page photograph with a sentence on
   it. Absolute, the band is sized by min-height and its content, and the
   photograph covers whatever that turns out to be, which is the only way a
   band works at both 375px and 1920px. */
.photo-band > img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  filter: var(--photo-grade) brightness(var(--photo-exposure));
  z-index: 0;
}
.photo-band::before {
  content: ""; grid-area: 1 / 1; z-index: 1;
  background: rgba(var(--scrim-rgb), var(--photo-veil));
}
.photo-band__body {
  grid-area: 1 / 1; z-index: 2;
  align-self: end;
  width: 100%;
  padding-block: calc(var(--scrim-fade) + var(--s5)) clamp(var(--s6), 4vw, var(--s8));
  padding-inline: var(--gutter);
  background: linear-gradient(to top,
    rgba(var(--scrim-rgb), var(--scrim-a)) 0,
    rgba(var(--scrim-rgb), var(--scrim-a)) calc(100% - var(--scrim-fade)),
    rgba(var(--scrim-rgb), 0) 100%);
}
.photo-band__inner {
  max-width: var(--wrap-wide);
  margin-inline: auto;
  color: var(--on-photo);
}
.photo-band__inner > * + * { margin-top: var(--s5); }
.photo-band__inner h1, .photo-band__inner h2, .photo-band__inner h3 { color: var(--on-photo); max-width: 20ch; }
.photo-band__inner .lead, .photo-band__inner p { color: var(--on-photo-soft); max-width: 46ch; }
.photo-band__inner .eyebrow { color: var(--on-photo-soft); }
/* The band is dark in BOTH themes, so a secondary button here takes the fixed
   white scale rather than --ink, which would vanish. Same rule the audit
   established for .section-ink, arrived at from the opposite direction: that
   band inverts and this one never does. */
.photo-band .btn-secondary { color: var(--on-photo); border-color: var(--on-dark-rule-strong); }
.photo-band .btn-secondary:hover { border-color: var(--on-photo); background: var(--on-dark-wash); }
/* Centred variant, for a band that closes a page rather than opening a
   section. Text still sits in the flat part of the scrim. */
.photo-band-center .photo-band__inner { text-align: center; }
.photo-band-center .photo-band__inner h1,
.photo-band-center .photo-band__inner h2,
.photo-band-center .photo-band__inner .lead,
.photo-band-center .photo-band__inner p { margin-inline: auto; }
.photo-band-center .row { justify-content: center; }
/* 375px: the band gets shorter, the gutter carries the inset, and the display
   size is already clamped. Nothing here can overflow because the section is a
   single grid cell at 100% width with overflow hidden. */
@media (max-width: 560px) {
  .photo-band { min-height: 22rem; }
  .photo-band__body { padding-block: calc(var(--scrim-fade) + var(--s3)) var(--s6); }
}

/* ============================================================================
   PHOTOGRAPH MOTION
   ----------------------------------------------------------------------------
   Four additions, each of which has to answer hierarchy, storytelling,
   feedback or state in one sentence, or it does not ship.
   ------------------------------------------------------------------------ */

/* STORYTELLING. The photograph settles into its frame as you reach it, so the
   frame reads as a window onto something rather than as a box with a picture
   in it. Scale only, clipped by the frame's overflow, so nothing reflows. */
.shot.reveal > img { transform: scale(1.05); }
.shot.reveal.in > img { transform: none; transition: transform 1s var(--ease); }

/* FEEDBACK. A photograph inside a link eases in under the cursor, so a tile
   that navigates is distinguishable from a tile that does not. Only ever fires
   on an actual link, which is why the selector starts at `a`. */
@media (hover: hover) {
  a .shot > img { transition: transform .6s var(--ease); }
  a:hover .shot > img { transform: scale(1.04); }
}

/* HIERARCHY. A row of items arrives in reading order instead of all at once,
   so the eye is given a first item. This replaces six data-delay attributes in
   the markup with one class on the parent; both still work. */
.reveal-seq > .reveal:nth-child(2) { transition-delay: .07s; }
.reveal-seq > .reveal:nth-child(3) { transition-delay: .14s; }
.reveal-seq > .reveal:nth-child(4) { transition-delay: .21s; }
.reveal-seq > .reveal:nth-child(5) { transition-delay: .28s; }
.reveal-seq > .reveal:nth-child(6) { transition-delay: .35s; }

/* STORYTELLING. The lane marking paints itself left to right as the section
   arrives, which is the one moment the road metaphor is doing work rather than
   sitting on the page being a dashed line. Overrides the .reveal transform
   because a rule that slides up is just a rule that slides up. */
.rule-road.reveal { opacity: .55; transform: scaleX(0); transform-origin: 0 50%; }
.rule-road.reveal.in { opacity: .55; transform: scaleX(1); transition: transform .9s var(--ease); }

@media (prefers-reduced-motion: reduce) {
  .shot.reveal > img, .shot.reveal.in > img,
  a .shot > img, a:hover .shot > img {
    transform: none !important; transition: none !important;
  }
  .reveal-seq > .reveal { transition-delay: 0s !important; }
  .rule-road.reveal, .rule-road.reveal.in {
    transform: none !important; transition: none !important; opacity: .55;
  }
}
