/* ============================================================================
   Prime Refine Group - 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); }
/* ---- emphasis band -------------------------------------------------------
   On the sister site this inverts: a near-black band on light paper. Inherited
   unchanged here it resolved to background:#eef2f6 - a BRIGHT WHITE band
   dropped into a dark page, ten times across five pages. That is not a strong
   accent, it is a page that looks broken, and it breaks the one-theme rule the
   rest of the site keeps.

   The band is crimson instead. It holds the same place in the hierarchy, it is
   unmistakably an interruption, and the page never leaves its own theme.
   White on it measures 5.99. */
.section-ink { background: var(--accent); color: var(--on-accent); }
/* .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. */
/* 72% was tuned against a white band. Composited on crimson it measures
   3.64:1 and FAILS AA. 88% gives 4.89. The mix must be re-measured whenever
   the band colour changes - that is the whole reason this line exists. */
.section-ink .lead, .section-ink .muted { color: color-mix(in srgb, currentColor 88%, transparent); }

main { min-height: 60vh; }

/* ---- type --------------------------------------------------------------
   The hierarchy is built from size, tracking and leading, because Barlow Condensed 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. */
/* EVERY HEADING IS CONDENSED AND UPPERCASE. This is the single change that
   separates the two sites fastest: the sister company sets mixed-case Archivo
   with negative tracking, an editorial voice. This is stencil-adjacent - the
   lettering on the side of a trailer, which is where the brand actually comes
   from. Condensed caps also buy back the width that positive tracking costs,
   so the headlines run larger without wrapping sooner. */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  text-transform: uppercase;
  line-height: var(--lh-heading);
  font-weight: 700;
  letter-spacing: var(--track-display);
  text-wrap: balance;
}
h1 { font-size: var(--t-h1); line-height: var(--lh-display); }
h2 { font-size: var(--t-h2); }
h3 { font-size: var(--t-h3); line-height: 1.16; letter-spacing: .02em; }
h4 { font-size: 1.05rem; letter-spacing: .06em; }
p  { text-wrap: pretty; }

.hero-title {
  font-family: var(--font-display);
  text-transform: uppercase;
  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(--on-accent); border-color: color-mix(in srgb, currentColor 55%, transparent); }
.section-ink .btn-secondary:hover { border-color: var(--on-accent); background: color-mix(in srgb, currentColor 8%, transparent); }
/* .btn-secondary had a band override and .btn-primary did not, so the PRIMARY
   call to action on the cobalt band was a cobalt fill on a cobalt ground with
   no visible boundary. Inverted fill is the usual answer: white plate, accent
   label, 5.91:1 either way round. */
.section-ink .btn-primary { background: var(--on-accent); color: var(--accent); }
.section-ink .btn-primary:hover { background: var(--on-accent); color: var(--accent-press); box-shadow: 0 0 0 5px color-mix(in srgb, var(--on-accent) 28%, transparent); }
/* The focus ring is --accent and this band is FILLED with --accent: 1.00:1,
   invisible to exactly the people who navigate by it. */
.section-ink :focus-visible { outline-color: var(--on-accent); }

.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;
    /* visibility, not just opacity: without it the four links stay in the tab
       order and in the accessibility tree while invisible, so a keyboard user
       tabs into links they cannot see and a screen reader announces a menu the
       toggle reports as collapsed. */
    visibility: hidden;
    transition: opacity var(--fast) var(--ease), transform var(--fast) var(--ease),
                visibility 0s linear var(--fast);
    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; visibility: visible; transition-delay: 0s; }
  .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 ------------------------------------------------------------ */
/* --ink is near-WHITE on this site, so inheriting this rule put light grey
   text on a near-white footer: 1.15:1, completely invisible. The footer is
   the darkest surface here, which is also where it belongs on a dark page. */
.footer { background: var(--sunk); color: var(--ink-soft); padding-block: var(--s8) var(--s6); }
/* Removed: :root[data-theme="dark"] .footer { background: var(--surface-sunk); }
   It was inherited from the sister site, where dark was the exception. Here
   dark is the DEFAULT, the base rule already paints --sunk, and --sunk follows
   the theme on its own. The attribute selector only ever fired for the theme
   toggle, so it did nothing a phone would see - which is exactly how the same
   line hid the sister site's whole footer on system dark. */
.footer a { color: var(--ink-soft); text-decoration: none; transition: color var(--fast) var(--ease); }
.footer a:hover { color: var(--ink); }
/* h3 AND h4: the column headings became h3 (the outline was jumping h2 -> h4
   on every page), but the baked copy still carries h4 until bake-partials.py is
   re-run. Matching both means neither state is ever unstyled - including for a
   visitor with scripting off, who only ever sees the baked markup. */
.footer h3, .footer h4 { color: var(--ink); font-size: var(--t-micro); letter-spacing: .14em; text-transform: uppercase; font-weight: 600; margin-bottom: var(--s4); }
/* This comment used to say the footer was dark in BOTH themes and therefore
   safe to paint from the fixed on-dark scale. That was true of the sister
   site. Here the footer is --sunk, which FOLLOWS the theme: #0a0d11 in dark,
   #e7ebef in light. Fixed light text on it measured 1.14:1 in light mode.
   Everything in the footer now uses the theme-aware ink scale, which resolves
   to 9.16 in dark and 7.18 in light. */
/* The company name in the footer. This was an INLINE style pinning
   --on-dark, a fixed #ffffff, carried over from the sister site whose footer
   was dark in both themes. This one is --sunk and follows the theme, so in
   light mode it rendered white on #e7ebef - 1.20:1, invisible, on all eleven
   pages. Being inline it also outranked every rule in this file, which is why
   the surrounding footer colour fixes never touched it.
   Measured now: 17.31:1 on the dark footer, 15.78:1 on the light one. */
.footer .brand-name { color: var(--ink); }
.footer .brand-sub { color: var(--ink-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);
  /* --rule follows the theme; --on-dark-rule is fixed white and
     disappears on a light footer. */
  border-top: 1px solid var(--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;
}
/* Dark is the default here, so the DEFAULT arrow must be the light-stroked
   one. The sister site had this behind :root[data-theme="dark"] alone, which
   only matches after the theme toggle is pressed - a phone on system dark kept
   the wrong arrow at 2.35:1. Inverted and given both halves. */
:root[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"); }
@media (prefers-color-scheme: light) {
  :root:not([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"); }
}

.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. */
/* CONTENT IS VISIBLE BY DEFAULT. Nothing on this page waits for JavaScript
   to become readable.

   It used to: .reveal started clipped to zero width and only opened when an
   IntersectionObserver in a DEFERRED script added .in. Deferred means after
   the HTML is parsed AND the file has crossed the network. Measured on the
   live site from Karachi: DOMContentLoaded at 953ms, a 4KB script taking
   462ms of that purely in latency. So every revealed section - which is most
   of the page - was blank for about a second, and the safety net that catches
   a failed observer sat at FOUR seconds. That is the delay that was reported.

   The wipe is now driven by the browser's own scroll timeline. Where that is
   supported the animation is identical and costs no JavaScript at all; where
   it is not, the content is simply there. Either way nothing is ever hidden
   waiting for a network request to finish. */
/* Defined HERE, not in motion.css. .reveal is a site.css rule and every page
   loads site.css, but about, privacy, terms and sms-terms do NOT load
   motion.css - so a keyframe referenced from here and defined there is
   undefined on four of eleven pages. That is the same split that has bitten
   this codebase before, written into its own comments: anything that must
   work on every page belongs in site.css. */
@keyframes wipe-in {
  /* The inset is expanded 3rem on the three edges the wipe does not travel
     along, and that is not cosmetic.

     clip-path: inset() clips to the element's BORDER BOX. Several components
     put decoration deliberately OUTSIDE that box: the numbered steps on the
     home page sit their ::before counter at top:-.85rem so it straddles the
     rule above them. With a plain inset(0 0 0 0) end state - which is exactly
     the border box - the top half of "01", "02" and "03" was cut off
     permanently, not just while animating. The old fade-up used opacity and
     transform, which do not clip, so nothing revealed this until the wipe
     replaced it.

     Only the RIGHT edge travels. Everything else is held well outside the box
     so no component can lose its overhang. */
  from { clip-path: inset(-3rem 100% -3rem -3rem); }
  to   { clip-path: inset(-3rem -3rem -3rem -3rem); }
}

.reveal { }

@supports (animation-timeline: view()) {
  .reveal {
    animation: wipe-in .62s var(--ease) both;
    animation-timeline: view();
    /* Finish early. The element should be fully open well before it reaches
       the middle of the screen, not still opening as it is read. */
    animation-range: entry 0% cover 18%;
  }
}

/* .in is kept as a no-op so any page or script still adding it is harmless. */
.reveal.in { clip-path: none; }
@media (prefers-reduced-motion: reduce) {
    /* clip-path MUST be reset here. The reveal no longer uses opacity or
     transform, so resetting only those would leave every revealed element
     clipped to zero width - the content would be INVISIBLE for exactly the
     people who asked for less motion. */
  .reveal, .reveal.in {
    opacity: 1; transform: none; transition: none;
    clip-path: none !important;
    /* Cancels the scroll-driven wipe as well, not just the old transition. */
    animation: none !important;
  }
}

/* ---- 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;
}

/* ---- band divider ------------------------------------------------------
   The sister site draws road lane markings here - dashes in the accent, long
   mark, long gap. Copying that would be copying its most recognisable piece
   of furniture.

   This is a single hard rule that runs the full width and steps up to a short
   crimson tab at the left, like the edge of a stamped steel plate. Solid, not
   dashed; structural, not decorative. */
.rule-road {
  position: relative;
  height: 1px; border: 0; margin: 0;
  background: var(--rule);
  overflow: visible;
}
.rule-road::before {
  content: ""; position: absolute; left: 0; top: -2px;
  width: 88px; height: 5px; background: var(--accent);
}

/* ---- 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); }

/* ---- placard heading ---------------------------------------------------
   The section-header device, used twelve times, so it carries more of the
   page's character than anything else here.

   The sister site stamps a short accent rule ABOVE the heading. This one sets
   a solid crimson SLAB down its left edge instead, the way a placard is bolted
   to a bulkhead. Horizontal punctuation against vertical structure: the two
   read as different systems at a glance, which is the whole point.

   The slab is set in em so it tracks the heading it marks rather than being
   one fixed width at every level. */
.marked { position: relative; padding-left: .62em; }
.marked::before {
  content: ""; position: absolute; inset: .1em auto .12em 0;
  width: .16em; min-width: 5px; background: var(--accent);
}
/* Over a photograph the crimson loses against a bright frame, so it goes
   white and lets the scrim carry the separation. */
.shot-cap .marked::before,
.section-ink .marked::before { background: currentColor; }

/* ---- 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. */
/* These were a PAIR: .reveal set the start state and .in, added by an
   IntersectionObserver, released it. That observer was removed when the reveal
   moved to a CSS scroll timeline, and nothing adds .in any more - so the start
   state became the only state. Every framed photograph was stuck 5% over-scaled
   and the road divider below was stuck at zero width.
   The settle now rides the scroll timeline like everything else, and degrades
   to "just visible" where that is unsupported. */
@supports (animation-timeline: view()) {
  .shot.reveal > img {
    animation: shot-settle .9s var(--ease) both;
    animation-timeline: view();
    animation-range: entry 0% cover 22%;
  }
}
@keyframes shot-settle { from { transform: scale(1.05); } to { transform: none; } }

/* 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. */
/* Same stranding as .shot.reveal above. Without .in this painted itself to
   zero width and stayed there, so the About page's one lane marking was
   invisible. The default is now the FINISHED state - visible whatever happens -
   and the draw is layered on top only where the scroll timeline exists. */
.rule-road.reveal { opacity: .55; transform-origin: 0 50%; }
@supports (animation-timeline: view()) {
  .rule-road.reveal {
    animation: road-draw .9s var(--ease) both;
    animation-timeline: view();
    animation-range: entry 0% cover 20%;
  }
}
@keyframes road-draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }

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

/* ---- legal documents: sentence case, deliberately ------------------------
   The uppercase display treatment above is right for a page someone scans and
   wrong for one they have to READ. "17. Governing Law and Dispute Resolution"
   set in condensed caps is measurably slower to read: all-caps removes the
   word-shape cues a reader uses to skim, and these are the pages where a
   carrier is checking what they are agreeing to.

   So inside .doc the headings keep the display face - the page still belongs
   to this site - but drop the uppercase and take normal tracking back. */
.doc h1, .doc h2, .doc h3, .doc h4 {
  text-transform: none;
  letter-spacing: .005em;
}
.doc h2, .doc h3 { line-height: 1.2; }

/* ---- buttons: condensed, uppercase, square -------------------------------
   The label is the one piece of type a visitor is guaranteed to read, so it
   carries the same voice as the headings. Square by system rule, so a button
   is a stamped plate rather than a pill. */
.btn {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: .1em;
  font-weight: 600;
}

/* The nav wordmark sets in the display face too, or it reads as a different
   company from the one in the headlines. */
.brand-name {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: .04em;
  font-weight: 700;
  font-size: 1.2rem;
}
.brand-sub { letter-spacing: var(--track-wide); }
.brand-mark { border-radius: 0; }

/* ============================================================================
   ============  PRIME REFINE IDENTITY LAYER  =================================
   ============================================================================
   Everything above this line is the SHARED skeleton the three sister sites
   inherited: photo hero, proof strip, stacked photo rows, card grid. Repainting
   that skeleton is exactly why Joseph Services still looks like Backup
   Logistics - the tokens changed and the structure did not, so you recognise it
   from ten feet away.

   This block changes the STRUCTURE. It is appended rather than merged so the
   divergence is one contiguous, deletable thing and nobody has to guess which
   rules are the shared system and which belong to this site.

   Four moves, and they are the whole identity:

     1  PILL. Anything pressable is fully round. Backup is 4/8px, Joseph is 0.
     2  TWO-TONE. Whole sections invert to near-black at full bleed, so the page
        alternates grounds instead of sitting on one.
     3  TYPE-LED HERO. No hero photograph at all. Both sister sites open on a
        full-bleed photo with the headline over it; this opens on words beside a
        data panel, which is the single biggest structural difference.
     4  THE BOARD. A departure-board component, mono and ruled, with one live
        row. It is the thing this site has that the other two do not.
   ========================================================================= */

/* The near-black used by every inverted surface. FIXED in both themes, unlike
   --ink which flips. Anything painted with this may safely use the --on-dark-*
   scale; anything painted with --ink may not. Conflating those two is the bug
   that shipped twice on the sister sites. */
:root { --ink-fixed: #12151A; }

/* ---- 1. PILL ------------------------------------------------------------
   If you can press it, it is round. One rule, no exceptions, nothing to
   remember. Form controls stay at --r so "control you type into" and "control
   you press" stay visually distinct. */
.btn { border-radius: var(--r-pill); padding: 0.85rem 1.6rem; letter-spacing: var(--track-snug); }
.btn-lg { padding: 1.05rem 2rem; }
.theme-btn, .nav-toggle { border-radius: var(--r-pill); }

/* No sheen and no fill-from-left: the primary lifts on a RING, which is the
   same elevation language the panels use, so the whole site has one idea of
   depth instead of three. */
.btn-primary { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 45%, transparent); }
@media (hover: hover) {
  .btn-primary:hover { background: var(--accent-press); box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent) 22%, transparent); }
  .btn-secondary:hover { box-shadow: 0 0 0 5px color-mix(in srgb, var(--ink) 8%, transparent); }
}
.btn:active { transform: translateY(1px); }

/* ---- 2. TWO-TONE -------------------------------------------------------
   .section-dark is a full-bleed near-black band. Unlike .section-ink it does
   NOT follow the theme - it is #12151A in both - which is exactly why the
   fixed --on-dark-* scale is safe inside it. The two components are named
   differently rather than one being a modifier of the other, so the difference
   cannot be missed. */
.section-dark {
  background: var(--ink-fixed);
  color: var(--on-dark);
  padding-block: var(--section-y);
}
.section-dark h1, .section-dark h2, .section-dark h3, .section-dark h4 { color: var(--on-dark); }
.section-dark p, .section-dark .lead { color: var(--on-dark-muted); }
.section-dark .faint, .section-dark .muted { color: var(--on-dark-faint); }
.section-dark a:not(.btn) { color: var(--on-dark); }
.section-dark .btn-secondary { color: var(--on-dark); border-color: var(--on-dark-rule-strong); }
.section-dark .btn-secondary:hover { border-color: var(--on-dark); background: var(--on-dark-wash); }

/* ---- 3. TYPE-LED HERO --------------------------------------------------
   .lede replaces the full-bleed photograph hero. Asymmetric on purpose: the
   headline column is wider than the panel beside it and neither is centred,
   because a centred headline over a photograph is the exact shape both sister
   sites open with.

   Height is content-driven, never 100vh. A full-viewport opener tells the
   reader there is nothing underneath it, and the thing underneath is the proof
   this audience is actually checking. */
.lede {
  padding-block: clamp(2.5rem, 1.5rem + 5vw, 5.5rem) clamp(2.5rem, 1.5rem + 4vw, 4.5rem);
  border-bottom: 1px solid var(--rule);
}
.lede-in {
  display: grid; grid-template-columns: 1.25fr 0.75fr;
  gap: clamp(2rem, 4vw, 4.5rem); align-items: center;
}
.lede-copy > * + * { margin-top: var(--s5); }
.lede h1 {
  font-size: var(--t-display);
  line-height: var(--lh-display);
  letter-spacing: var(--track-display);
  max-width: 15ch;
  text-wrap: balance;
}
/* The accent sits UNDER the word as a thick rule rather than colouring it, so
   the type keeps --ink and its 16.26:1. Setting the word in --accent would
   measure 5.25 - passing, but visibly weaker beside the rest of the line. */
/* The accent under the word, not on it: setting "authority" in cobalt would
   drop it from 16.26:1 to 5.25:1 - passing, but visibly weaker than the rest
   of the line.

   THIS WAS DRAWN AS A BACKGROUND GRADIENT AT 88% OF THE BOX AND IT LANDED IN
   THE NEXT LINE. Measured live at 375px: the rule painted at y276-282 while
   the following line box started at y272, so it read as a stray rule floating
   between two lines rather than as an underline. The cause is this site's own
   display leading - --lh-display is 0.95, so the line box is SHORTER than the
   font and there is no room beneath the baseline for anything.

   Two changes. The rule is now text-decoration, which anchors to the BASELINE
   rather than to a percentage of a box, and which also survives the word
   wrapping across two lines - the gradient would have stretched one rule over
   both fragments. And the lede headline gets slightly looser leading than the
   global display value, purely to make room for it. 1.06 is the smallest value
   that clears; it is still tighter than either sister site. */
.lede h1 { line-height: 1.06; }
.lede h1 .hl {
  text-decoration-line: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: .1em;
  text-underline-offset: .06em;
  text-decoration-skip-ink: none;
}
.lede .lead { max-width: 46ch; color: var(--ink-soft); font-size: var(--t-lead); line-height: var(--lh-lead); }
.lede .row-cta { margin-top: var(--s6); }
@media (max-width: 940px) {
  .lede-in { grid-template-columns: 1fr; gap: var(--s7); }
  .lede h1 { max-width: 18ch; }
}

/* ---- 4. THE BOARD ------------------------------------------------------
   A departure board: mono, ruled, tabular - the register this audience reads
   numbers in all day - with exactly one live row marked in --signal. It is the
   component neither sister site has, and it is what lets the hero work without
   a photograph.

   Every row states something TRUE and checkable. There is no invented
   telemetry here; these are the service's actual, stable facts. */
.board {
  background: var(--ink-fixed);
  color: var(--on-dark);
  border-radius: var(--r-lg);
  padding: var(--s5);
  box-shadow: var(--lift-3);
}
.board-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s4);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--on-dark-faint);
  padding-bottom: var(--s4);
  border-bottom: 1px solid var(--on-dark-rule);
}
.board-live { display: inline-flex; align-items: center; gap: .5rem; color: var(--signal); }
.board-live::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: var(--signal);
  animation: board-pulse 2.4s var(--ease-out) infinite;
}
@keyframes board-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--signal) 55%, transparent); }
  70%  { box-shadow: 0 0 0 7px color-mix(in srgb, var(--signal) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--signal) 0%, transparent); }
}
.board-rows { list-style: none; padding: 0; margin: 0; }
.board-rows li {
  display: grid; grid-template-columns: 1fr auto;
  gap: var(--s4); align-items: baseline;
  padding-block: var(--s3);
  border-bottom: 1px solid var(--on-dark-rule);
  font-size: var(--t-small);
}
.board-rows li:last-child { border-bottom: 0; }
.board-k { color: var(--on-dark-muted); }
.board-v {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  color: var(--on-dark); letter-spacing: -.02em;
}
.board-rows li.is-live .board-v { color: var(--signal); }

/* ---- BENTO -------------------------------------------------------------
   Mixed-size tiles, which is a different shape from the equal three-across
   card row both sister sites use. One tile spans two columns so the grid is
   visibly irregular; the irregularity is the point. */
.tiles {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--s4); margin-top: var(--s7);
}
.tile {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  padding: var(--s5);
  display: flex; flex-direction: column; gap: var(--s3);
  transition: border-color var(--fast) var(--ease), transform var(--med) var(--ease);
}
@media (hover: hover) { .tile:hover { border-color: var(--accent); transform: translateY(-2px); } }
.tile h3 { font-size: var(--t-h3); letter-spacing: var(--track-snug); }
.tile p { color: var(--ink-soft); font-size: var(--t-small); }
.tile-wide { grid-column: span 2; }
@media (max-width: 640px) { .tile-wide { grid-column: span 1; } }
.tile .tag {
  align-self: flex-start;
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--accent-ink);
  background: var(--accent-wash);
  border-radius: var(--r-pill);
  padding: .25rem .7rem;
}
.section-dark .tile { background: var(--on-dark-wash); border-color: var(--on-dark-rule); }
.section-dark .tile h3 { color: var(--on-dark); }
.section-dark .tile p { color: var(--on-dark-muted); }
.section-dark .tile .tag { color: var(--ink-fixed); background: var(--signal); }

/* ---- CHIPS -------------------------------------------------------------
   Replaces the equipment strip's hard grid of plates. Same content, different
   shape - and it applies the site's one shape rule to a non-pressable element,
   which ties the strip to the buttons. */
.chips { display: flex; flex-wrap: wrap; gap: var(--s2); }
.chip {
  font-family: var(--font-mono); font-size: var(--t-micro);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  border: 1px solid var(--rule-strong); border-radius: var(--r-pill);
  padding: .4rem .9rem; color: var(--ink-soft);
}
.section-dark .chip { border-color: var(--on-dark-rule-strong); color: var(--on-dark-muted); }

/* ---- FAQ ---------------------------------------------------------------
   Native <details>. No script, works with JavaScript off, and the marker is a
   plus that becomes a minus on one transform. */
.faq { border-top: 1px solid var(--rule); margin-top: var(--s6); }
.faq details { border-bottom: 1px solid var(--rule); }
.faq summary {
  list-style: none; cursor: pointer;
  display: grid; grid-template-columns: 1fr 16px; gap: var(--s4);
  align-items: center;
  padding-block: var(--s4); min-height: 44px;
  font-weight: 600; font-size: var(--t-body);
  letter-spacing: var(--track-snug);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: ""; grid-column: 2; grid-row: 1;
  width: 14px; height: 2px; background: var(--ink); justify-self: center;
}
.faq summary::before {
  content: ""; grid-column: 2; grid-row: 1;
  width: 2px; height: 14px; background: var(--ink);
  justify-self: center; align-self: center;
  transition: transform var(--med) var(--ease), opacity var(--fast) linear;
}
.faq details[open] summary::before { transform: scaleY(0); opacity: 0; }
.faq summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--r); }
.faq .faq-a { padding-bottom: var(--s5); color: var(--ink-soft); max-width: var(--measure); }
.section-dark .faq, .section-dark .faq details { border-color: var(--on-dark-rule); }
.section-dark .faq summary::after, .section-dark .faq summary::before { background: var(--on-dark); }
.section-dark .faq .faq-a { color: var(--on-dark-muted); }

/* ---- STEPS -------------------------------------------------------------
   Three steps as a horizontal ruled track rather than three cards. The number
   is a pill on the rule, so the sequence reads as a route with stops on it. */
.route { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s5); margin-top: var(--s7); counter-reset: step; }
.route-stop { border-top: 2px solid var(--rule-strong); padding-top: var(--s5); position: relative; }
.route-stop::before {
  counter-increment: step; content: "0" counter(step);
  position: absolute; top: -.85rem; left: 0;
  font-family: var(--font-mono); font-size: var(--t-micro); font-weight: 700;
  background: var(--accent); color: var(--on-accent);
  border-radius: var(--r-pill); padding: .2rem .7rem;
}
.route-stop h3 { font-size: var(--t-h3); margin-bottom: var(--s3); letter-spacing: var(--track-snug); }
.route-stop p { color: var(--ink-soft); font-size: var(--t-small); }
.section-dark .route-stop { border-top-color: var(--on-dark-rule-strong); }
.section-dark .route-stop::before { background: var(--signal); color: var(--ink-fixed); }
.section-dark .route-stop p { color: var(--on-dark-muted); }
@media (max-width: 820px) { .route { grid-template-columns: 1fr; gap: var(--s6); } }

/* ---- STAT ROW ----------------------------------------------------------
   Facts in mono at display size. Replaces the sister sites' three-cell proof
   strip with something that reads as a readout rather than as three boxes. */
.readout { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: var(--s5); }
.readout-item .readout-v {
  display: block;
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: clamp(1.9rem, 1.3rem + 2.4vw, 3rem);
  line-height: 1; letter-spacing: -.04em; color: var(--ink);
}
.readout-item .readout-k {
  display: block; margin-top: var(--s3);
  font-size: var(--t-small); color: var(--ink-soft);
}
.section-dark .readout-item .readout-v { color: var(--on-dark); }
.section-dark .readout-item .readout-k { color: var(--on-dark-muted); }

/* ---- accent text on the fixed dark band --------------------------------
   --accent-ink is tuned for the bone ground; on #12151A it measures 2.32 and
   is unreadable. Anything that paints text with the accent therefore needs a
   lifted value inside .section-dark. Found by listing every rule that sets
   color to an accent token and then checking which of those classes actually
   appear inside a dark band in the markup - a token-level audit does not
   catch it, because both tokens are individually fine. */
.section-dark .eyebrow,
.section-dark .link,
.section-dark .req { color: var(--accent-on-dark); }
.section-dark .link {
  background-image: linear-gradient(var(--accent-on-dark), var(--accent-on-dark));
}

/* The focus ring is --accent, and .section-ink is FILLED with --accent, so
   inside that band the ring was cobalt on cobalt: 1.00:1, invisible to exactly
   the users who depend on it. On any accent-filled or fixed-dark surface the
   ring switches to the on-dark colour instead. */
.section-ink :focus-visible,
.section-dark :focus-visible,
.btn-primary:focus-visible,
.board :focus-visible { outline-color: var(--on-dark); }

/* ---- the brand lockup --------------------------------------------------
   REPORTED: the PR square looked to be sagging against the wordmark, in the
   header and the footer, on desktop and on mobile.

   MEASURED, and the cause is not what it looks like. .brand is align-items:
   center, so the 34px mark was centred on the TEXT BLOCK and sat within 2.8px
   of that block's centre - the CSS was doing exactly what it said. But it was
   18.9px below the WORDMARK'S cap-height centre, and the wordmark is what the
   eye aligns a mark to. A two-line block whose second line is a 0.66rem label
   has its visual mass much higher than its geometric centre.

   The mark's BOTTOM was already right: 53.0 against the sub's baseline at
   53.4, half a pixel out. The whole error was at the top, where the square
   began 8.6px BELOW the wordmark's cap. So it was not mispositioned, it was
   too SHORT - it floated inside the text block instead of bracketing it.

   The optical span a mark should cover here, wordmark cap-top to sub
   baseline, measures 43.1px. At 43px, centred, the square lands within about
   1.4px of that span at both ends, which is under the threshold anything is
   visible at, and it stays correct if the type scale changes because it is
   sized from the same rem scale the type is.

   The nav's own mobile overrides below are NOT this: there the sub is hidden,
   so the lockup is a mark beside a single line and a smaller square is right. */
/* 33px, and the story of this number is worth keeping. It was 34px, and the
   square looked short against the wordmark - so it was sized up to 43px to
   match the optical span measured at the time. That measurement was taken
   while the two brand lines were being held apart by an inherited line-height
   strut (see below), which inflated the span it was measured against. With the
   strut collapsed the span is 32.7px, so 43px overhung 6.3px above the
   wordmark cap and 4px below the sub baseline. The ORIGINAL 34px was very
   nearly right; the gap was the defect, not the mark.

   Measure the thing you are matching before you resize the thing that matches
   it. */
.brand-mark { width: 33px; height: 33px; font-size: 0.9rem; }
/* And then 4px up. align-items: center centres the mark on the text block's
   BOX, and that box carries descender space below the sub's baseline that no
   glyph ever occupies - so the optical centre sits above the geometric one.
   Measured at 1280x900 after the resize: the mark landed +4.1px at the top AND
   +4.1px at the bottom. Identical offsets at both ends is the signature of a
   pure translation, which is why this is a nudge and not a size change.

   It is a transform rather than a margin so it cannot affect the flex layout
   or the nav's height. RE-MEASURE IT if the type scale or the sub's size
   changes - it is derived from those, not chosen. */
.brand-mark { transform: translateY(-3px); }

/* ---- the gap between the two brand lines -------------------------------
   REPORTED as loose, and the margin was the wrong suspect. The two lines are
   split by a <br> inside an unclassed <span>, and that span inherits BODY's
   line-height - 1.62, or 27.54px. A line box is sized by max(its own
   line-height, the parent's STRUT), so the strut won both times: the wordmark
   asks for 19.2px and rendered a 25px box, the sub asks for 10.56px and
   rendered 13px. Nearly 9px of the gap was inherited leading that neither
   element set and nothing could see.

   Collapsing the strut on the column lets each line size itself, which is
   what the two line-height declarations were trying to do all along. The
   sub's 3px margin then means 3px rather than 3px on top of the leading. */
.brand > span:not(.brand-mark) { line-height: 1; }

/* ---- nav: the brand must never overflow it -----------------------------
   REPORTED FROM A REAL PHONE, 2026-08-06. The wordmark was clipped along its
   top edge on mobile.

   Cause: .nav had a FIXED `height: var(--nav-h)`, and .brand-name is uppercase
   display type at 1.2rem with positive tracking. The rebrand made the wordmark
   six characters longer than the site this component was built for - "PRIME
   REFINE GROUP" against "JOSEPH SERVICES" - so on a narrow screen it wrapped to
   two lines, and with .brand-sub beneath it the brand block grew taller than
   72px. A fixed-height box does not grow, so the overflow was simply cut off.

   Two changes and the first is the one that matters. A fixed height on a box
   whose content can wrap is a clipping bug waiting to happen, so it becomes a
   MIN-height: whatever else changes later - a longer name, a bigger font, a
   translated string - the nav now grows instead of eating the content. The
   second change stops the wrap happening at all at the sizes it happened at.

   .brand-sub is hidden only INSIDE THE NAV. It also renders in the footer,
   where there is room for it and where hiding it would lose real information. */
.nav { height: auto; min-height: var(--nav-h); }
.nav-in { height: auto; min-height: var(--nav-h); }
.brand-name { white-space: nowrap; }

@media (max-width: 640px) {
  .nav .brand-name { font-size: 0.98rem; letter-spacing: .02em; }
  .nav .brand-sub { display: none; }
  .nav .brand-mark { width: 30px; height: 30px; font-size: .8rem; transform: none; }
}
@media (max-width: 380px) {
  .nav .brand-name { font-size: 0.86rem; letter-spacing: .01em; }
}
/* 320px is the narrowest phone still in real use (iPhone SE 1st gen, older
   Androids). MEASURED on the live page: the wordmark renders 10.1px wide per
   1px of font-size, so at 0.86rem it is 139px against 125px of available space
   - and with white-space: nowrap that is a horizontal scrollbar rather than a
   wrap. 0.72rem gives 116px, clear by 9px.

   The breakpoints above were checked the same way at 320/360/375/381/414/480/
   560/639/641/700/768/900. Guessing this would have shipped the overflow. */
@media (max-width: 360px) {
  .nav .brand-name { font-size: 0.72rem; letter-spacing: 0; }
  .nav .brand-mark { width: 26px; height: 26px; font-size: .72rem; transform: none; }
}
