/* ============================================================
   CRONIMET TERMINALS — Main Stylesheet
   Lava New Media | 2026
   ============================================================ */

/* ------------------------------------------------------------
   CUSTOM PROPERTIES
------------------------------------------------------------ */
:root {
  --blue:       #041a3d;
  --green:      #52ae32;
  --metal:      #91a2ac;
  --dark-metal: #041a3d;
  --red:        #ff2800;
  --white:      #ffffff;
  --off-white:  #f4f5f6;

  --grey-10: #e0e8ee;
  --grey-30: #bbc7cf;
  --grey-50: #91a2ac;
  --grey-70: #677a84;
  --grey-90: #40545e;

  --gradient: linear-gradient(135deg, #52ae32 0%, #41b8d5 100%);

  --nav-h: 72px;
  --page-pad: 60px;       /* horizontal breathing room inside boxes / footer */
  --content-max: 1400px;  /* the content grid; with --page-pad it defines THE left content edge */
  --box-overlap: 80px;    /* how much box bleeds past section edge */
  --split-img: 57vw;      /* image column width */

  /* THE COPY EDGE (standing rule): every piece of page copy on desktop
     starts on this one shared left edge. The reference is the approved
     copy position of the "Backed by the CRONIMET Group" box: a 120px
     floor from the viewport edge until the --content-max grid centers
     past it (viewports over 1640px), then the centered grid's left
     line. The header (logo + nav) is the OTHER permitted edge and
     keeps its --page-pad gutter; do not derive it from this token.
     Use the token as horizontal padding on full-width bands, as
     margin-left on full-bleed copy blocks (hero, page header), or as a
     grid spacer column (split sections). The 100% inside it must
     resolve against a full-viewport-width containing block; never use
     it inside an already-padded container. Backgrounds and images are
     exempt (they may bleed or inset freely), but text sitting on them
     is not. The floor steps down to --page-pad at <=1100px so tablet
     and mobile layouts stay exactly as they are. */
  --copy-floor: 120px;
  --copy-edge: max(var(--copy-floor), calc((100% - var(--content-max)) / 2));

  /* THE ARROW SIZE (standing rule, 2026-08-07). The green arrow renders
     at ONE size everywhere on the site: every component, every
     breakpoint. It does NOT inherit the size of the label beside it,
     so a 12px commodities label and a 16px service row show the same
     glyph. Before this token the arrow rendered at six different sizes
     (16 / 14.08 / 13.12 / 12.48 / 12 / 10.88px); 16px is the largest
     and the approved one, the service-row size. Set it here, never in
     a component. Arial's arrow is exactly 1em wide, so the glyph
     occupies 16px of advance in every context.
     --arrow-gap is the matching space to the adjacent text: the old
     em-derived gaps (0.5em of a 12px label = 6px) read pinched once
     the glyph grew, so the gap is fixed too. */
  --arrow-size: 16px;
  --arrow-gap: 8px;
}

/* ------------------------------------------------------------
   RESET
------------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html {
  font-size: 16px;
  scroll-behavior: smooth;
  -webkit-font-smoothing: antialiased;
}

/* LOCKED (2026-08-06): horizontal scroll is impossible, everywhere,
   forever. clip, not hidden — clip creates no scroll container and
   does not break position: sticky. This is a safety net on top of
   source fixes: no element may ever render wider than the viewport,
   and any overflow found in QA gets fixed at the source, never by
   leaning on this rule. */
html, body {
  overflow-x: clip;
}

/* THE CANVAS BEYOND THE PAGE IS NAVY (2026-08-08, Ed). Everything
   outside the page box takes its colour from html/body: the strip
   behind the iOS status bar, the overscroll rubber band at both ends,
   and the browser chrome tint alongside the theme-color meta in
   header.php. That was off-white, which is why the very top of the
   screen on a real iPhone read white above the navy header.
   The off-white that shows BETWEEN sections is a different job, and it
   moves to #content below: it is the page canvas, not the void around
   it. Every section lives inside <main id="content">, and the header
   and footer outside it are navy already, so the two surfaces meet
   exactly where they always did. */
html {
  background: var(--blue);
}

body {
  font-family: 'Source Sans 3', sans-serif;
  background: var(--blue);
  color: var(--white);
}

#content {
  background: var(--off-white);   /* ← KEY: off-white shows between sections */
}

img, video { display: block; max-width: 100%; }
address { font-style: normal; }
ul { list-style: none; }
a { color: inherit; text-decoration: none; }

/* Hanging indent for native list markers (2026-09-10, Ed, standing
   rule): any ul or ol that shows real markers keeps them OUTSIDE the
   text box, in the gutter the list padding opens, so a wrapped item
   aligns every line with its own first line and never returns under
   the marker. The universal reset above zeroes list padding, so a
   list that turns markers on MUST restore it or outside markers
   overflow the list box. No theme component uses ol, and no
   component ul carries .wp-block-list, so these reach exactly the
   native lists an editor can drop in (core/list is whitelisted) and
   nothing else; the legal shells restate them with their own rhythm.
   Nav and footer menus are navigation, keep list-style: none, and
   stay out of the hanging-list rule. */
ol,
ul.wp-block-list {
  list-style-position: outside;
  padding-left: 1.4em;
}
ul.wp-block-list { list-style-type: disc; }

/* ------------------------------------------------------------
   TYPOGRAPHY HELPERS
------------------------------------------------------------ */
.accent { color: var(--green); }

.label-sm {
  display: flex;
  align-items: baseline; /* arrow sits on the FIRST line when the label wraps; standing rule for all arrow+text rows */
  gap: var(--arrow-gap);
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 600;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--metal);
  margin-bottom: 0.75rem;
}

.label-sm--light { color: var(--metal); }

/* The arrow carries its own size (see --arrow-size): never inherited
   from the label beside it, so it is identical in every component. */
.arrow {
  color: var(--green);
  font-weight: 700;
  font-size: var(--arrow-size);
  /* Labels track out to 0.12 to 0.14em and the arrow inherits that as a
     trailing letter-space, which quietly added 1.5 to 2.2px on top of
     the row gap and made the gap different in every component. Zero it
     so --arrow-gap IS the spacing. With the gap at 8px this reproduces
     the pre-16px spacing of the label rows almost exactly. */
  letter-spacing: 0;
}

.headline-rule {
  width: 40px;
  height: 1px;
  background: var(--green);
  margin: 1.25rem 0 1.75rem;
}

/* ------------------------------------------------------------
   TEXT LINKS IN COPY (the locked link standard, 2026-08-07)
   Rest: bold, no underline — the weight against regular-weight
   surrounding copy is the non-color distinction. Hover AND keyboard
   focus bring the green underline, plus the surface's focus ring.
   Green is never the resting color of a body-size link.
   Two surface variants; the component carries the behavior, the
   variant carries the color. Used by the CRONIMET Group parent-site
   links (cronimet_group_link) and available to any future body link.
------------------------------------------------------------ */
.text-link {
  font-weight: 700;
  text-decoration: none;
  text-decoration-color: var(--green);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}
.text-link:hover,
.text-link:focus-visible { text-decoration-line: underline; }

.text-link--on-dark { color: var(--white); }
.text-link--on-dark:focus-visible {
  outline: 2px solid var(--green); /* 6.1:1 on navy */
  outline-offset: 3px;
}

.text-link--on-light { color: var(--blue); }
.text-link--on-light:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 3px;
}

/* Headline wrap rule (2026-09-11, Ed, site-wide): no H2 or H3 carries
   a hard return. Every H2 and H3 fills the full width of its column
   and wraps naturally wherever each viewport breaks it, and text-wrap:
   balance breaks the wrapped lines evenly instead of stranding a lone
   word on the last line. All h2/h3 plus the headline-grade classes
   that live on other tags (stat labels, contact card titles). H1 page
   titles are the ONE exception (restored 2026-09-11, Ed): the hero and
   the page-header titles keep their deliberate stacked rows, so h1 is
   deliberately NOT in this selector. Progressive enhancement, no
   layout shift. */
h2, h3,
.stat-label,
.parent-stat-label,
.contact-card-label {
  text-wrap: balance;
}

/* Body copy and list items take the softer widow control. Progressive:
   browsers without text-wrap keep the plain break. */
p, li {
  text-wrap: pretty;
}

/* ------------------------------------------------------------
   PILL BUTTONS
   Rule: pill outline only. Filled ONLY for primary CTA.
------------------------------------------------------------ */
.btn-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--arrow-gap);
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 700;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  padding: 12px 26px;
  border-radius: 100px;
  border: 1.5px solid transparent;
  cursor: pointer;
  transition: background 0.22s ease, color 0.22s ease, border-color 0.22s ease;
  white-space: nowrap;
}

.btn-pill--filled {
  background: var(--green);
  border-color: var(--green);
  color: var(--blue); /* site-wide standard: navy on green fill; white is 2.82:1 and fails AA */
}
.btn-pill--filled:hover {
  background: transparent;
  color: var(--white);
}
/* Arrows follow the button text on green fill, return to green when the fill drops */
.btn-pill--filled .arrow,
.btn-pill--outline-green:hover .arrow { color: var(--blue); }
.btn-pill--filled:hover .arrow { color: var(--green); }

.btn-pill--outline-white {
  background: transparent;
  border-color: rgba(255,255,255,0.5);
  color: var(--white);
}
.btn-pill--outline-white:hover {
  border-color: var(--white);
  background: rgba(255,255,255,0.08);
}

.btn-pill--outline-green {
  background: transparent;
  border-color: var(--green);
  color: var(--white);
}
.btn-pill--outline-green:hover {
  background: var(--green);
  color: var(--blue); /* green-filled state, same navy-text standard */
}

.btn-pill--lg {
  font-size: 0.88rem;
  padding: 16px 36px;
}

/* Visible keyboard focus (2026-08-10, WCAG 2.4.7). The component had
   NO focus style and the reset drops the browser default, so the
   primary CTA on every page was invisible to a keyboard visitor.

   Two-tone ring, the site's device wherever one focus style has to
   survive several surfaces, because the pill is a shared component
   that has to read on every surface it can land on: solid navy,
   navy-over-photography (hero, CTA band),
   and light, with three fills of its own underneath (green, outline
   white, outline green). No single colour clears 3:1 on all of them.

   The marker's order is REVERSED here (navy inside, grey-10 outside)
   for two reasons the marker does not have. The outward-facing band
   is the one measured against the page, and grey-10 is the token the
   contrast audit already proved on photography (5.80:1 on the hero's
   worst frame, where navy would disappear); and the inward-facing
   band sits against the button, which can be a green fill, where navy
   is 6.11:1 and grey-10 would be only 2.27:1.

   Measured: grey-10 on navy 13.89:1, navy on off-white 15.77:1,
   grey-10 on the hero's worst frame 5.80:1, navy on the green fill
   6.11:1. Every boundary clears the 3:1 non-text minimum.

   box-shadow, not outline, so the ring follows the 100px pill radius.
   The transparent outline is the site's existing forced-colors
   fallback (same device as the Forminator field rule): box-shadow is
   dropped in forced-colors mode and the outline becomes a real
   system-coloured ring. :focus-visible only, so a mouse click never
   draws it, and resting, hover and active states are untouched. */
.btn-pill:focus-visible {
  outline: 2px solid transparent;
  outline-offset: 3px;
  box-shadow: 0 0 0 3px var(--blue), 0 0 0 6px var(--grey-10);
}

/* ------------------------------------------------------------
   NAVIGATION
------------------------------------------------------------ */
/* Skip link: parked above the viewport until keyboard focus lands on it */
.skip-link {
  position: fixed;
  top: -100px;
  left: 24px;
  z-index: 300;
  background: var(--blue);
  color: var(--white);
  border: 1px solid var(--green);
  border-radius: 100px;
  padding: 12px 24px;
  font-weight: 600;
  transition: top 0.2s ease;
}
.skip-link:focus { top: 12px; }

/* Visible keyboard focus on the navy header (green ring passes the 3:1 non-text rule at 6.1:1) */
.site-nav a:focus-visible,
.nav-toggle:focus-visible,
.skip-link:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 3px;
}

/* The header is SOLID navy at the top edge, in every state (2026-08-08,
   Ed): on a notched phone this strip sits directly under the status
   bar, and anything translucent there lets the page show through
   behind the clock.
   The safe-area padding is defensive. The site does not set
   viewport-fit=cover, so iOS keeps the content inside the safe area and
   paints the inset from the html/body navy above; env() therefore
   resolves to 0 today and this adds nothing. If viewport-fit=cover is
   ever introduced, this already carries the header's navy up behind the
   status bar, and at that point every offset derived from --nav-h
   (hero-content, page-header, form-section scroll-margin, page-interior)
   needs the same inset added or content will sit under the notch. */
.site-nav {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 100;
  background: var(--blue);
  padding-top: env(safe-area-inset-top, 0px);
  border-bottom: 1px solid rgba(255,255,255,0.06);
  transition: background 0.3s ease, box-shadow 0.3s ease, transform 260ms ease;
}

/* Hide on scroll down, show on scroll up (ported from the pghopera build).
   Under reduced motion the hide/show still functions, just without the slide. */
.site-nav.is-hidden { transform: translateY(-100%); }

@media (prefers-reduced-motion: reduce) {
  .site-nav, .skip-link { transition: none; }
}

/* Scrolled state keeps its lift but drops the 4% transparency and the
   blur behind it (2026-08-08, Ed): under a phone status bar that 4%
   let page content ghost through behind the clock. The blur went with
   it because there is nothing left to blur, and it was costing a
   compositor layer on every scroll for no visible gain. */
.site-nav.scrolled {
  background: var(--blue);
  box-shadow: 0 2px 40px rgba(0,0,0,0.35);
}

.nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: var(--nav-h);
  /* content-max plus its gutters (border-box), so the logo sits exactly
     on the shared left content edge at every width */
  max-width: calc(var(--content-max) + 2 * var(--page-pad));
  margin: 0 auto;
  padding: 0 var(--page-pad);
}

/* -------------------------------------------------------
   SITE LOGO — two-file approach
   gf-monogram-white.svg  : GF circle only (viewBox 0 0 56.9 56.9)
   cronimet-wordmark-only-white.svg : CRONIMET text only (viewBox 68 0 215.46 56.9)
   TERMINALS lives in the same flex column as the wordmark
   img, so it left-aligns to the wordmark automatically.
   No percentage math needed.
------------------------------------------------------- */
.site-logo {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 1;
  text-decoration: none;
  flex-shrink: 0;
}

.site-logo-row {
  display: inline-flex;
  align-items: center;
  gap: 14px;
}

.site-logo-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 1;
}

.site-logo-monogram {
  display: block;
  width: auto;
  flex-shrink: 0;
}

.site-logo-wordmark {
  display: block;
  width: auto;
}

.site-logo-sublabel {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 400;
  letter-spacing: 0.25em;
  color: #91a2ac;
  margin-top: 2px;
  margin-left: 4px; /* optical adjustment — aligns T under C stroke, not SVG edge */
}

/* Nav header */
.site-nav .site-logo-monogram { height: 36px; }
.site-nav .site-logo-wordmark { height: 28px; }
.site-nav .site-logo-sublabel { font-size: 11px; }

/* Footer */
.site-footer .site-logo-monogram { height: 48px; }
.site-footer .site-logo-wordmark { height: 38px; }
.site-footer .site-logo-sublabel { font-size: 13px; }

.nav-links {
  display: flex;
  align-items: center;
  gap: 2.5rem;
}

.nav-link {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 600;
  font-size: 1rem; /* 16px, measured from the cronimet.de main menu; pairs with the 0.02em tracking */
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--metal);
  position: relative;
  transition: color 0.2s ease;
}

.nav-link::after {
  content: '';
  position: absolute;
  bottom: -3px;
  left: 0;
  width: 0;
  height: 1px;
  background: var(--green);
  transition: width 0.25s ease;
}

.nav-link:hover,
.nav-link.active { color: var(--white); }

.nav-link:hover::after,
.nav-link.active::after { width: 100%; }

.nav-cta {
  color: var(--metal) !important; /* rests like a nav link; white is the hover state */
  border: 1px solid var(--green);
  padding: 7px 44px;
  border-radius: 100px;
  transition: background 0.2s ease, color 0.2s ease;
}
.nav-cta::after { display: none; }
.nav-cta:hover {
  background: var(--green);
  color: var(--blue) !important; /* white on green is 2.82:1 and fails AA; navy is 6.11:1 */
}

.nav-toggle {
  display: none;
  flex-direction: column;
  gap: 5px;
  background: none;
  border: none;
  cursor: pointer;
  padding: 4px;
}
.nav-toggle span {
  display: block;
  width: 22px;
  height: 2px;
  background: var(--white);
  transition: transform 0.25s ease, opacity 0.2s ease;
}

/* ------------------------------------------------------------
   HERO
------------------------------------------------------------ */
.hero {
  position: relative;
  height: 100vh;
  min-height: 680px;
  overflow: hidden;
  background: var(--blue);
  display: flex;
  flex-direction: column;
}

/* Video background — sits behind all content */
.hero-video-wrapper {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  z-index: 0;
}

/* The poster picture paints first and stays under the video at every
   width. It is the ONLY hero background reduced-motion visitors ever
   see: the loader in main.js never attaches video sources for them at
   any width, so they fetch zero video bytes. Every other visitor gets
   video over it (2026-09-08, Ed): the 1080p pair at 901px and up, the
   720p mp4 at 900px and below. */
.hero-fallback {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  height: 100%;
  max-width: none;  /* override global reset max-width: 100% */
  object-fit: cover;
}

.hero-video-wrapper video {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  height: 100%;
  max-width: none;  /* override global reset max-width: 100% */
  object-fit: cover;
  /* Transparent until it is actually PLAYING, so first paint is always
     the poster picture and the swap can never show a black frame. The
     transition only ever runs when main.js flips the class, which it
     never does under reduced motion. */
  opacity: 0;
  transition: opacity 0.6s cubic-bezier(0.33, 1, 0.68, 1);
}

.hero-video-wrapper video.is-playing { opacity: 1; }

.hero-overlay {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  background: rgba(4, 26, 61, 0.6);
  z-index: 1;
}

/* Content wrapper — fills full hero height, centers content vertically */
.hero-content {
  position: relative;
  z-index: 2;
  width: 100%;
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-top: var(--nav-h);
}

/* Single left-aligned content column, starting on the shared left
   content edge (the old 8vw margin was its own third edge) */
.hero-block {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  margin-left: var(--copy-edge);
  max-width: 700px;
}

/* Stacked H1 rows (restored 2026-09-11, Ed): the hero title is the
   H1 exception to the no-hard-returns headline rule, which now covers
   H2 and H3 only. Each .hl span is a deliberate stacked row. */
.hero-headline {
  display: flex;
  flex-direction: column;
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 900;
  text-transform: uppercase;
  line-height: 0.88;
  letter-spacing: -0.03em;
}

.hl {
  display: block;
  font-size: clamp(3rem, 7vw, 6rem);
  color: var(--white);
}

.hl--green { color: var(--green); }

/* TEXT OVER PHOTOGRAPHY steps to --grey-10 (2026-08-08 contrast audit,
   LOCKED). Metal Grey is the body color on SOLID navy, where it measures
   6.53:1. It is not the floor on navy-over-photography: the hero's 60%
   overlay sits on a moving image, and sampling the actual composited
   pixels across the loop put the metal eyebrow at 2.73:1 on its worst
   frame (2.31:1 at the brightest pixel) — a real AA failure, not a
   marginal one. grey-30 (the old step-up) only reached 4.17:1 there.
   grey-10 measures 5.80:1 on the worst frame and 4.92:1 at the
   brightest single pixel, so it clears 4.5:1 even against a specular
   highlight. The same step applies to the closing CTA band's copy over
   its photo. This does NOT relax the brand rule: body copy on solid
   navy is still Metal Grey, and grey-10 is still not white. Re-measure
   (sample the composited pixels, do not assume) if the hero video or
   the CTA photograph is ever replaced.
   RE-MEASURED 2026-09-08 for the valley-flyover hero loop (which
   replaced the river-perspective clip; the Long Aerial poster is
   unchanged). Every web encode carries a highlight rolloff (whites
   capped at 83%, midtones untouched) baked in for exactly this rule:
   this footage's water glare and bright roofs would otherwise
   composite under 4.5:1 beneath the 60% overlay, and the overlay is
   already at the top of its sanctioned range. With the rolloff,
   grey-10 over the composited pixels measures: 1080p desktop encode
   worst frame mean 6.37:1 with 99.991% of sampled text-region pixels
   at or above 4.5:1 across the full loop (residue: transient
   single-pixel codec ringing glints, floor 3.81:1, 0.03% of the worst
   frame); 720p mobile encode (phones now play video too) worst frame
   mean 6.69:1, 99.982% at or above 4.5:1, floor 3.75:1; poster
   (2026-09-04 measurement, file unchanged) mean 6.46:1 with every
   pixel at or above 4.5:1 (minimum 4.75:1). Keep the rolloff in any
   future re-encode, or re-measure without it. */
.hero-eyebrow {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 600;
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--grey-10);
  display: flex;
  align-items: baseline; /* arrow on the first line when wrapped */
  gap: var(--arrow-gap);
}

.hero-body {
  font-size: clamp(0.95rem, 1.1vw, 1.05rem);
  line-height: 1.65;
  color: var(--grey-10);
  max-width: 480px;
}

.hero-ctas {
  display: flex;
  gap: 12px;
}

.hero-scroll {
  position: absolute;
  bottom: 40px;
  right: 60px;
  z-index: 10;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}

.hero-scroll-label {
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  color: var(--grey-10); /* over-photography step, see .hero-eyebrow */
  text-transform: uppercase;
  writing-mode: vertical-rl;
}

.hero-scroll-line {
  width: 1px;
  height: 44px;
  background: linear-gradient(to bottom, var(--green), transparent);
  animation: scrollPulse 2.2s ease-in-out infinite;
}

@keyframes scrollPulse {
  0%, 100% { opacity: 0.35; transform: scaleY(1); }
  50%       { opacity: 1;    transform: scaleY(1.1); }
}

/* Image background hero (interior pages — no video) */
.hero-img-wrapper {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  z-index: 0;
}

.hero-img-bg {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  background-size: cover;
  background-position: center;
}

/* ------------------------------------------------------------
   PAGE HEADER — compact interior page opener. The standard for
   every page except Home: arrow eyebrow, page H1 at a reduced
   display scale, green rule, one supporting line, and an
   optional per-page photo on the right that bleeds to the right
   viewport edge and hangs --box-overlap past the band bottom
   (the split-section overlap grammar). The photo runs clean, no
   overlay: text never sits on it. No buttons, no scroll
   indicator. A page header, not a landing moment.
------------------------------------------------------------ */
.page-header {
  --ph-pad-b: 48px;
  position: relative;
  z-index: 2; /* the figure hang paints over the next section */
  background: var(--blue);
  padding: calc(var(--nav-h) + 48px) 0 var(--ph-pad-b);
}

.page-header__block {
  /* the shared left content edge, same token as .hero-block */
  margin-left: var(--copy-edge);
  padding-right: var(--page-pad);
  max-width: 900px;
}

/* No-figure variant (2026-09-10, the legal pages): the 900px cap
   exists so a one-row headline stays one line BESIDE the figure
   column; with no figure there is nothing to clear, so the block gets
   headroom and a one-row H1 like TERMS AND CONDITIONS. (measured
   817px at the title clamp's 4.75rem cap against 840px of content
   width under the 900px cap) can never wrap at desktop from a few
   pixels of rendering drift. The lede keeps its own 34rem measure. */
.page-header:not(.page-header--figure) .page-header__block { max-width: 980px; }

/* Figure variant: the text column reserves the figure's width plus a
   gap; both use the same clamp so they can never disagree */
.page-header--figure .page-header__block {
  padding-right: 0;
  margin-right: calc(clamp(300px, 34vw, 560px) + 48px);
}

.page-header__eyebrow {
  display: flex;
  align-items: baseline; /* arrow on the first line when wrapped */
  gap: var(--arrow-gap);
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 600;
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--metal);
  margin-bottom: 1.5rem;
}

.page-header__title {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 900;
  text-transform: uppercase;
  line-height: 0.9;
  letter-spacing: -0.03em;
  /* Clearly below the hero's clamp(3rem, 7vw, 6rem), still above the
     section headlines' 4.5rem cap, and sized so a one-row headline
     stays one line beside the figure column at desktop widths. Line
     breaks come only from the field's rows, never from wrapping. */
  font-size: clamp(2.4rem, 4.6vw, 4.75rem);
  color: var(--white);
}

.page-header .headline-rule { margin: 1.5rem 0 1.75rem; }

.page-header__lede {
  font-size: clamp(0.95rem, 1.1vw, 1.05rem);
  line-height: 1.65;
  color: var(--metal);
  /* Widow rule: the supporting line fits on ONE line at desktop.
     34rem clears the Services line with margin; keep new ledes short
     enough to fit this measure. */
  max-width: 34rem;
}

/* Figure: absolutely positioned against the band so it bleeds to the
   right viewport edge, tops out level with the text content, and hangs
   --box-overlap past the navy bottom edge into the next section,
   echoing the split overlap. */
.page-header__figure {
  position: absolute;
  top: calc(var(--nav-h) + 48px); /* level with the band's content top */
  right: 0;
  bottom: calc(-1 * var(--box-overlap));
  width: clamp(300px, 34vw, 560px);
}

.page-header__figure-img {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  background-color: var(--dark-metal);
}

/* Same breathing room after the header as after the hero; also
   clears the figure hang (80px) with room to spare */
.page-header + .split { margin-top: 120px; }

/* ------------------------------------------------------------
   INTRO BAND — editorial lead paragraph directly below the page
   header (Facilities standard; any interior page may use it).
   One calm paragraph on off-white, on the copy edge, one step
   above body scale. Top padding clears the header figure's
   --box-overlap hang; spacing to the next section comes from the
   sibling rules below.
------------------------------------------------------------ */
.intro-band {
  background: var(--off-white);
  padding: 140px var(--copy-edge) 0;
}

.intro-band__inner {
  max-width: var(--content-max);
  margin: 0 auto;
}

.intro-band__text {
  font-size: clamp(1.1rem, 1.5vw, 1.45rem);
  line-height: 1.6;
  color: var(--blue);
  max-width: 34em;
}

.intro-band + .split { margin-top: 160px; }

/* ------------------------------------------------------------
   SPLIT SECTIONS — core asymmetric offset layout
   Images bleed to viewport edge. Boxes overlap images by
   --box-overlap (80px) horizontally and vertically.
------------------------------------------------------------ */
.split {
  display: grid;
  position: relative;
  /* No horizontal padding — images must touch viewport edges */
  /* Vertical spacing provided by margin from sibling rules below */
}

/* Image left / box right */
.split--img-left {
  grid-template-columns: var(--split-img) 1fr;
}

/* Image right / box left. A spacer column pins the box copy to the
   shared copy edge (the old fixed 1fr + padding grammar left this text
   at 120px from the viewport edge — a third left edge on wide screens).
   The navy panel is a background, so it may run past the edge; the
   text inside it may not. */
.split--img-right {
  grid-template-columns: var(--copy-edge) 1fr var(--split-img);
}

.split--img-right .split__img-col { grid-column: 3; }

/* Space above first split section (off-white shows after dark hero) */
.hero + .split {
  margin-top: 120px;
}

/* Space between consecutive split sections */
.split + .split {
  margin-top: 200px;
}

/* Space between last split and stats band */
.split + .stats-band {
  margin-top: 160px;
}

/* Space between stats band and next split */
.stats-band + .split {
  margin-top: 160px;
}

/* ------- Image column ------- */
.split__img-col {
  position: relative;
  /* min-height ensures image has height even if box content is short */
  min-height: 600px;
}

.split__img-bg {
  position: absolute;
  /* Extend image 80px above/below to visually match box height */
  inset: calc(-1 * var(--box-overlap)) 0;
  background-size: cover;
  background-position: center;
  /* Fallback gradient if Unsplash image fails to load */
  background-color: var(--dark-metal);
}

/* Client photography (2026-09-08; plain jpeg img since 2026-09-14,
   the <picture> webp source is retired and Converter for Media serves
   the webp encode on the jpeg URL): a real <img> with srcset INSIDE
   the same wrappers the old background divs painted, so the wrappers
   keep their geometry, stacking, fallback navy, and (for splits) the
   GSAP parallax. The img is pinned to its wrapper and covers it
   exactly as background-size: cover / background-position: center did.
   A per-image focal point may override object-position via an inline
   style the render reads from attachment meta (_cronimet_object_position,
   seeded by 01-media.php); that is how a tall desktop column and the
   wide 360px mobile strip both keep the subject in frame. Tune focal
   points in the seed meta, never by re-cropping by eye. */
.split__img-bg img,
.cta-bg img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* ------- Content column ------- */
.split__content-col {
  display: flex;
  align-items: center;
}

/* img-left: box is on the right, overlaps image on its LEFT */
.split--img-left .split__content-col {
  padding: var(--box-overlap) var(--page-pad) var(--box-overlap) 0;
}

.split--img-left .split__box {
  margin-left: calc(-1 * var(--box-overlap));
  padding: 56px var(--page-pad) 56px calc(var(--box-overlap) + 40px);
}

/* img-right: box is on the left, overlaps image on its RIGHT. The
   content column starts on the copy edge; the panel pulls back by its
   own interior padding so the text (not the panel) lands on the edge. */
.split--img-right .split__content-col {
  grid-column: 2;
  padding: var(--box-overlap) 0;
}

.split--img-right .split__box {
  margin-left: calc(-1 * var(--page-pad));
  margin-right: calc(-1 * var(--box-overlap));
  padding: 56px calc(var(--box-overlap) + 40px) 56px var(--page-pad);
}

/* ------- Box ------- */
.split__box {
  width: 100%;
  position: relative;
  z-index: 2;
  margin-top: calc(-1 * var(--box-overlap));
  margin-bottom: calc(-1 * var(--box-overlap));
  box-shadow: 0 24px 80px rgba(4,26,61,0.22), 0 4px 16px rgba(0,0,0,0.12);
}

.split__box--navy    { background: var(--blue); }
.split__box--darkgrey { background: var(--dark-metal); }

.sect-num {
  display: block;
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 900;
  font-size: clamp(5rem, 7vw, 8rem);
  line-height: 1;
  letter-spacing: -0.04em;
  color: rgba(255,255,255,0.06);
  margin-bottom: -1rem;
  user-select: none;
}

.split__headline {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.025em;
  line-height: 0.92;
  font-size: clamp(2.2rem, 3.8vw, 4.5rem);
  color: var(--white);
  margin-bottom: 0;
}

.split__body {
  font-size: clamp(0.88rem, 1vw, 1rem);
  line-height: 1.7;
  color: var(--metal);
  margin-bottom: 1rem;
  max-width: 44ch;
}

/* ------- Service list (Section 01) ------- */
.service-list {
  margin: 0 0 1.75rem;
}

.service-row {
  display: grid;
  grid-template-columns: 28px 1fr;
  gap: 0 16px;
  padding: 18px 0;
  border-top: 1px solid rgba(255,255,255,0.08);
  /* Arrow rows align to the first line, and they align by BASELINE, not
     by box top: the arrow is a fixed 16px against a 0.92rem name, so
     top alignment left the glyph riding 2.5px low. */
  align-items: baseline;
}

.service-row:last-child { border-bottom: 1px solid rgba(255,255,255,0.08); }

.service-n {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 700;
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  /* grey-70 is 3.85:1 on navy and fails AA; grey-50 clears it at 6.53:1,
     matching .service-band--navy .service-band__num. No page renders a
     numbered service row today (split-section always emits the arrow),
     so this is a latent case closed, not a visible change. */
  color: var(--grey-50);
  padding-top: 2px;
}

.service-name {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 700;
  font-size: 0.92rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--white);
  margin-bottom: 4px;
  /* Widow rule: a name that wraps in the hanging text column breaks
     in balanced lines, no single-word last line. Progressive.
     balance, not pretty (2026-09-10, Ed): pretty still stranded
     "Access" and "Unloading" alone on the last line at 1440/1280. */
  text-wrap: balance;
}

/* Item body inside a split's service rows (Home service cards,
   Facilities storage and modes lists). 0.83rem computed to 13.28px,
   which passed contrast but was below the site's own comfortable
   reading floor for running copy on navy; 0.94rem is 15.04px
   (2026-08-10, legibility pass). Colour and weight are unchanged, and
   this is the only size that moves: the lede, .split__body, labels
   and headings all keep their scale. line-height stays the unitless
   1.55, so leading scales with the size (13.28/20.58 becomes
   15.04/23.31) and needs no separate adjustment. */
.service-desc {
  font-size: 0.94rem;
  line-height: 1.55;
  color: var(--metal);
}

/* ------- Tags (Section 02) ------- */
.tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 1.25rem 0 1.75rem;
}

.tag {
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--metal);
  border: 1px solid rgba(145,162,172,0.3);
  padding: 5px 12px;
}

.coord-block {
  display: flex;
  gap: 20px;
  margin-top: 1.5rem;
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--metal);
}

/* ------- Parent stats (Section 04) ------- */
.parent-stats {
  display: flex;
  align-items: center;
  gap: 0;
  margin: 1.75rem 0;
}

.parent-stat { flex: 1; }

.parent-stat-num {
  display: block;
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 900;
  font-size: clamp(2.5rem, 4vw, 4.5rem);
  letter-spacing: -0.04em;
  line-height: 1;
  color: rgba(255,255,255,0.2);
}

.parent-stat-label {
  display: block;
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--metal);
  margin-top: 6px;
}

.parent-stat-div {
  width: 1px;
  height: 60px;
  background: rgba(255,255,255,0.1);
  margin: 0 28px;
  flex-shrink: 0;
}

/* ------------------------------------------------------------
   STATS BAND — Section 03, full-width dark navy
------------------------------------------------------------ */
.stats-band {
  background: var(--blue);
  padding: 100px var(--copy-edge);
  position: relative;
  z-index: 1;
}

.stats-inner {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0;
  max-width: var(--content-max);
  margin: 0 auto;
  /* default align-items: stretch — all stat columns same height */
}

/* Each stat is a flex column: number fills space, rule/label/sub at bottom */
.stat {
  display: flex;
  flex-direction: column;
  padding: 0 clamp(20px, 3vw, 44px);
  border-right: 1px solid rgba(145, 162, 172, 0.3);
}
.stat:first-child { padding-left: 0; }
.stat:last-child  { border-right: none; padding-right: 0; }

/* Number fills available height; text bottom-aligns so rule lines up */
.stat-num {
  flex: 1;
  display: flex;
  align-items: flex-end;
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 900;
  font-size: clamp(3.5rem, 8vw, 7rem);
  line-height: 0.88;
  letter-spacing: -0.04em;
  color: var(--white);
  padding-bottom: 0.5rem;
}

.stat-rule {
  width: 100%;
  height: 1px;
  background: var(--green);
  margin: 0 0 0.6rem;
  flex-shrink: 0;
}

.stat-label {
  display: block;
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 700;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--white);
  margin-bottom: 4px;
}

.stat-sub {
  display: block;
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.78rem;
  color: var(--metal);
  letter-spacing: 0.05em;
}

.stats-divider {
  display: none;
}

/* ------------------------------------------------------------
   COMMODITIES BAND — static off-white section
------------------------------------------------------------ */
.commodities-band {
  background: var(--off-white);
  padding: 80px var(--copy-edge);
  margin-top: 160px;
}

.commodities-inner {
  max-width: var(--content-max);
  margin: 0 auto;
}

.commodities-label {
  display: flex;
  align-items: baseline; /* arrow on the first line when wrapped */
  gap: var(--arrow-gap);
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 600;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--grey-90); /* metal is 2.41:1 on off-white; grey-90 clears AA at 7.2:1 */
  margin-bottom: 1.5rem;
}

.commodities-list {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 700;
  font-size: clamp(1.6rem, 3vw, 2.4rem);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--blue);
  line-height: 1.6;
}

/* A commodity never splits across lines (2026-08-08, Ed): "INDUSTRIAL
   MINERALS" broke in two mid-item on phones, which read as two
   commodities. Every item is its own unbreakable span at every width.
   Safe on desktop by measurement: the longest item is 17.06em wide, so
   at the 2.4rem cap it needs 655px of the 1185px content column, and
   the ratio only improves as the clamp scales down with the viewport.
   The mobile size below is derived from the same 17.06em; if a longer
   commodity is ever added, re-measure and re-derive it. */
.comm-item { white-space: nowrap; }

.comm-dot {
  color: var(--grey-70); /* green is 2.58:1 on off-white; grey-70 clears the 3:1 non-text minimum at 4.1:1 */
  padding: 0 0.2rem;
}

/* ------------------------------------------------------------
   SERVICE BAND — full-width editorial list section (Services).
   Light or navy; numbered index items or arrow rows; optional
   card-scale offset figure beside the header.
------------------------------------------------------------ */
.service-band {
  padding: 120px var(--copy-edge);
}

.service-band--light { background: var(--off-white); color: var(--blue); }
.service-band--navy  { background: var(--blue);      color: var(--white); }

.service-band__inner {
  max-width: var(--content-max);
  margin: 0 auto;
}

/* Watermark numeral follows the split-section spec; light surface
   needs the navy-tinted ghost instead of the white one.
   Alpha 0.11 (2026-08-08, Ed): at 0.05 the numeral measured 1.10:1
   against the off-white and effectively disappeared, while the navy
   band's white ghost sits at 1.17:1 — the light watermark was the
   fainter of the two. 0.11 reads as 1.25:1: unmistakably a design
   element, still nowhere near text contrast, so it stays behind the
   content. The navy bands' rgba(255,255,255,0.06) is unchanged. */
.service-band--light .sect-num { color: rgba(4, 26, 61, 0.11); }

/* Eyebrow on off-white: metal is 2.41:1 there; grey-90 clears AA
   (same correction as .commodities-label) */
.service-band--light .label-sm { color: var(--grey-90); }

.service-band__head-text { margin-bottom: 56px; }

/* Figure variant: the inner container becomes a 2-column grid. The
   right rail track carries the image cap (minmax max = 440px) so the
   cap lives on the TRACK, not the item — an item in an auto track can
   overflow during lazy-load reflow (the bug that let the packaging
   photo paint at its 783px natural width). Composition: figure top
   edge on the band content's first grid line, right edge flush to the
   1400px container's last grid line, natural height, no overhang. */
.service-band__inner--figure {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(280px, 440px);
  grid-template-areas:
    "head fig"
    "list fig";
  column-gap: 80px;
  align-items: start;
}

.service-band__inner--figure .service-band__head-text { grid-area: head; }
.service-band__inner--figure .service-band__list      { grid-area: list; }

.service-band__headline {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.025em;
  line-height: 0.92;
  font-size: clamp(2.2rem, 3.8vw, 4.5rem);
}

.service-band--light .service-band__headline { color: var(--blue); }
.service-band--navy  .service-band__headline { color: var(--white); }

.service-band__intro {
  font-size: clamp(0.88rem, 1vw, 1rem);
  line-height: 1.7;
  max-width: 52ch;
}

.service-band--light .service-band__intro { color: var(--grey-90); }
.service-band--navy  .service-band__intro { color: var(--metal); }

.service-band__figure {
  grid-area: fig;
  width: 100%;
  margin: 0;
}

.service-band__figure img {
  width: 100%;
  height: auto;
  display: block;
}

/* Items flow in CSS columns (not grid) so each row keeps its own
   height and the rhythm stays even when a name wraps */
.service-band__list {
  column-count: 2;
  column-gap: 80px;
}

.service-band__item {
  break-inside: avoid;
  display: grid;
  grid-template-columns: 34px 1fr;
  gap: 0 14px;
  padding: 18px 0;
  /* Index figure sits on the name's baseline, same as .service-row.
     Baseline, not box top: the figure is either a fixed 16px arrow or
     a 0.68rem numeral, and neither top-aligns onto the name's cap
     line on its own. This replaces the old padding-top optical nudge
     on .service-band__num, which only ever suited the numeral. */
  align-items: baseline;
}

.service-band--navy  .service-band__item { border-top: 1px solid rgba(255,255,255,0.08); }
.service-band--light .service-band__item { border-top: 1px solid rgba(4,26,61,0.12); }

.service-band__num {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 700;
  font-size: 0.68rem;
  letter-spacing: 0.1em;
}

.service-band--navy  .service-band__num { color: var(--grey-50); } /* grey-70 fails AA on navy */
.service-band--light .service-band__num { color: var(--grey-90); }

/* Widow rule: a name that wraps in the hanging text column breaks in
   balanced lines, no single-word last line. On the BODY cell, not the
   name span: text-wrap acts on the block container that lays the
   lines, and the name is an inline span inside this div. The note
   inherits it, matching the site's body-copy treatment. Progressive. */
.service-band__body { text-wrap: pretty; }

.service-band__name {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 700;
  font-size: 0.92rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.service-band--navy  .service-band__name { color: var(--white); }
.service-band--light .service-band__name { color: var(--blue); }

.service-band__note {
  font-size: 0.78rem;
  line-height: 1.5;
  margin-top: 2px;
}

.service-band--navy  .service-band__note { color: var(--metal); }
.service-band--light .service-band__note { color: var(--grey-90); }

/* Grouped lists (Services band 04, 2026-09-02): labeled arrow lists
   side by side. Each group is one grid cell so its items can never
   migrate across the gutter the way a shared CSS-column flow would
   let them; the list inside a group is therefore a single column. */
.service-band__groups {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 80px;
  align-items: start;
}

.service-band__inner--figure .service-band__groups { grid-area: list; }

/* Group heading: one step above the item names (which sit at 0.92rem
   with 0.06em tracking), under it the standard 40px green rule at a
   tightened rhythm. h3 in the markup; the band headline above is
   the h2. */
.service-band__group-heading {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 800;
  font-size: 1.05rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.service-band--light .service-band__group-heading { color: var(--blue); }
.service-band--navy  .service-band__group-heading { color: var(--white); }

.service-band__group .headline-rule { margin: 0.9rem 0 0.6rem; }

/* Optional lead line under a group heading, in the intro's voice */
.service-band__group-lead {
  font-size: clamp(0.88rem, 1vw, 1rem);
  line-height: 1.7;
  max-width: 52ch;
  margin-bottom: 0.35rem;
  text-wrap: pretty;
}

.service-band--light .service-band__group-lead { color: var(--grey-90); }
.service-band--navy  .service-band__group-lead { color: var(--metal); }

.service-band__list--group { column-count: 1; }

/* Section spacing: bands carry their own padding; splits need the
   same breathing room against them as against stats bands */
.split + .service-band,
.service-band + .split { margin-top: 160px; }

/* ------------------------------------------------------------
   CONTACTS SECTION (About page)
------------------------------------------------------------ */
.contacts-section {
  background: var(--blue);
  padding: 100px var(--copy-edge);
  margin-top: 160px;
  border-top: 1px solid rgba(255,255,255,0.08);
}

.contacts-inner {
  max-width: var(--content-max);
  margin: 0 auto;
}

.contacts-headline {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.025em;
  line-height: 0.92;
  font-size: clamp(2.8rem, 5vw, 5.5rem);
  color: var(--white);
  margin: 0 0 3rem;
}

.contacts-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-top: 1px solid rgba(255,255,255,0.08);
}

.contact-card {
  padding: 2.5rem 2.5rem 2.5rem 0;
  border-right: 1px solid rgba(255,255,255,0.08);
}

.contact-card:last-child {
  border-right: none;
  padding-right: 0;
}

.contact-card:not(:first-child) {
  padding-left: 2.5rem;
}

.contact-card-label {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 700;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--metal);
  margin-bottom: 1rem;
}

.contact-detail {
  display: block;
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.95rem;
  color: var(--white);
  line-height: 1.8;
  font-style: normal;
  transition: color 0.2s ease;
}

a.contact-detail:hover { color: var(--green); }

.contact-detail--meta {
  color: var(--metal);
  font-size: 0.85rem;
}

/* External links in a contact card follow the locked link standard
   (2026-08-08): bold white on the navy band, no underline at rest, the
   green underline on hover and keyboard focus. The old treatment
   (metal at rest, green TEXT on hover) broke the standing rule that
   green is never readable text on dark — only the footer link hover
   carries that exception. */
a.contact-detail--link {
  color: var(--white);
  font-weight: 700;
  text-decoration: none;
  text-decoration-color: var(--green);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}
/* a.contact-detail:hover above is (0,2,1); these must match it to win */
a.contact-detail--link:hover,
a.contact-detail--link:focus-visible {
  color: var(--white);
  text-decoration-line: underline;
}
a.contact-detail--link:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 3px;
}

/* ------------------------------------------------------------
   CTA SECTION — the site-wide closing band above the footer.
   Photo background + navy overlay when an image is set (the
   standard); flat navy as the no-image fallback.
------------------------------------------------------------ */
/* The LEFT padding is the copy edge, as everywhere else on the site.
   The RIGHT padding is only --page-pad, and that is deliberate: the
   copy-edge rule governs where copy STARTS, and the CTA right cell is
   named in that rule as a grid-positioned column the edge does not
   apply to. Symmetric copy-edge padding was the root of the band's
   column collapse (2026-08-10, Ed): at 1101px the --copy-floor engages
   and takes 80px off EACH side, so the band lost 160px of working
   width at the exact point the display type stepped up, and the
   headline column went BACKWARDS as the viewport grew (643px at
   1100px, 390px at 1101px). Releasing the right side returns ~100px
   of that in the starved 1101 to 1640 range. Above 1640 nothing moves:
   --content-max caps .cta-inner at 1400 and the band's right edge
   lands exactly where every other section's does. */
.cta-section {
  position: relative;
  overflow: hidden;
  background: var(--blue);
  padding: 120px var(--page-pad) 120px var(--copy-edge);
  border-top: 1px solid var(--green);
  margin-top: 0;
}

.cta-bg {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  z-index: 0;
}

/* Same 60% navy as the hero overlay: the approved photo-under-text treatment */
.cta-overlay {
  position: absolute;
  inset: 0;
  background: rgba(4, 26, 61, 0.6);
  z-index: 1;
}

/* Asymmetric by design. An even 1fr 1fr split gave the headline half
   the band while the headline is the only thing in it that cannot
   reflow: the display type scales at 5vw and the longest line on the
   site, the Facilities "GROUND TO STORE IT.", measures 9.474em, so it
   needs roughly 60% of the working width at every desktop size while
   the body column only ever needs enough for the pill button. The
   button is the hard floor here at 284px intrinsic (a pill never
   wraps), which is why the right track is minmax(284px, 1fr) rather
   than a bare fr: it can never be squeezed under its own content, and
   the left track carries any shortfall instead of overflowing.

   The ratio steps at 1100 because the band's geometry genuinely
   changes there: below it the copy floor follows --page-pad and the
   band is wide, so the body column can stay generous; above it the
   floor engages and the headline needs the larger share. Derived
   against 9.474em headroom at 901, 1101, 1440 and 1920; re-derive both
   ratios if a longer CTA headline is ever authored.

   margin-right auto, not margin auto: with an asymmetric padding pair
   the old centering would have pushed the inner off the copy rail on
   wide screens. Left-aligning pins the first column to the copy edge
   and lets --content-max cap the width from the right. */
.cta-inner {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: minmax(0, 1.85fr) minmax(284px, 1fr);
  gap: 60px;
  align-items: center;
  max-width: var(--content-max);
  margin: 0 auto 0 0;
}

/* Over a photo every piece of small text steps to grey-10 (the
   over-photography rule at .hero-eyebrow; measured against the actual
   composited pixels of the CTA photograph under the 60% overlay).
   The eyebrow was the miss: it kept metal and measured 4.09:1 on the
   band average (2.00:1 at the brightest pixel), an AA failure. The
   body and phone line passed at grey-30 (6.57 and 6.94) but thinned to
   ~3.2:1 in the bright water, so they take the same step. Measured at
   grey-10: eyebrow 8.70, body 9.14, phone line 9.66.
   THE SITE DEFAULT BAND IS PHOTO-FREE (2026-09-09, Ed, seventh
   pass): 02-options.php clears the default bg fields, the render
   drops .cta-section--photo, the photo, and the overlay together,
   and the band is the plain solid navy where white and Metal Grey
   pass AA outright (15.9:1 and 6.53:1), so no composited audit
   applies to the default band on Services, Facilities, Terms, and
   Privacy. Every CTA photo is a PER-PAGE override now.
   HOME PAGE OVERRIDE (2026-09-09, seventh pass):
   dome-warehouses-flyover, frame 1419 (t 47.347s) of the client
   flyover video, with the standing highlight rolloff (curves 0/0
   0.5/0.5 0.75/0.72 1/0.83). The frame was chosen against this
   audit: the sharpest frame in the sanctioned 43.5 to 52s range
   ships a jpg eyebrow floor of 4.43:1 from codec ringing on the
   bright dome roofs. Shipped numbers over the composited pixels of
   each text region at the Home 1920 band (1905 x 522.3): jpg eyebrow
   mean 5.85:1 (floor 4.52), headline (white) mean 9.31:1 (floor
   5.47), body mean 6.04:1 (floor 4.55), phone line mean 7.84:1
   (floor 5.45); webp floors 4.65 / 5.58 / 4.52 / 5.43; 100% of
   sampled text-region pixels at or above 4.5:1 in both formats.
   Keep the rolloff in any re-encode of this frame, or re-measure
   with work/scripts/2026-09-09-cta-restructure-seventh-pass.py.
   ABOUT PAGE OVERRIDE (2026-09-09, second pass): the About band
   carries its own background (terminal-pad-crane-barge, seeded per
   page by 04-pages.php per the one-use-per-image rule) with the same
   rolloff, audited the same way by
   work/scripts/2026-09-09-about-swaps-second-pass.py on BOTH shipped
   formats: jpg eyebrow mean 7.79:1 (floor 4.59), headline mean
   10.61:1 (floor 4.95), body mean 11.16:1 (floor 4.94), phone line
   mean 11.68:1 (floor 4.84); webp floors 4.58 / 5.18 / 4.89 / 4.91;
   100% of sampled text-region pixels at or above 4.5:1. Two things
   are load bearing here: the rolloff (plain floors were 3.94 to
   4.25), and the CROP PLACEMENT, which was solved against the audit
   because the dumping dust plume and the truck's white bed panels cap
   the eyebrow floor below 4.5:1 for most vertical placements. Never
   re-encode either CTA photo without the rolloff, and never move this
   crop, without re-measuring.
   DELIVERY CHANGE (2026-09-14): the hand-encoded webp attachments are
   retired; Converter for Media now machine-encodes a webp FROM each
   shipped jpeg (rolloff inherited from the jpeg) and serves it on the
   jpeg URL. The webp floors recorded above describe the retired hand
   encodes; the jpg numbers remain the audited encodes. Re-measure the
   SERVED webp (fetch the jpeg URL with Accept: image/webp) at the
   next contrast pass. */
.cta-section--photo .cta-body,
.cta-section--photo .cta-phone-line,
.cta-section--photo .label-sm { color: var(--grey-10); }

.cta-headline {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.025em;
  line-height: 0.92;
  font-size: clamp(2.8rem, 5vw, 5.5rem);
  color: var(--white);
}

.cta-body {
  font-size: clamp(0.9rem, 1.05vw, 1.05rem);
  line-height: 1.7;
  color: var(--metal);
  max-width: 44ch;
  margin-bottom: 2rem;
}

.cta-phone-line {
  margin-top: 1rem;
  font-size: 0.85rem;
  color: var(--metal);
}

.cta-phone {
  color: var(--white);
  font-weight: 600;
}
.cta-phone:hover { text-decoration: underline; }

/* ------------------------------------------------------------
   FOOTER
------------------------------------------------------------ */
/* Footer keeps --page-pad padding (NOT --copy-edge) so the decorative
   full-width green rule keeps its 60px inset. The copy inners make up
   the difference with side margins: inside this padded box,
   (copy-floor - page-pad) + centering reproduces the copy edge
   exactly, and collapses to 0 below 1100px where the floor follows
   the gutter (tablet/mobile unchanged). */
.site-footer {
  --footer-inset: max(calc(var(--copy-floor) - var(--page-pad)), calc((100% - var(--content-max)) / 2));
  background: var(--blue);
  padding: 0 var(--page-pad) 48px;
}

/* The green rule runs the FULL viewport width at every breakpoint
   (2026-08-08, Ed): it is the site's top-of-footer divider, so it
   reads as a band edge, not as a content-width underline. The footer
   keeps its --page-pad gutter for everything else; the rule cancels
   that gutter with matching negative side margins and width: auto, so
   it fills the footer's border box exactly — flush to both viewport
   edges, never a pixel past them (no overflow to clip). */
.footer-rule {
  width: auto;
  margin-left: calc(-1 * var(--page-pad));
  margin-right: calc(-1 * var(--page-pad));
  height: 1px;
  background: var(--green);
  opacity: 0.5;
  margin-bottom: 72px;
}

.footer-inner {
  display: grid;
  grid-template-columns: 1.6fr 1fr 1.5fr 1.5fr;
  gap: 60px;
  max-width: var(--content-max);
  margin: 0 var(--footer-inset) 56px;
}

/* Logo only since 2026-08-08 (Ed): the FAIR / RELIABLE / GLOBAL
   tagline and its .footer-tagline rule were removed site-wide.
   align-items so the lock-up shrink-wraps: as a stretched flex item
   the linked lock-up would carry the click target across the whole
   brand column, past the graphic. The visual position is unchanged
   (the stretched box was left-aligned inside anyway), and the mobile
   footer's own align-items: center still wins at 900px and below. */
.footer-brand { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; }

/* The footer lock-up links home (2026-09-10, Ed), the same anchor the
   header renders via cronimet_logo(). .site-logo already carries
   text-decoration: none and there is no hover state, matching the
   header logo link; the keyboard ring below is the header's exact
   ring (green clears 3:1 on navy), restated here because the
   header's rule is scoped to .site-nav. */
.site-footer .site-logo:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 3px;
}

.footer-nav ul { display: flex; flex-direction: column; gap: 10px; }

.footer-nav a {
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.85rem;
  color: var(--metal);
  transition: color 0.2s ease;
}
.footer-nav a:hover { color: var(--white); }

/* The footer menu items carry the green arrow glyph in the markup at
   every width (filter in inc/theme-helpers.php), but it only shows in
   the mobile footer, where the nav is restyled as a block of tap
   targets. The desktop footer is unchanged. */
.footer-nav .arrow { display: none; }

.footer-address {
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.85rem;
  line-height: 1.8;
  color: var(--metal);
  margin-bottom: 12px;
}

.footer-link {
  display: block;
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.85rem;
  color: var(--metal);
  margin-bottom: 6px;
  transition: color 0.2s ease;
}
.footer-link:hover { color: var(--green); }

/* Labeled contact lines (Phone: / Fax:). The fax number is plain text
   by rule; the phone anchor sits inline after its label. */
.footer-contact-line {
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.85rem;
  color: var(--metal);
  margin-bottom: 6px;
}
.footer-contact-line .footer-link {
  display: inline;
  margin-bottom: 0;
}

/* LinkedIn (2026-09-02, per Jake): one white mark closing the contact
   column. White rather than metal because it is a brand mark, not
   copy; hover goes green per the footer link grammar (the sanctioned
   green-on-dark hover exception). */
.footer-social-link {
  display: inline-flex;
  align-items: center;
  margin-top: 14px;
  color: var(--white);
  transition: color 0.2s ease;
}

.footer-social-link:hover { color: var(--green); }

.footer-social-link:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 3px;
}

.footer-social-icon { display: block; width: 20px; height: 20px; }

.footer-parent-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--grey-50); /* grey-70 is 3.85:1 on navy; grey-50 clears AA at 6.53:1 */
  font-family: 'Source Sans 3', sans-serif;
  margin-bottom: 6px;
}

.footer-parent-name {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 700;
  font-size: 0.9rem;
  color: var(--white);
  margin-bottom: 4px;
}

/* The parent-company attribution links to the CRONIMET Group site
   (2026-08-08, Ed). Locked link standard on navy: bold white, no
   underline at rest, green underline on hover and keyboard focus.
   inline-flex so the mobile footer can give it a real tap target. */
.footer-parent-link {
  display: inline-flex;
  align-items: center;
  color: var(--white);
  text-decoration: none;
  text-decoration-color: var(--green);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}
.footer-parent-link:hover,
.footer-parent-link:focus-visible { text-decoration-line: underline; }
.footer-parent-link:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 3px;
}

.footer-parent-city {
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.8rem;
  color: var(--metal);
}

/* Two stacked entity blocks under one eyebrow (2026-09-18, Katja
   Grimm via Jake Schwab): CRONIMET Corporation, then the CRONIMET
   Group. The gap between them is the SAME value the contact column
   uses between the address block and the phone block (12px desktop,
   14px in the mobile footer), so the column reads as one system. */
.footer-parent-entity + .footer-parent-entity { margin-top: 12px; }

.footer-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  max-width: var(--content-max);
  margin: 0 var(--footer-inset);
  padding-top: 28px;
  border-top: 1px solid rgba(255,255,255,0.06);
}

.footer-legal {
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.75rem;
  color: var(--grey-50); /* grey-70 fails AA on navy, see footer-parent-label */
}
.footer-legal a { color: var(--grey-50); transition: color 0.2s ease; }
.footer-legal a:hover { color: var(--white); }

/* ------------------------------------------------------------
   SCROLL ANIMATION INITIAL STATES (GSAP controls reveal)
   All pre-reveal offsets live under html.js (end of file) so they
   only ever apply when the reveal will actually run. The old
   ungated .split--img-* translateX(±40px) rules here parked
   visible boxes 40px off the copy edge for reduced-motion and
   no-JS visitors (html.js never lands for them); never reintroduce
   an offset outside the html.js gate.
------------------------------------------------------------ */

/* ------------------------------------------------------------
   RESPONSIVE
------------------------------------------------------------ */
@media (max-width: 1100px) {
  :root {
    --split-img: 52vw;
    --page-pad: 40px;
    --box-overlap: 60px;
    --copy-floor: var(--page-pad); /* copy edge follows the gutter below true desktop */
  }

  /* Below the copy floor the band is wide again, so the body column
     gets its share back (see the ratio note at .cta-inner). */
  .cta-inner { grid-template-columns: minmax(0, 1.6fr) minmax(284px, 1fr); }
}

@media (max-width: 900px) {
  :root {
    --split-img: 100%;
    --page-pad: 28px;
    --box-overlap: 0px;
  }

  /* Stack split sections vertically on tablet/mobile */
  .split--img-left,
  .split--img-right {
    grid-template-columns: 1fr;
    grid-template-rows: 360px auto;
  }

  .split--img-left .split__img-col,
  .split--img-right .split__img-col {
    grid-row: 1;
    grid-column: 1;
    min-height: 360px;
  }

  /* ONE mobile edge (standing rule): the navy panel runs full-bleed
     and its interior padding IS --page-pad, so the box copy lands on
     the same edge as the logo. The old 28px column padding + 32px box
     padding stacked to a 60px inset — a second, deeper edge. */
  .split--img-left .split__content-col,
  .split--img-right .split__content-col {
    grid-row: 2;
    grid-column: 1;
    /* MOBILE BAND RHYTHM (2026-08-08, Ed): the tail of off-white below
       a navy box. See the band-transition block at the end of this
       breakpoint for the full set. */
    padding: 0 0 32px;
  }

  .split__img-bg { inset: 0; }

  .split--img-left .split__box,
  .split--img-right .split__box {
    margin: -40px 0 0;
    padding: 48px var(--page-pad);
  }

  /* ---------- MOBILE BAND RHYTHM (2026-08-08, Ed, LOCKED) ----------
     One set of numbers for every band transition at <=900px, so a
     navy band ending and a light section starting reads as a seam,
     not as a hole. Bands carry 56px of their own vertical padding
     (the closing CTA photo band keeps 64px so the headline has room
     over the image), the gap BETWEEN bands is 32px, and the tail of
     off-white under a split's navy box is 32px. Content-to-content
     across a split-to-band seam is therefore 120px instead of the
     old 192px, of which 64px is visible background instead of 120px.
     Several of these bands (stats, commodities, CTA) never had a
     mobile padding override at all and were still carrying their
     100 to 120px desktop values on a 390px screen — that was most of
     the dead space. Desktop (>900px) is untouched. */
  .hero + .split           { margin-top: 32px; }
  .page-header + .split    { margin-top: 32px; }
  .split + .split          { margin-top: 0; }
  .split + .stats-band     { margin-top: 32px; }
  .stats-band + .split     { margin-top: 32px; }
  .stats-band              { padding-top: 56px; padding-bottom: 56px; }
  .commodities-band        { margin-top: 32px; padding-top: 56px; padding-bottom: 56px; }

  /* COMMODITIES ON MOBILE: a stacked list, one item per line, no dots
     (2026-08-08, Ed). The inline dot-separated run broke badly here:
     separators stranded at the start of wrapped lines and multi-word
     commodities split across lines. Stacking is the only layout where
     neither can happen, because there is never more than one item on a
     line for a separator to sit between. The dots are hidden rather
     than removed from the markup so desktop keeps its run untouched;
     whitespace-only text between the spans is not rendered as a flex
     item, so nothing is left behind.

     The size is derived, not guessed: the longest item is 17.06em
     wide, so it fits the shared copy edge whenever the font is at most
     (available width / 17.06). Available width here is the measure
     minus the bullet gutter below, and / 18 against a 17.06em item
     keeps about 5% headroom at 360px (17.0px against an 17.9px
     ceiling); the size rises with the screen until the 1.5rem cap
     takes over at ~486px. Re-derive it if a longer commodity is ever
     added OR if the gutter changes; the nowrap on .comm-item means a
     too-long item would overflow rather than wrap, and the mobile QA
     sweep is what would catch it. */
  .commodities-list {
    --comm-bullet: 6px;
    --comm-gutter: calc(var(--comm-bullet) + var(--arrow-gap));
    display: flex;
    flex-direction: column;
    font-size: min(1.5rem, calc((100vw - 40px - var(--comm-gutter)) / 18));
    line-height: 1.3;
  }
  .commodities-list .comm-dot { display: none; }

  /* MOBILE BULLETS (2026-08-10, Ed). Stacking above drops the
     separators, which left the items with no marker at all. Each item
     gets a leading dot at <=900px only; the desktop run is untouched.
     It is a ::before, so no markup changes and the marker is not
     announced by a screen reader, which is what we want: it is
     decoration, and the items are already read as separate items.

     Colour: --grey-70, the SAME token as the .comm-dot separators
     above (2026-08-10, Ed). One list in two layouts reads as one
     component, so its marker is one colour; it launched green the same
     day and that made the two halves look like two different lists.
     Grey-70 is 4.1:1 on off-white, past the 3:1 non-text minimum, and
     is the reason the separators are not green either (green is 2.58:1
     there).

     Size: 6px, a quarter of the 24px item text at the top of this
     range. The dot has to read as a marker and not as a character in
     the line, which is exactly what the desktop separator does here,
     since it inherits the full text size.

     Spacing: --arrow-gap, the same 8px that sets every arrow-plus-label
     pair on the site, so the marker keeps the site's visual rhythm.
     The gap is the whole spacing, so the dot zeroes the list's 0.04em
     tracking the way .arrow does (see the --arrow-gap note in :root).

     Hanging indent: padding-left holds the text column at bullet plus
     gap, and the matching negative text-indent pulls the first line
     (which starts with the dot) back out to the edge. So the DOT lands
     on --page-pad, the text starts after it, and a wrapped second line
     aligns under the text rather than running back under the dot. Same
     grammar as the arrow rows, where the glyph owns the mobile edge and
     the label sits in the gutter beside it. text-indent applies because
     the items are flex children of the stacked list and so are
     blockified; on desktop they are inline spans and neither property
     is in play.

     Vertical: the dot centers on the CAP line, not the baseline, or it
     sits visibly low against all-caps type. An inline-block with no
     in-flow content takes its baseline from its bottom margin edge, so
     raising it by (cap middle - half the dot) puts the dot's middle on
     the middle of the caps. Source Sans 3's cap height is 0.66em, so
     the cap middle is 0.33em above the baseline. */
  .commodities-list .comm-item {
    padding-left: var(--comm-gutter);
    text-indent: calc(-1 * var(--comm-gutter));
  }

  .commodities-list .comm-item::before {
    content: "";
    display: inline-block;
    width: var(--comm-bullet);
    height: var(--comm-bullet);
    margin-right: var(--arrow-gap);
    border-radius: 50%;
    background: var(--grey-70);
    letter-spacing: 0;
    vertical-align: calc(0.33em - var(--comm-bullet) / 2);
  }
  .contacts-section        { margin-top: 32px; padding: 56px var(--page-pad); }
  .cta-section             { margin-top: 0; padding-top: 64px; padding-bottom: 64px; }

  /* Service band: stack head -> figure -> list in one column */
  .service-band            { padding: 56px var(--page-pad); }
  .split + .service-band,
  .service-band + .split   { margin-top: 32px; }
  .service-band__head-text { margin-bottom: 40px; }
  .service-band__inner--figure {
    grid-template-columns: 1fr;
    grid-template-areas: "head" "fig" "list";
    column-gap: 0;
  }
  .service-band__figure    { max-width: 440px; margin: 0 0 40px; }
  .service-band__list      { column-count: 1; }

  /* Grouped lists stack, one group after the other, with the same 40px
     breathing room the head-text keeps above the list */
  .service-band__groups                      { display: block; }
  .service-band__group + .service-band__group { margin-top: 40px; }

  /* Hanging indent (2026-09-10, Ed, site-wide list standard): the
     figure keeps its own grid column at EVERY breakpoint, so a name
     that wraps aligns every line with its own first line, never back
     under the marker. This replaces the display: contents flattening
     that let wrapped copy return to the edge below the figure (the
     "Relationships with Class I railroads" wrap bug on Services at
     phone widths). The figure still sits inline on the item's first
     line and still lands on the shared --page-pad edge (it is the
     marker column's left edge); only the column narrows to the
     marker's own 16px advance (--arrow-size; the 0.68rem two-digit
     numerals measure about 13px and fit the same column) with the
     10px mobile gap, so the first line of text keeps the x position
     the old inline layout gave it. Baseline alignment and the item
     padding carry over from the base rules; the desc gap comes from
     .service-name's own 4px block margin, live again now that the
     name is a block. */
  .service-band__item { grid-template-columns: var(--arrow-size) 1fr; gap: 0 10px; }
  .service-band__note { margin-top: 4px; }

  .service-row { grid-template-columns: var(--arrow-size) 1fr; gap: 0 10px; }

  /* Contacts: stack on tablet */
  .contacts-grid { grid-template-columns: 1fr; }
  .contact-card,
  .contact-card:not(:first-child) { padding: 1.5rem 0; border-right: none; border-top: 1px solid rgba(255,255,255,0.08); }
  .contact-card:first-child { border-top: none; padding-top: 1.5rem; }

  /* Stats: 2×2 grid on tablet */
  .stats-inner {
    grid-template-columns: 1fr 1fr;
    gap: 0;
  }

  .stat { padding: 0 clamp(16px, 2.5vw, 28px); }
  /* 2x2 grid: both left-column stats (1 and 3) sit flush on the mobile
     edge; only first-child was zeroed before, leaving stat 3 inset */
  .stat:nth-child(odd) { padding-left: 0; }
  .stat:nth-child(2) { border-right: none; padding-right: 0; }
  .stat:nth-child(n+3) {
    border-top: 1px solid rgba(145, 162, 172, 0.2);
    margin-top: 36px;
    padding-top: 36px;
  }
  .stat:nth-child(4) { border-right: none; padding-right: 0; }

  /* CTA stacks */
  .cta-inner { grid-template-columns: 1fr; gap: 40px; }

  /* ---------- MOBILE FOOTER (2026-08-08, Ed) ----------
     Four clearly separated blocks instead of one undifferentiated
     grey column: the brand lock-up centered under a green rule, the
     nav as a stack of real tap targets, the contact block with the
     phone and email reading as actions, and the parent-company
     attribution centered at the bottom. Hairlines and spacing carry
     the separation. Desktop (>900px) is untouched.

     Deliberate exception to the one-mobile-edge rule, at Ed's
     instruction: the brand block and the attribution are CENTERED.
     Everything else in the footer (nav, contact, legal) still lands
     on the shared --page-pad edge, and the arrow glyph is what sits
     on it in the nav rows, per the site-wide arrow grammar. */
  .footer-rule { margin-bottom: 48px; }

  .footer-inner {
    display: block;
    margin-bottom: 32px;
  }

  /* 1. Brand: the logo lock-up, centered. No divider under it
     (2026-08-08, Ed): the block launched with the site's 40px green
     rule below the lock-up, but with the tagline gone the same day it
     was a rule under a logo with nothing to divide, and the hairline
     that opens the nav block below already separates the two. */
  .footer-brand {
    align-items: center;
    text-align: center;
  }

  /* 2. Nav: white, a step up in size and weight, 44px tap targets,
     green arrow on the copy edge so the rows read as actions */
  .footer-nav {
    margin-top: 28px;
    border-top: 1px solid rgba(255, 255, 255, 0.10);
  }
  .footer-nav ul { gap: 0; }
  .footer-nav li + li { border-top: 1px solid rgba(255, 255, 255, 0.06); }
  .footer-nav .arrow { display: inline; }
  .footer-nav a {
    display: flex;
    align-items: center;
    gap: var(--arrow-gap);
    min-height: 48px; /* past the 44px minimum with the row borders */
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--white);
  }
  /* Green on tap/hover is the footer's sanctioned exception to
     "green is never readable text on dark" */
  .footer-nav a:hover,
  .footer-nav a:active { color: var(--green); }
  .footer-nav a:focus-visible,
  .footer-contact a:focus-visible,
  .footer-parent-link:focus-visible {
    outline: 2px solid var(--green);
    outline-offset: 2px;
  }

  /* 3. Contact: address stays quiet grey, phone and email read as
     actions in white bold and carry their own 44px targets */
  .footer-contact {
    margin-top: 28px;
    padding-top: 24px;
    border-top: 1px solid rgba(255, 255, 255, 0.10);
  }
  .footer-address { margin-bottom: 14px; }
  .footer-contact-line {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 0;
    font-size: 0.9rem;
  }
  .footer-contact .footer-link {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    margin-bottom: 0;
    color: var(--white);
    font-weight: 700;
    font-size: 0.95rem;
  }
  .footer-contact .footer-link:hover,
  .footer-contact .footer-link:active { color: var(--green); }
  /* The LinkedIn mark keeps a 44px tap target on its OWN row (flex,
     not inline-flex: the mobile email link is inline-flex, and two
     inline boxes share a line, which floated the icon beside the
     address). The icon itself stays 20px and lands on the shared
     mobile edge. */
  .footer-social-link {
    display: flex;
    min-height: 44px;
    min-width: 44px;
    justify-content: flex-start;
    margin-top: 4px;
  }

  /* 4. Attribution: centered at the bottom, each entity name a
     tappable link. The gap between the two entity blocks matches the
     mobile contact column's address-to-phone spacing (14px). */
  .footer-parent {
    margin-top: 28px;
    padding-top: 28px;
    border-top: 1px solid rgba(255, 255, 255, 0.10);
    text-align: center;
  }
  .footer-parent-name { font-size: 1rem; margin-bottom: 2px; }
  .footer-parent-link { min-height: 44px; padding: 0 4px; }
  .footer-parent-entity + .footer-parent-entity { margin-top: 14px; }

  /* Nav */
  .nav-toggle { display: flex; }

  .nav-links {
    display: none;
    position: absolute;
    top: var(--nav-h);
    left: 0;
    width: 100%;
    background: rgba(4,26,61,0.98);
    flex-direction: column;
    padding: 20px var(--page-pad) 28px; /* menu text on the shared mobile edge */
    gap: 0;
    border-bottom: 1px solid rgba(255,255,255,0.08);
  }
  .nav-links.open { display: flex; }
  .nav-links li { border-bottom: 1px solid rgba(255,255,255,0.06); }
  .nav-links li:last-child { border-bottom: none; } /* no divider under the Contact Us pill */
  /* 44px minimum touch targets: text size + vertical padding keep every row past the minimum */
  .nav-link { display: block; padding: 14px 0; font-size: 1.125rem; }
  .nav-cta { margin-top: 12px; padding: 13px 44px; text-align: center; }

  /* Hero block — reduce left margin at tablet */
  .hero-block {
    margin-left: var(--page-pad);
    max-width: 90vw;
  }

  /* Scroll indicator hugs the shared mobile gutter (desktop keeps 60) */
  .hero-scroll { right: var(--page-pad); }

  /* Page header — same margin reduction, tighter vertical rhythm.
     Figure variant stacks: text first, then the photo full-bleed at a
     modest fixed height, flush to the band bottom, no hang. */
  .page-header { padding: calc(var(--nav-h) + 44px) 0 44px; }
  .page-header__block { margin-left: var(--page-pad); padding-right: var(--page-pad); }
  .page-header--figure { padding-bottom: 0; }
  /* padding-right must be restated: the desktop figure rule zeroes it
     (higher specificity than the plain mobile block rule), which let
     the lede run to the right screen edge on phones */
  .page-header--figure .page-header__block { margin-right: 0; margin-bottom: 40px; padding-right: var(--page-pad); }
  .page-header__figure { position: relative; inset: auto; width: 100%; height: 220px; }

  /* Intro band: no figure hang to clear on mobile (the header photo is
     a flush relative strip), so the lead drops to the section rhythm */
  .intro-band { padding-top: 44px; }
  .intro-band + .split { margin-top: 32px; }
}

@media (max-width: 768px) {
  /* Hero body — flow freely, no width constraint */
  .hero-body {
    max-width: 100%;
    font-size: 0.95rem;
  }

  /* The hero CTA treatments are identical on desktop and mobile
     (2026-08-08, Ed): GET IN TOUCH is the solid green pill, OUR
     SERVICES is the outline. The old rule here stripped the fill off
     whichever button was filled, so both read as outlines on a phone
     and the hero had no primary action at the width where it matters
     most. Both stay full width in the stacked layout below. */
}

@media (max-width: 600px) {
  :root { --page-pad: 20px; }

  /* CTA headline: DERIVED from the longest line, the same way the
     stacked commodities size is (2026-08-10, Ed). The band is one
     column here, so the measure is exactly 100vw - 2 * 20px, and the
     longest CTA line on the site is 9.474em. Fitting it needs
     9.474f <= 100vw - 40, i.e. f <= 0.1056vw - 4.22, which at 360px
     allows 0.0939vw; 9.1vw keeps ~3% headroom there and more above.
     The 2.8rem cap is the unchanged desktop-floor value and still
     governs from about 492px up, so nothing above that width moves.
     Re-derive if a longer CTA headline is ever authored. */
  .cta-headline { font-size: clamp(2rem, 9.1vw, 2.8rem); }

  /* Hero block — full width with the shared --page-pad margins */
  .hero-block {
    margin-left: var(--page-pad);
    margin-right: var(--page-pad);
    max-width: calc(100vw - 2 * var(--page-pad));
    gap: 1.1rem;
  }

  /* Shift the centered text block up so the stacked CTAs clear the scroll
     indicator on short viewports. Margin on the block moves it within the
     centering; padding on .hero would drag the indicator up with it because
     .hero-scroll anchors to .hero-content's bottom edge. */
  .hero-block { margin-bottom: 130px; }

  /* Stats: single column on mobile */
  .stats-inner {
    grid-template-columns: 1fr;
    gap: 0;
  }

  .stat,
  .stat:nth-child(2),
  .stat:nth-child(n+3),
  .stat:nth-child(4) {
    border-right: none;
    border-top: 1px solid rgba(145, 162, 172, 0.2);
    padding: 28px 0;
    margin-top: 0;
  }

  .stat:first-child {
    border-top: none;
    padding-top: 0;
  }

  /* Left-aligned, not centered: mobile copy sits on ONE edge, and a
     centered legal line floats off it. (The footer's brand block and
     parent attribution are the two sanctioned centered exceptions,
     see the mobile footer block above; the legal strip is not one.) */
  .footer-bottom { flex-direction: column; gap: 10px; align-items: flex-start; text-align: left; }

  .hero-ctas { flex-direction: column; }

  .hero-ctas .btn-pill { justify-content: center; }
}

/* ============================================================
   WP THEME ADDITIONS (not in prototype styles.css)
   ============================================================ */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .hero-scroll-line { animation: none; }
  /* Belt and suspenders: the main.js loader already never attaches the
     hero video sources under reduced motion, so this hides an element
     that could only ever show its poster anyway. */
  .hero-video-wrapper video { display: none; }
}

/* Pre-reveal initial states, applied only when animations will run. */
html.js .hl { opacity: 0; transform: translateY(48px); }
html.js .hero-eyebrow,
html.js .hero-body,
html.js .hero-ctas { opacity: 0; transform: translateY(12px); }
html.js .page-header [data-ph-fade] { opacity: 0; transform: translateY(16px); }
html.js .hero-scroll { opacity: 0; }
/* Box and stat reveals: initial state here, class flip from the
   IntersectionObserver in main.js, motion via CSS transition
   (cubic-bezier below = power2.out). The resting .is-revealed state is
   transform: none so revealed copy sits exactly on the copy rail; a
   reveal that plays late or not at all can only ever leave the element
   hidden mid-fade, never parked off the rail once revealed. */
html.js [data-animate="box"] {
  opacity: 0;
  transform: translateX(40px);
  transition: opacity 0.75s cubic-bezier(0.33, 1, 0.68, 1), transform 0.75s cubic-bezier(0.33, 1, 0.68, 1);
}
html.js [data-animate="stat"] {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity 0.65s cubic-bezier(0.33, 1, 0.68, 1), transform 0.65s cubic-bezier(0.33, 1, 0.68, 1);
}
/* Mobile pre-reveal offset is VERTICAL: at <=900px the boxes are
   full-bleed, and a translateX offset extended the page 40px past the
   right viewport edge while un-revealed — scrollable overflow that
   iOS let the user drag sideways (the drag-the-page-off-screen bug).
   The reveal becomes a rise-up on mobile, matching the stats. */
@media (max-width: 900px) {
  html.js [data-animate="box"] { transform: translateY(24px); }
}

html.js [data-animate="box"].is-revealed,
html.js [data-animate="stat"].is-revealed,
html.js [data-animate="box-inner"].is-revealed {
  opacity: 1;
  transform: none;
}

/* SPLIT BOX REVEAL ON MOBILE reveals the COPY, not the panel
   (2026-08-08, Ed, LOCKED). At <=900px the split stacks image over
   box and the navy box sits 40px up over the photo by design (the
   overlap grammar). Fading the PANEL through that overlap painted a
   translucent navy rectangle across the bottom of the photograph for
   the length of the reveal, with the box shadow smearing another
   ~56px of navy above it: it read as a rendering fault, not as
   motion. So on mobile the panel is simply there — solid, static,
   correct from the first paint — and the reveal moves to the copy
   inside it, which never overlaps any image. The desktop reveal is
   untouched: there the panel slides in from the right over the image
   column, which is the whole point of the composition.
   .split__box-inner is a plain wrapper around the box content; it
   carries no layout of its own so the panel padding still sets the
   copy rail. */
html.js [data-animate="box-inner"] { opacity: 1; } /* inert on desktop */

@media (max-width: 900px) {
  html.js .split__box[data-animate="box"] {
    opacity: 1;
    transform: none;
    transition: none;
  }
  html.js .split__box [data-animate="box-inner"] {
    opacity: 0;
    transform: translateY(24px);
    transition: opacity 0.75s cubic-bezier(0.33, 1, 0.68, 1), transform 0.75s cubic-bezier(0.33, 1, 0.68, 1);
  }
  /* Restated here, after the pre-reveal rule above: the generic
     .is-revealed rule ties this selector's specificity and loses on
     source order, which would strand the copy hidden. */
  html.js .split__box [data-animate="box-inner"].is-revealed {
    opacity: 1;
    transform: none;
  }
  html.js .split__box [data-animate="box-inner"].is-settled {
    transition: none;
  }
}

/* The hidden-document safety valve settles instantly: transition
   timelines are frozen while a document never renders, so a reveal
   with a transition would report the un-revealed computed values
   forever. is-settled drops the transition so the resting values
   apply the moment the class lands. */
html.js [data-animate="box"].is-settled,
html.js [data-animate="stat"].is-settled,
html.js [data-animate="box-inner"].is-settled,
html.js [data-animate="rings"].is-settled .lm-ring {
  transition: none;
}

/* SEISMIC RING INTRO (Contact location map; draw-in 2026-08-13, filled
   zones 2026-08-14, REPLACED BY A RADIAL EXPANSION 2026-08-19, Ed).
   The three mileage zones now arrive the way rings arrive on a
   seismograph: each one scales up out of the CRONIMET marker to its
   full size, 100 first, then 250, then 500, 200ms apart, easing out,
   the whole sequence done in 1.2s. It plays once, when the big map
   first scrolls into view. Nothing else in the section animates on
   entry, so a reveal that plays late or never can only ever leave the
   ZONES un-arrived, never the map or its labels.

   The old draw-in (a dashoffset on the edge with the fill following a
   quarter second behind) is retired. It drew each ring as an outline
   being traced, which read as a diagram being constructed; this reads
   as reach travelling outward from the terminal, which is the point of
   the map.

   THE ORIGIN IS FREE. Every ring is a circle centred on the marker, so
   transform-box: fill-box with transform-origin: center puts the scale
   origin on the marker without a single hardcoded coordinate: the 100
   ring's own bounding box is centred on 505.4, 348.2, and so are the
   other two to within a unit. Move the marker and this follows it.

   EACH RING STARTS AT THE MARKER, NOT AT A SHARED SCALE. The starting
   scale is the marker's radius over the ring's own radius, which is
   why the three numbers differ: 11.5/54.8 for the 100 ring, 11.5/137
   for the 250, 11.5/274 for the 500. A single shared scale would start
   the three at three different sizes, which is not what leaving one
   marker looks like.

   THE EDGES DO NOT FATTEN. A scale transform scales the stroke with
   the shape, and from 0.042 to 1 that is a stroke growing twenty five
   fold in front of the visitor. non-scaling-stroke holds every zone
   edge at a constant CSS pixel width instead. It is declared as an
   attribute on the sprite paths rather than here, because unlike fill
   and stroke-width it is NOT an inherited property and so cannot be
   set on the <use> and reach the clone; see render.php. The widths
   below are the values the rings already render at, so the settled map
   is unchanged: the big map's 2 user units at a 620px render was
   1.29px, and the small maps' 3 units across their 312 to 460px range
   was 0.98 to 1.44px.

   Only transform and opacity animate, so this costs no layout and no
   repaint. Gated on html.js like every other pre-reveal state, which
   functions.php only stamps when the visitor has NOT asked for reduced
   motion, so reduced motion never applies the un-arrived state and all
   three zones are simply present, settled, from the first paint. Only
   the big map's figure carries data-animate="rings". */
html.js [data-animate="rings"] .lm-ring {
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
  transition: transform 0.8s cubic-bezier(0.22, 0.61, 0.36, 1),
              opacity 0.3s ease-out;
}
html.js [data-animate="rings"] .lm-ring--100 { transform: scale(0.21);  transition-delay: 0s; }
html.js [data-animate="rings"] .lm-ring--250 { transform: scale(0.084); transition-delay: 0.2s; }
html.js [data-animate="rings"] .lm-ring--500 { transform: scale(0.042); transition-delay: 0.4s; }

html.js [data-animate="rings"].is-revealed .lm-ring {
  transform: none;
  opacity: 1;
}

/* ------------------------------------------------------------
   INTERIOR PAGES (legal shells, 404, fallback content)
------------------------------------------------------------ */
.page-interior {
  background: var(--off-white);
  color: var(--blue);
  padding: calc(var(--nav-h) + 80px) var(--copy-edge) 120px;
  min-height: 60vh;
}

/* Headered interior (2026-09-10): a text-only page that opens with the
   standard page-header band (the legal pages) drops the wrapper's nav
   offset, because the band already clears the fixed nav. 80px plus the
   first heading's own top margin lands the copy ~120px under the navy,
   the same breathing room the header gets before a split. */
.page-interior--headered { padding-top: 80px; }

.page-interior-inner {
  max-width: 840px;
  /* reading measure stays 840; the section's --copy-edge padding puts
     the column on the shared left edge (ragged right, never centered) */
  margin: 0 auto 0 0;
}

.page-interior-title {
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.025em;
  line-height: 0.92;
  font-size: clamp(2.2rem, 3.8vw, 4.5rem);
  margin-bottom: 32px;
}

.page-interior-inner p {
  line-height: 1.65;
  margin-bottom: 1em;
  /* Widow rule: no single-word last lines in the legal copy at the
     840px measure. Progressive; browsers without text-wrap keep the
     plain break. */
  text-wrap: pretty;
}

.page-interior-inner h2,
.page-interior-inner h3 {
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin: 1.6em 0 0.6em;
  text-wrap: balance;
}

/* Hanging indent (standing rule): markers stay OUTSIDE in the 1.4em
   gutter the padding opens, so wrapped legal copy aligns with its own
   first line, never under the disc or the number. */
.page-interior-inner ul,
.page-interior-inner ol {
  list-style-position: outside;
  padding-left: 1.4em;
  margin-bottom: 1em;
}

.page-interior-inner ul { list-style-type: disc; }
.page-interior-inner ol { list-style-type: decimal; }

.page-interior--404 {
  background: var(--blue);
  color: var(--metal);
}

.page-interior--404 .page-interior-title {
  color: var(--white);
}

.page-interior--404 p {
  margin-bottom: 1.5em;
}

/* Mobile band rhythm (2026-08-08): the interior wrapper's bottom
   padding joins the same 56px set as every other band at <=900px, so
   the legal pages and the 404 end into the footer on the standard
   seam (the legal pages' closing CTA band was removed 2026-09-11). */
@media (max-width: 900px) {
  .page-interior { padding-top: calc(var(--nav-h) + 56px); padding-bottom: 56px; }
  /* Band rhythm: under the page-header band the wrapper joins the
     56px set like every other band transition. Must follow the rule
     above (same specificity; later wins). */
  .page-interior--headered { padding-top: 56px; }
}

@media (max-width: 768px) {
  .page-interior { padding-left: var(--page-pad); padding-right: var(--page-pad); }
}

/* ------------------------------------------------------------
   CONTACT FORM SECTION — one full-width NAVY band (2026-08-08
   redesign, Ed). The previous composition is retired in full: the
   white surface, the navy panel bleeding off the left viewport
   edge, the right-bleed terminal photograph and the Terminal
   Operations text block that sat on the white are all gone. The
   navy is the SECTION now, not a panel inside it, so there is no
   bleed math left here at all: the band is simply full width and
   the copy sits on the standard --copy-edge padding like every
   other band on the site.

   Page rhythm is now navy header, navy form band, grey-blue map
   section, navy footer. The header and the form band read as one
   continuous navy field, with the page header's right-bleed figure
   hanging across the seam.

   Desktop: two columns on the locked copy edge, the form left and
   the locked contact block right, separated by the site's standard
   80px gutter so the block never crowds the fields. Mobile (≤900):
   stacked, form first, contact block below, both full width on the
   shared copy edge.

   The contact block is reversed-out type, not a card: the white
   info card that sat here at launch went in the same 2026-08-08
   revision, along with the white surface and the left-bleed panel.
   Nothing on this page is a white card any more.

   The fields keep the SCREENED treatment unchanged (they were
   always navy-backed, so the redesign asks nothing of them):
   filled with white at 7% over the navy, a soft lift (#162A4B
   effective), not a white box, inside a visible grey-70 border on
   all four sides (3.85:1 on navy, 3.2:1 on the fill, both past
   the 3:1 non-text minimum) at 2px so the red error flip stays
   reflow-free. White input text on the screened fill (14.3:1),
   white labels at readable size, green required glyphs (green on
   navy is 6.1:1, past even the 4.5:1 text bar; aria-required
   carries the semantics), a green focus ring on every field and
   the button, red error borders (4.6:1 on navy, 3.8:1 on the
   screened fill) with white message text carrying the words
   (17.2:1), and the standard green pill submit with navy text
   whose hover drops the fill to transparent with white text and a
   green border (the on-dark pill grammar). The audience skews
   older; the fields must read unmistakably as form fields. The
   10px input radius is the ONLY sanctioned exception to the
   sharp-or-pill corner rule (Ed, 2026-08-07).
------------------------------------------------------------ */
/* Band 1: the white intro. Horizontal padding lives on __inner so the
   navy band below can be a full-width child of this same section with
   no bleed math (--copy-edge inside it still resolves its 100% against
   the full-width section, which is what the token needs).

   THE #form ANCHOR LANDS HERE, on the section root (2026-08-15, Ed,
   reversing the 2026-08-08 decision that landed CTAs on the navy band
   below). Landing on the band put "TELL US WHAT YOU ARE MOVING." above
   the viewport, so a CTA arrival read as a bare form with its heading
   cut off. The offset clears the fixed nav plus breathing room, and
   the intro band's own top padding does the rest: the heading and its
   supporting line sit fully visible above the form at every width.
   The anchor-landing hold in main.js (which keeps the nav shown
   through the programmatic scroll) is target-agnostic and needs no
   change; scroll-margin is honored by the smooth scroll and by the
   reduced-motion instant jump alike. */
.form-section {
  background: var(--white);
  padding: 0;
  scroll-margin-top: calc(var(--nav-h) + 24px);
}

.form-section__inner {
  max-width: calc(var(--content-max) + 2 * var(--copy-edge));
  margin: 0 auto;
  padding: 96px var(--copy-edge) 64px;
}

.form-section .form-heading {
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.025em;
  line-height: 0.92;
  font-size: clamp(2.2rem, 3.8vw, 4.5rem);
  color: var(--blue);
  margin-bottom: 16px;
}

/* grey-90 on white is 7.9:1 (AA); metal would be ~2.8:1 */
.form-section .form-intro {
  color: var(--grey-90);
  line-height: 1.65;
  /* No measure cap (2026-08-08, Ed): the supporting line must sit on
     ONE line at desktop, and the old 50ch cap broke this 88-character
     sentence in two. It runs about 650px, so it fits the content
     column at every desktop width and wraps naturally once stacked. */
  max-width: none;
  margin-bottom: 0;
}

/* Band 2: the navy form band, full viewport width. The #form anchor
   moved OFF this band and up to the section root (2026-08-15, see the
   landing note above): landing here skipped the white intro and cut
   the section heading off. Nothing targets the band by fragment any
   more, so it carries no scroll-margin. */
.form-band {
  background: var(--blue);
  padding: 90px var(--copy-edge) 120px;
}

.form-band__inner {
  max-width: var(--content-max);
  margin: 0 auto;
}

/* Two columns on the copy edge: form left, contact block right, with
   the site's standard 80px gutter between them (the same one the CTA
   band and the service-band figure use). The right track is fixed
   within a narrow range so the block keeps a stable measure; the form
   takes the rest. align-items: start pins the contact block to the top
   of the column, level with the form, and leaves the navy below it
   deliberately clean rather than centering it in a tall column. */
.form-section__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(300px, 380px);
  column-gap: 80px;
  align-items: start;
}

/* The fields keep their established measure; the track can be wider
   than the form on generous screens, and that extra width is
   breathing room to the right of the fields, not stretched inputs. */
.form-section .forminator-ui.forminator-custom-form {
  max-width: 680px;
}

/* ------- The contact block, reversed out on the navy -------
   Plain type, not a card (2026-08-08 revision, Ed): the site's eyebrow
   style for the label, the same 40px green rule the other dark bands
   use under it, then the locked contact block. Hierarchy carries the
   scan: the company name and the two links are white with weight, the
   address and the fax number sit quieter in Metal Grey, which is
   6.53:1 on navy and the brand body color on dark.

   Reveal: opacity only. The generic [data-animate="box"] pre-reveal is
   a translateX(40px), and this column's right edge sits ON the copy
   rail, so a rightward offset would push it into the viewport edge
   while un-revealed. That is the horizontal-overflow trap the
   no-overflow rule names, and it is also what would flash the block
   across the band. It fades in place instead. */
.form-contact { align-self: start; }

html.js .form-contact[data-animate="box"] { transform: none; }

/* Measured, not assumed: with align-items: start on the grid, the
   eyebrow's top and Forminator's first field label top land on exactly
   the same line (both at the grid's top edge, 0px apart at 1440), so
   no nudge is needed. The margin reset only removes .label-sm's own
   0.75rem, which would otherwise double up with the rule's margin
   below. Re-measure if either component's box changes. */
.form-contact__label { margin-bottom: 0; }

.form-contact .headline-rule { margin: 0.9rem 0 1.6rem; }

.form-contact__company {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 600;
  font-size: 1rem;
  line-height: 1.8;
  color: var(--white);
}

.form-contact__address {
  font-family: 'Source Sans 3', sans-serif;
  font-size: 1rem;
  line-height: 1.8;
  color: var(--metal);
  margin-bottom: 1.25rem;
}

.form-contact__line {
  font-family: 'Source Sans 3', sans-serif;
  font-size: 1rem;
  line-height: 1.9;
  color: var(--metal);
}

/* The locked link standard on a dark surface: bold white, no underline
   at rest, green underline on hover and keyboard focus. */
.form-contact__link {
  color: var(--white);
  font-weight: 700;
  text-decoration: none;
  text-decoration-color: var(--green);
  text-decoration-thickness: 2px;
  text-underline-offset: 3px;
}
.form-contact__link:hover,
.form-contact__link:focus-visible { text-decoration-line: underline; }
.form-contact__link:focus-visible {
  outline: 2px solid var(--green); /* 6.1:1 on navy */
  outline-offset: 3px;
}

/* ------- Forminator overrides: the boxed-field spec -------
   Vertical rhythm (2026-08-07 fix): ONE spacing value, 22px,
   between every field group at every width. The grid row-gap
   covers columns stacking inside a row, the row margin covers
   row-to-row, and both use the same value. The !importants are
   load-bearing: Forminator's grid stylesheet ties our
   specificity and loads after main.css — its 20px row margin
   was silently beating our value, and its responsive rule adds
   margin-bottom: 20px to stacked columns at narrow widths,
   which doubled with the row-gap into a 40px hole below the
   Name field while every other gap sat at 20px. Zeroing the
   column margins keeps the rhythm uniform, and it holds when a
   validation message appears because the message flows inside
   its own field group. */
.form-section .forminator-ui.forminator-custom-form .forminator-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 22px 24px;
  margin-bottom: 22px !important;
}

.form-section .forminator-ui.forminator-custom-form .forminator-row:last-child {
  margin-bottom: 0 !important;
}

.form-section .forminator-ui.forminator-custom-form .forminator-col {
  float: none;
  width: auto;
  padding: 0;
  margin: 0 !important;
}

.form-section .forminator-ui.forminator-custom-form .forminator-col-12 {
  grid-column: 1 / -1;
}

/* Labels above the fields at readable size, not micro-labels;
   white on the navy panel (17.2:1) */
.form-section .forminator-ui.forminator-custom-form .forminator-field .forminator-label {
  font-family: 'Source Sans 3', sans-serif !important;
  font-weight: 600 !important;
  font-size: 0.95rem !important;
  letter-spacing: 0.01em;
  color: var(--white) !important;
  margin-bottom: 8px;
}

/* Green required glyph, back from the original navy build: on navy
   green is 6.1:1 — past the 4.5:1 text bar, so the marker passes AA
   even read as text; aria-required carries the semantics */
.form-section .forminator-ui.forminator-custom-form .forminator-required {
  color: var(--green);
  margin-left: 2px;
}

.form-section .forminator-ui.forminator-custom-form .forminator-input,
.form-section .forminator-ui.forminator-custom-form .forminator-textarea {
  width: 100%;
  /* The screened-back fill: white at 7% over the panel navy reads as
     a soft lift (#162A4B effective; white text on it is 14.3:1), and
     the grey-70 border is visible on all four sides (3.85:1 on navy,
     3.2:1 on the fill, both past the 3:1 non-text minimum). The
     border keeps the white-box build's 2px width, so field geometry
     is unchanged and the red error flip causes no reflow. */
  background: rgba(255, 255, 255, 0.07) !important;
  border: 2px solid var(--grey-70) !important;
  border-radius: 10px !important; /* sanctioned corner exception, inputs only */
  color: var(--white) !important;
  caret-color: var(--white);
  font-family: 'Source Sans 3', sans-serif !important;
  font-size: 1.05rem !important;
  line-height: 1.4 !important;
  padding: 14px 18px !important;
  height: auto !important;
  box-shadow: none !important;
  outline: 2px solid transparent; /* becomes a real outline in forced-colors mode */
  outline-offset: 2px;
  transition: border-color 0.2s ease, background-color 0.2s ease,
              box-shadow 0.2s ease, outline-color 0.2s ease;
}

/* No field carries placeholder text today (03-forminator.php seeds
   every placeholder empty), but if one ever does: grey-50 on the
   screened fill is 5.4:1, past the 4.5:1 text bar. Opacity pinned
   because Firefox dims placeholders by default, which would undercut
   the measured ratio. */
.form-section .forminator-ui.forminator-custom-form .forminator-input::placeholder,
.form-section .forminator-ui.forminator-custom-form .forminator-textarea::placeholder {
  color: var(--grey-50) !important;
  opacity: 1 !important;
}

/* Hover: the original navy build's soft green glow as a pre-focus
   hint (decorative enhancement; the focus ring below is the state
   that carries the accessibility requirement) */
.form-section .forminator-ui.forminator-custom-form .forminator-input:hover,
.form-section .forminator-ui.forminator-custom-form .forminator-textarea:hover {
  box-shadow: 0 0 0 3px rgba(82, 174, 50, 0.35) !important;
}

/* The focus state keyboard users can never lose: a solid green ring
   offset from the white box, over the navy panel (green on navy is
   6.1:1, well past the 3:1 non-text minimum — the ring that failed
   on the light surfaces works here) */
.form-section .forminator-ui.forminator-custom-form .forminator-input:focus,
.form-section .forminator-ui.forminator-custom-form .forminator-textarea:focus {
  border-color: var(--grey-70) !important; /* re-asserted: Forminator styles focus borders */
  outline: 2px solid var(--green) !important; /* !important: Forminator resets focus outlines */
  outline-offset: 2px;
  box-shadow: none !important;
  /* the original build's slight extra lift under the ring; white
     text is still 13.1:1 on the 10% fill */
  background: rgba(255, 255, 255, 0.1) !important;
  color: var(--white) !important;
}

/* Browser autofill repaints to the screened fill with white text.
   Autofill cannot show an alpha fill (the inset-shadow trick needs a
   solid), so it paints #162A4B, the blended equivalent of the 7%
   screen over the panel navy. */
.form-section .forminator-ui.forminator-custom-form .forminator-input:-webkit-autofill {
  -webkit-box-shadow: 0 0 0 1000px #162a4b inset !important;
  -webkit-text-fill-color: var(--white) !important;
  caret-color: var(--white);
}

.form-section .forminator-ui.forminator-custom-form .forminator-textarea {
  min-height: 170px !important; /* Forminator's own min-height rule carries !important */
  resize: vertical;
}

/* Inline validation on the navy panel: the grey-70 border flips red,
   legible against both surfaces it borders (3.8:1 on the screened
   fill, 4.6:1 on navy, both past the 3:1 non-text minimum), and the
   plain-language message under the field is white semibold (17.2:1):
   the text carries the words in the surface's safe color, the border
   carries the alert color, same logic as the light build's navy
   messages. */
.form-section .forminator-ui.forminator-custom-form .forminator-field.forminator-has_error .forminator-input,
.form-section .forminator-ui.forminator-custom-form .forminator-field.forminator-has_error .forminator-textarea {
  border-color: var(--red) !important;
  box-shadow: none !important;
}

.form-section .forminator-ui.forminator-custom-form .forminator-error-message {
  background: transparent !important;
  border: 0 !important;
  padding: 6px 0 0 !important;
  color: var(--white) !important;
  font-weight: 600;
  font-size: 0.9rem;
}

/* Submit: the standard green pill, navy text on green fill (locked
   button standard; white on green fails AA) */
.form-section .forminator-ui.forminator-custom-form .forminator-button-submit {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 700 !important;
  font-size: 0.88rem !important;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  background: var(--green) !important;
  color: var(--blue) !important;
  border: 1.5px solid var(--green) !important;
  border-radius: 100px !important;
  padding: 16px 36px !important;
  margin-top: 10px;
  cursor: pointer;
  transition: background 0.22s ease, color 0.22s ease, border-color 0.22s ease;
}

/* Hover drops the fill (the on-dark pill grammar, back from the
   original navy build): green border, white text on the navy panel
   (17.2:1); the navy-fill hover was the on-light state and would
   dissolve into the panel */
.form-section .forminator-ui.forminator-custom-form .forminator-button-submit:hover {
  background: transparent !important;
  border-color: var(--green) !important;
  color: var(--white) !important;
}

/* Keyboard focus: the SAME ring as .btn-pill:focus-visible, so the
   site has one focus treatment rather than two (2026-08-10). This
   button is not the .btn-pill component (Forminator renders it), so
   the pill rule cannot reach it; the values are duplicated here on
   purpose and the two rules must be changed together.

   Navy inner band, grey-10 outer band, matching the pill exactly.
   The outward band is grey-10 on the .form-band navy behind it:
   13.89:1, far past the 3:1 non-text minimum. The inward band is
   navy against the button's own green fill: 6.11:1.

   box-shadow rather than outline, per the pill: it follows the
   button's 100px radius, and it is not subject to any native control
   quirk. !important is required, not decorative: Forminator's sheet
   loads later and its [data-design="default"] .forminator-button
   rules set box-shadow, including one on :active that would otherwise
   out-specify this rule when a keyboard visitor presses Enter (that
   fires :active and :focus-visible together). The transparent outline
   is the forced-colors fallback used on the pill and on the fields
   above: box-shadow is dropped in forced-colors mode and the outline
   becomes a real system-coloured ring.

   NOTE for future QA, learned the hard way here: this button computes
   transition: 0.2s (an ALL-property transition, because Forminator's
   later sheet overrides the transition list set above, which carries
   no !important). In a non-rendered document the transition clock is
   frozen at t=0, so getComputedStyle reports the PRE-state of any
   transitioned property forever, including for a forced inline
   !important probe. That made the previous outline rule read as
   outline-width: 0px and look broken when it was not. Suppress
   transitions before trusting any computed focus style here. */
.form-section .forminator-ui.forminator-custom-form .forminator-button-submit:focus-visible {
  outline: 2px solid transparent !important;
  outline-offset: 3px;
  box-shadow: 0 0 0 3px var(--blue), 0 0 0 6px var(--grey-10) !important;
}

/* Response messages: white text on the navy panel (17.2:1), a green
   rule accent for success and a red one for errors (decorative
   accents; the text carries the outcome, and Forminator's default
   pink boxes are off-brand) */
.form-section .forminator-ui.forminator-custom-form .forminator-response-message {
  background: transparent !important;
  border: 0 !important;
  border-left: 2px solid var(--green) !important;
  border-radius: 0 !important;
  color: var(--white) !important;
  font-size: 1rem;
  line-height: 1.6;
  padding: 4px 0 4px 14px !important;
  margin: 0 0 28px !important;
}

.form-section .forminator-ui.forminator-custom-form .forminator-response-message.forminator-error {
  border-left-color: var(--red) !important;
}

.form-section .forminator-ui.forminator-custom-form .forminator-response-message p {
  color: var(--white) !important;
}

/* ------- The success state (2026-08-09, Ed) -------
   A successful submission REPLACES the form with this panel, in the
   space the form occupied, and it stays replaced for the page view:
   the form does not come back, and there is deliberately no "send
   another message" control (the reset is a reload or a fresh visit).
   Forminator carries the same decision in its own settings
   (submission-behaviour: behaviour-hide, autoclose off, seeded by
   03-forminator.php), so the fields cannot return and the
   confirmation can never fade out on a timer, which is what made the
   old one so easy to miss. The wrap/panel swap and the scroll live in
   main.js (CONTACT FORM SUCCESS section).

   Nothing new is invented here. The headline is the .form-heading
   display treatment reversed out for navy, the divider is the site's
   standard 40px .headline-rule, the mark is a literal text character
   in Source Sans 3 (which carries U+2713) exactly as the green arrow
   is a literal character, and the corners stay square: no card, no
   box, no shadow.

   Deliberately NOT animated in. An entrance transition rides the
   rendering pipeline, and a pipeline that never runs strands the
   element at opacity 0; this is the one piece of copy on the site
   that must never fail to appear. The swap plus the scroll carries
   the state change on its own. */
.form-section__form-wrap[hidden],
.form-success[hidden] { display: none; }

.form-success {
  padding: 0 0 8px;
  /* main.js scrolls the panel here on submit, so the visitor sees the
     state change wherever they were in the form. Same offset as the
     band's own anchor: clears the fixed header at every width, worst
     case (header shown). Reduced motion already flips the html
     scroll-behavior to auto and lands at the same place. */
  scroll-margin-top: calc(var(--nav-h) + 24px);
}

/* Focus is moved here programmatically so the confirmation is
   announced; the panel itself is the visible feedback, so it takes no
   ring for that. A keyboard visitor who tabs back to it still gets
   one. */
.form-success:focus { outline: none; }
.form-success:focus-visible {
  outline: 2px solid var(--green); /* 6.1:1 on navy */
  outline-offset: 8px;
}

/* Decorative and aria-hidden, so the text-contrast bar does not apply
   to it; green on navy is 6.1:1 in any case. Green as a glyph and a
   1px rule only, never as a fill behind type. */
.form-success__check {
  font-family: 'Source Sans 3', sans-serif;
  /* 900, the display weight, so the mark reads as deliberate against
     the headline under it rather than as a stray tick. Source Sans 3
     carries U+2713 in every weight the theme self-hosts, Black
     included, so the heavier face is a real glyph and not a synthesized
     one. U+2714 (HEAVY CHECK MARK) would be the obvious way to add
     weight and is deliberately NOT used: it is an emoji-set character,
     and some platforms render it in emoji presentation. */
  font-weight: 900;
  font-size: 3.25rem;
  line-height: 1;
  color: var(--green);
  margin-bottom: 16px;
}

/* White on navy is 17.2:1. Wraps to a second line on a phone rather
   than overflowing, per the no-overflow rule. */
.form-success__headline {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.025em;
  line-height: 0.92;
  font-size: clamp(2.2rem, 3.8vw, 4.5rem);
  color: var(--white);
}

/* Body copy on solid navy is Metal Grey, 6.53:1, the brand rule. */
.form-success__line {
  font-family: 'Source Sans 3', sans-serif;
  font-size: 1.05rem;
  line-height: 1.7;
  color: var(--metal);
  max-width: 34rem;
}

@media (max-width: 900px) {
  /* Mobile band rhythm (2026-08-08): the white intro band and the navy
     band each carry the shared 56px, so the seam between them reads
     like every other band transition on the site. */
  .form-section__inner { padding: 64px var(--page-pad) 56px; }
  .form-band { padding: 56px var(--page-pad); }
  /* Stacked: white intro band, then the navy band with the form full
     width and the contact block below it, everything on the shared
     mobile copy edge. Nothing bleeds here. */
  .form-section__grid { grid-template-columns: 1fr; row-gap: 48px; }
  .form-section .forminator-ui.forminator-custom-form { max-width: none; }
  .form-section .forminator-ui.forminator-custom-form .forminator-row {
    grid-template-columns: 1fr;
  }
  /* 44px tap targets on the phone and email links */
  .form-contact__link {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  /* The mark steps down with the headline so the panel keeps its
     proportions on a phone; the 48px row-gap above the contact block
     is the space below it, so the panel drops its own tail. */
  .form-success { padding: 0; }
  .form-success__check { font-size: 2.75rem; }
}


/* ------------------------------------------------------------
   LOCATION MAP. Full-width closing section (Contact page).
   Rebuilt 2026-08-19 (Ed) from one toggleable canvas into SMALL
   MULTIPLES: one quiet big map for the proximity story, six small
   maps for the detail, every port name in real HTML beneath them.
   No toggles, no Show all, no highlight groups, no tooltips.

   ONE INTERACTION, desktop only: hovering or keyboard-focusing a
   corridor row in the legend brightens that corridor on the big map
   and softens the other four to 40 percent. Pure CSS (:has), gated
   at 1241px and on a fine pointer, so it does not exist on touch or
   at mobile widths. Everything is at full strength by default, so a
   browser without :has loses nothing at all and no content is ever
   hidden. Each corridor is addressable as one set because
   data-lm-corridor is on its line group, its dot group, its leaders
   and its labels alike.

   LABELS ARE HTML, positioned in percentages over the canvas, sized
   in constant CSS pixels. SVG text scales with the drawing, which is
   unworkable across a 312px small map and a 620px big one; HTML type
   holds one legible size at every width. It also makes the collision
   problem monotone (the narrower the map, the larger the labels are
   relative to it), so the placements were solved and checked at the
   NARROWEST width each map is ever given, and every wider one is
   safe by construction. Do not change these font sizes without
   re-running the placement in scripts (see render.php).

   HALOS. Every on-map label carries a hard white 1px outline plus a
   soft 3px spread, built from stacked text-shadows. That outline is
   what the type is measured against: navy, grey-90 and the four
   route colours are all 7.4:1 or better on white, and the halo hugs
   the glyphs with no gap, so a label crossing a ring fill still
   reads at its full ratio.

   COLOUR. The five route colours follow the client PDF's logic:
   blue carries barge and Gulf, told apart by solid versus dashed;
   green the Great Lakes; rust the East Coast; plum the Southeast.
   Rust and plum are sanctioned from OUTSIDE the core palette for
   this section's routes, dots and labels ONLY (client instruction,
   2026-08-15); do not let them leak into another component. Every
   route also has its own dash pattern, mirrored in the legend by
   reusing the same .lm-routestyle--* class on a live SVG sample, so
   no corridor is ever distinguished by colour alone and the legend
   cannot drift from the map. All five clear 3:1 on the 100 mile
   fill, the deepest ground any of them crosses.

   RINGS. Filled zones at the client's strengths (35 / 25 / 15
   percent) on the big map and on the markets map; a fifth of that
   on the five corridor maps, where they are context rather than
   subject. State borders and coast sit under the fills by the
   stacking rule and are quieter than every data element.

   MARKET DOTS SWITCH ON ONE PROPERTY: --lm-show-markets on the
   section, 1 or 0, shipping OFF since 2026-08-19. It drives the
   whole market dot group on the big map and nothing else. The
   Chicago dot is not in that group: it belongs to the six
   orientation labels, so it survives the switch either way.
------------------------------------------------------------ */
.location-map {
  /* THE MARKET DOT SWITCH. 1 shows the twelve regional market dots
     on the big map, 0 hides them. One line, nothing else to touch.

     OFF (2026-08-19, Ed): the creative director compared both states
     live and chose the quieter map. Twelve unlabeled neutral dots
     scattered among 29 port dots read as more data rather than as
     more meaning, and the big map's job is the proximity story, not
     the market list. The market cities are not lost: they are named,
     labeled and colour-keyed to their ring on the Markets Within
     Reach small map, and grouped by ring in the text beneath it.
     The dots and their markup stay in place, so this is one
     character to put back. */
  --lm-show-markets: 0;

  /* Basemap. Quieter than any data element (the stacking rule), but
     dark enough that the states clearly read under the ring fills. */
  --lm-land:   rgba(4, 26, 61, 0.08);
  --lm-coast:  rgba(4, 26, 61, 0.26);
  --lm-border: rgba(4, 26, 61, 0.17);

  /* Route colours. Blue is a 45 percent shade of the tokens'
     #41b8d5; green a shade of the brand green, distinct from the
     bright terminal green; rust and plum the sanctioned pair. */
  --lm-blue:  #174d57;
  --lm-green: #255015;

  /* The ambient ripple's ring. Brand green at the same hue and
     saturation (105 degrees, 55 percent) taken down to 20 percent
     lightness, which is what it costs to be SEEN: the brand green
     itself is 1.17:1 on the 100 mile fill it travels across, so the
     original ripple was invisible by construction rather than by
     choice. This measures 3.93:1 pure and 3.14:1 composited at its
     peak opacity, both past the 3:1 non-text floor. */
  --lm-ripple: #254f17;
  --lm-rust:  #8d3d06;
  --lm-plum:  #7a2b52;

  /* Ring zones at the client's strengths. Edges are darker versions
     of each fill's own hue, measured 3.49 / 4.35 / 3.43 against the
     fill each one closes. */
  --lm-ring-100-fill: rgba(82, 174, 50, 0.35);
  --lm-ring-100-edge: #295818;
  --lm-ring-250-fill: rgba(34, 105, 116, 0.25);
  --lm-ring-250-edge: var(--lm-blue);
  --lm-ring-500-fill: rgba(4, 26, 61, 0.15);
  --lm-ring-500-edge: rgba(4, 26, 61, 0.62);

  /* The zones as they COMPOSITE on the map, solid, for the legend
     swatches: 100 = green 35% over the 250 stack, 250 = blue 25%
     over the 500 stack, 500 = navy 15% over the land tint. Re-derive
     all three if any fill changes. */
  --lm-zone-100: #84b28c;
  --lm-zone-250: #9fb4bc;
  --lm-zone-500: #c8cdd4;

  background: var(--white);
  padding: 110px var(--copy-edge) 120px;
}

.location-map__inner {
  max-width: var(--content-max);
  margin: 0 auto;
}

.location-map__headline {
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.025em;
  line-height: 0.92;
  font-size: clamp(2.2rem, 3.8vw, 4.5rem);
  color: var(--blue);
  text-wrap: balance;
}

/* The desktop block-span break is retired (2026-09-11, Ed, site-wide
   rule): the headline renders as one flowing line of text and wraps
   naturally, balance keeping the wrapped lines even. */

.location-map .headline-rule { margin: 1.5rem 0 0; }

/* The geometry sprite. The landmass and the state borders are 50KB
   between them and seven maps share them, so they live here once and
   every map pulls them in with <use>. It must take no space and paint
   nothing itself. */
.lm-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* ------- Stage: big map and its legend card ------- */
.location-map__stage {
  display: grid;
  gap: 40px;
  margin-top: 40px;
}

/* Beside the map from 1101px, which is also where the copy floor
   goes to 120px, so the card's left border lands on it with the
   headline. Below that the card stacks UNDER the map: it explains
   what is above it, and a legend above an unseen map explains
   nothing. The figure is first in the DOM, so the stacked order is
   the reading order and only the desktop layout needs the reorder. */
@media (min-width: 1101px) {
  .location-map__stage {
    grid-template-columns: minmax(240px, 440px) minmax(360px, 620px);
    gap: clamp(48px, 5vw, 88px);
    align-items: start;
    justify-content: start;
  }
  .lm-legend { order: -1; }
}

/* 620px keeps the map plus the legend inside a 1440 by 900 viewport
   with the section headline showing: 72 nav + 101 headline + 25 rule
   + 40 gap + 641 map = 879. Widen it and that stops being true. */
.location-map__figure {
  margin: 0;
  max-width: 620px;
}

/* ------- The map canvas ------- */
/* THE positioning context for every label, plate and leader: it holds
   the SVG and nothing else in flow, so its box IS the SVG box at
   every width and a percentage position is the same point as an SVG
   coordinate. Anything that must sit ON a map goes in here; anything
   in normal flow stays outside. */
.lm-map {
  position: relative;
}

.lm-map__svg {
  display: block;
  width: 100%;
  height: auto;
}

/* ------- Basemap ------- */
/* fill, stroke, stroke-width and fill-rule are inherited properties,
   which is the whole reason the <use> sprite works: styling the <use>
   element styles the cloned path. The sprite paths carry no paint of
   their own; give one a presentation attribute and this stops
   reaching it. */
.lm-land {
  fill: var(--lm-land);
  fill-rule: evenodd;
  stroke: var(--lm-coast);
  stroke-linejoin: round;
}

.lm-states {
  fill: none;
  stroke: var(--lm-border);
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* Stroke widths are user units, so they are stated per map size and
   land at roughly 1px in both. */
.lm-map--big .lm-land,
.lm-map--big .lm-states { stroke-width: 1.5; }
.lm-map--mini .lm-land,
.lm-map--mini .lm-states { stroke-width: 3; }

/* ------- Distance rings: background zones ------- */
/* THESE WIDTHS ARE CSS PIXELS, NOT USER UNITS, unlike every other
   stroke in this section: the sprite's ring paths carry
   vector-effect="non-scaling-stroke" so the seismic intro can scale a
   zone out of the marker without its edge fattening with it (see the
   intro block earlier in this file, and render.php for why the
   attribute has to live on the path). The values are what the rings
   rendered at before, so the settled map is unchanged: the big map's
   old 2 user units came to 1.29px at its 620px desktop render and
   0.67 to 1.17px across the widths below 601px, and the small maps'
   old 3 units came to 0.98 to 1.44px across their 312 to 460px range. */
.lm-ring { stroke-width: 1.3; }
.lm-map--mini .lm-ring { stroke-width: 1.1; }

@media (max-width: 600px) {
  .lm-map--big .lm-ring { stroke-width: 0.8; }
}

.lm-ring--100 { fill: var(--lm-ring-100-fill); stroke: var(--lm-ring-100-edge); }
.lm-ring--250 { fill: var(--lm-ring-250-fill); stroke: var(--lm-ring-250-edge); }
.lm-ring--500 { fill: var(--lm-ring-500-fill); stroke: var(--lm-ring-500-edge); }

/* The five corridor maps carry the rings as context, not as subject:
   a fifth of the strength, so the route and its ports are plainly the
   loudest thing on them. The markets map keeps them at full strength,
   because there they ARE the subject. */
.lm-map--faint .lm-ring--100 { fill: rgba(82, 174, 50, 0.13); stroke: rgba(41, 88, 24, 0.45); }
.lm-map--faint .lm-ring--250 { fill: rgba(34, 105, 116, 0.09); stroke: rgba(23, 77, 87, 0.40); }
.lm-map--faint .lm-ring--500 { fill: rgba(4, 26, 61, 0.055); stroke: rgba(4, 26, 61, 0.28); }

/* ------- Route lines ------- */
/* One style class per corridor, shared verbatim by the legend sample
   swatches (the same class on both paths), so colour, weight and dash
   pattern can never drift between the map and its legend. The barge
   route is the only solid line; every other corridor gets its OWN
   dash pattern, which is what keeps them apart without leaning on
   colour alone. */
.lm-route,
.lm-sample path {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.lm-routestyle--barge { stroke: var(--lm-blue);  stroke-width: 3.6; }
.lm-routestyle--gulf  { stroke: var(--lm-blue);  stroke-width: 3;   stroke-dasharray: 11 7; }
.lm-routestyle--lakes { stroke: var(--lm-green); stroke-width: 2.8; stroke-dasharray: 8 5; }
.lm-routestyle--east  { stroke: var(--lm-rust);  stroke-width: 2.8; stroke-dasharray: 3 5.5; }
.lm-routestyle--south { stroke: var(--lm-plum);  stroke-width: 2.8; stroke-dasharray: 12 4 2.5 4; }

/* A small map carries one corridor at half the width, so its line
   steps up to hold the same visual weight. */
.lm-map--mini .lm-route { stroke-width: 5; }
.lm-map--mini .lm-routestyle--gulf  { stroke-dasharray: 18 11; }
.lm-map--mini .lm-routestyle--lakes { stroke-dasharray: 13 8; }
.lm-map--mini .lm-routestyle--east  { stroke-dasharray: 5 9; }
.lm-map--mini .lm-routestyle--south { stroke-dasharray: 20 6 4 6; }

/* ------- Dots ------- */
.lm-dot { stroke: var(--white); stroke-width: 1.8; }
.lm-map--mini .lm-dot { stroke-width: 2.4; }

.lm-dot--barge, .lm-dot--gulf { fill: var(--lm-blue); }
.lm-dot--lakes { fill: var(--lm-green); }
.lm-dot--east  { fill: var(--lm-rust); }
.lm-dot--south { fill: var(--lm-plum); }

/* Regional market dots. On the big map they are drawn UNDER the route
   lines (the stacking rule) and neutral; on the markets small map each
   one takes the colour of the zone it sits in, all three measured 5:1
   or better on the fill they land on. */
.lm-mkt {
  fill: var(--grey-90);
  stroke: var(--white);
  stroke-width: 1.4;
}
.lm-map--mini .lm-mkt { stroke-width: 2.2; }
.lm-mkt--100 { fill: var(--lm-ring-100-edge); }
.lm-mkt--250 { fill: var(--lm-ring-250-edge); }
.lm-mkt--500 { fill: var(--blue); }

/* The switch. Opacity rather than display, so nothing reflows and the
   dots keep their place in the stack either way. */
.lm-map--big .lm-mkts { opacity: var(--lm-show-markets); }

.lm-leader {
  fill: none;
  stroke: rgba(4, 26, 61, 0.45);
  stroke-width: 1.6;
}
.lm-map--mini .lm-leader { stroke: rgba(4, 26, 61, 0.38); stroke-width: 2.6; }

/* ------- The CRONIMET leader (2026-08-19, Ed) -------
   A port leader is a tether: it says which dot a name belongs to, and a
   translucent hairline is enough. THE CRONIMET LEADER IS A POINTER: it
   answers "where are they", so it is solid navy at full opacity with a
   filled arrowhead at the dot end, and it is deliberately heavier than
   anything it crosses. It replaced a 2.4 unit line at 62 percent alpha,
   which was the weight of a state border and which the eye slid off.

   The widths are user units, so the leader scales with the map like
   every other drawn thing, and the arrowhead scales with it for free
   because the marker is markerUnits="strokeWidth". 4 units renders
   2.6px on the 620px big map and 6 units renders 2.2px on a 350px small
   map, both a shade over the 2px and 1.5px asked for: the brief also
   said heavier than any route line, and the barge route is 3.6 units on
   the big map and 5 on the small ones, so anything at or under those
   would have lost to the thing it has to dominate. */
.lm-leader--home {
  stroke: var(--blue);
  stroke-width: 4;
  stroke-linecap: butt; /* the arrowhead is the end, not a rounded cap */
}

.lm-map--mini .lm-leader--home {
  stroke: var(--blue);
  stroke-width: 6;
}

/* Navy on the 100 mile green fill is 7.15:1, the same measurement the
   marker's collar is built on, so the head clears the 3:1 non-text
   floor everywhere it can land. Filled, never stroked, so its edge
   stays crisp at every scale. */
.lm-arrowhead {
  fill: var(--blue);
  stroke: none;
}

/* THE STROKE WIDTH IS UNIFORM PER MAP, and that is now load bearing
   (2026-08-20, Ed). The big map used to step from 4 to 6 units below
   600px so the pointer kept its weight on a phone. It cannot any more:
   since the line stops at the head's base, render.php has to place the
   path end by adding the head's reach back on, and that reach scales
   with the stroke. A stroke that changed at a breakpoint would drag
   the tip with it and eat the clearance the marker collar needs. 4
   units still renders 2.6px on the 620px desktop map and 1.33px at a
   360px phone, against 0.5px basemap hairlines, and it is solid navy
   at full opacity where they are translucent. */

/* ------- CRONIMET marker: glow, pulse, dot ------- */
.lm-glow-stop-1 { stop-color: #52ae32; stop-opacity: 0.5; }
.lm-glow-stop-2 { stop-color: #52ae32; stop-opacity: 0.26; }
.lm-glow-stop-3 { stop-color: #52ae32; stop-opacity: 0.08; }
.lm-glow-stop-4 { stop-color: #52ae32; stop-opacity: 0; }

/* Green is only 1.2:1 on the 100 mile fill, so the navy collar is not
   decoration: it is the marker's boundary, 7.15:1 against that fill. */
.lm-homedot {
  fill: var(--green);
  stroke: var(--blue);
}

/* The pulse. transform-box: fill-box puts the origin at the circle's
   own centre, so the ring grows from the marker rather than from the
   viewBox corner. Gated on html.js like all site motion, and only on
   the big map: the small maps are static by design. */
.lm-pulse {
  fill: none;
  stroke: var(--green);
  stroke-width: 2.5;
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
}

html.js .lm-map--big .lm-pulse {
  animation: lm-pulse 2.8s cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
}

@keyframes lm-pulse {
  0%   { transform: scale(0.55); opacity: 0.7; }
  70%  { opacity: 0; }
  100% { transform: scale(2.6); opacity: 0; }
}

/* ------- The ambient ripple (2026-08-19, Ed), big map only -------
   A second, slower, wider ring than the marker pulse above: it leaves
   the marker and travels out to the 100 mile ring, then goes again.
   The point is that the eye is drawn back to Aliquippa every few
   seconds without anything blinking at it.

   THE NUMBERS ARE THE GEOMETRY, not taste. The circle is authored at
   r=11.5, the outer edge of the marker's navy collar, so the ripple
   starts exactly at the marker rather than inside it. The 100 mile
   ring's radius measures 54.8 user units from the same centre, so the
   scale ceiling is 54.8 / 11.5 = 4.77. Change the marker or the ring
   and both numbers move together.

   PERFORMANCE. One element, and only transform and opacity animate, so
   this never triggers layout and never repaints the map under it (the
   alternative, animating the r attribute, is a geometry change and
   would). vector-effect keeps the stroke at a true 1.4px however far
   the transform has scaled it, which is also what stops the ring
   thickening as it grows: a ripple that fattens as it expands reads as
   a blob rather than as a wave.

   VISIBLE, AND STILL QUIET (2026-08-19, Ed). It launched in brand
   green at 0.55 on a 1.4px line and could barely be seen, and that was
   physics rather than a slider: brand green on the green 100 mile fill
   is 1.17:1. The ring is now a dark shade of the same green (see
   --lm-ripple), 1.8px, peaking at 0.85, which reads clearly as an
   expanding ring on every ground it crosses and still clears 3:1. What
   keeps it ambient rather than alarming is that it is thin, slow and
   drawn UNDER the routes, the port dots and the type (see render.php),
   so it can never wash across anything that carries meaning.

   Gated on html.js like all site motion, which is only stamped when
   the visitor has NOT asked for reduced motion, and killed again in
   the reduced-motion block at the end of this section. */
.lm-ripple {
  fill: none;
  stroke: var(--lm-ripple);
  stroke-width: 1.8;
  vector-effect: non-scaling-stroke;
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
}

html.js .lm-map--big .lm-ripple {
  animation: lm-ripple 3.6s linear infinite;
}

/* Linear, and the fade is shaped by its own stops rather than by an
   easing curve. The site's usual power2.out was tried first and is
   wrong here: it front-loads the travel, so the ring reached the
   outer third of its journey with almost none of its opacity left and
   the wave died before it arrived. A steady expansion with a fade
   that holds through the middle is what reads as a ripple. */
@keyframes lm-ripple {
  0%   { transform: scale(1);    opacity: 0; }
  8%   { opacity: 0.85; }
  70%  { opacity: 0.34; }
  100% { transform: scale(4.77); opacity: 0; }
}

/* ------- On-map labels ------- */
/* The anchor point is the dot plus a solved offset; the modifier says
   which edge of the block sits on that point. Vertically the block is
   always centred on it. white-space: nowrap because the line breaks
   are authored in the data as <br>: a browser re-wrap would produce a
   box that was never collision checked. */
.lm-lab {
  position: absolute;
  margin: 0;
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 600;
  line-height: 1.18;
  letter-spacing: 0.01em;
  white-space: nowrap;
  pointer-events: none;
  z-index: 2;
  text-shadow:
     1px  1px 0 #fff, -1px  1px 0 #fff,  1px -1px 0 #fff, -1px -1px 0 #fff,
     1px  0   0 #fff, -1px  0   0 #fff,  0    1px 0 #fff,  0   -1px 0 #fff,
     0 0 3px #fff, 0 0 3px #fff;
}

.lm-lab--start  { transform: translateY(-50%); }
.lm-lab--middle { transform: translate(-50%, -50%); }
.lm-lab--end    { transform: translate(-100%, -50%); }

.lm-map--big  .lm-lab { font-size: 11px; }
.lm-map--mini .lm-lab { font-size: 11.5px; }

.lm-lab--barge, .lm-lab--gulf { color: var(--lm-blue); }
.lm-lab--lakes { color: var(--lm-green); }
.lm-lab--east  { color: var(--lm-rust); }
.lm-lab--south { color: var(--lm-plum); }
.lm-lab--mkt   { color: var(--grey-90); }

/* The market labels on the markets map take their zone's colour, the
   same key as their dots. All three are 7:1 or better on the halo. */
.lm-lab--b100 { color: #22490f; }
.lm-lab--b250 { color: #123f47; }
.lm-lab--b500 { color: var(--blue); }

/* River names, set along their own line. Upright, never italic (the
   site rule), and told apart from the port labels by tracking. */
.lm-river {
  position: absolute;
  margin: 0;
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.10em;
  color: var(--lm-blue);
  white-space: nowrap;
  pointer-events: none;
  z-index: 2;
  text-shadow:
     1px  1px 0 #fff, -1px  1px 0 #fff,  1px -1px 0 #fff, -1px -1px 0 #fff,
     1px  0   0 #fff, -1px  0   0 #fff,  0    1px 0 #fff,  0   -1px 0 #fff,
     0 0 3px #fff;
}

/* ------- The CRONIMET plate ------- */
/* White, square, attached to the dot by a short leader that meets its
   lower left corner, running up and right into open Pennsylvania. It
   has layout priority over everything else on the map (client rule):
   every other label was placed around it. 11.5px is not a style
   choice, it is what fits the one clear slot between the Albany and
   New York / New Jersey dots at desktop width. pointer-events: none so
   it never eats a scroll. */
.lm-plate {
  position: absolute;
  z-index: 4;
  margin: 0;
  transform: translateY(-100%);
  background: var(--white);
  box-shadow: 0 0 0 1px rgba(4, 26, 61, 0.40), 0 6px 18px rgba(4, 26, 61, 0.16);
  padding: 5px 8px;
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 800;
  font-size: 11.5px;
  line-height: 1.20;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--blue);
  pointer-events: none;
}

.lm-plate span {
  display: block;
  white-space: nowrap;
}

/* The same plate on a small map, carrying the compact copy (2026-08-19,
   Ed). It exists so all seven maps say "here they are" the same way:
   an arrow into a white plate, not an arrow into loose type. Anchoring
   differs from the big plate on purpose. The big one hangs from its
   lower left CORNER, because its position was hand-fixed to a measured
   slot; these are solver-placed like every other small-map label, so
   the solved point sits on one EDGE, vertically centred, and they reuse
   the .lm-lab--start / --end anchor transforms rather than inventing a
   second convention. The solver knows the padding below: keep the two
   in step (HOME_PAD_X / HOME_PAD_Y in work/map-label-placer). */
.lm-plate-sm {
  position: absolute;
  z-index: 4;
  margin: 0;
  background: var(--white);
  box-shadow: 0 0 0 1px rgba(4, 26, 61, 0.40), 0 4px 12px rgba(4, 26, 61, 0.14);
  padding: 4px 7px;
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 800;
  font-size: 11.5px;
  line-height: 1.20;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--blue);
  pointer-events: none;
}

/* ------- Legend card ------- */
.lm-legend {
  background: var(--white);
  border: 1px solid var(--blue);
  padding: 26px 24px 24px;
}

/* Reveal: opacity only, the same exception the form contact block
   carries. The card is copy, so its border sits ON the 120px copy
   floor with the headline, and a copy-edge element never gets a
   horizontal pre-reveal offset. */
html.js .lm-legend[data-animate="box"] { transform: none; }

/* Ring rows: informational, and built to look it. No border, no fill,
   no pointer, no hover. */
.lm-legend__rings {
  display: grid;
  gap: 14px;
}

.lm-ring-item {
  display: grid;
  grid-template-columns: 18px 1fr;
  gap: 14px;
  align-items: start;
}

/* The swatch is the zone as it appears ON THE MAP: the solid
   composited colours, not the raw fills (raw translucent fills over
   white read as empty checkboxes). */
.lm-ring-key {
  width: 18px;
  height: 18px;
  margin-top: 0.15em;
}
.lm-ring-key--100 { background: var(--lm-zone-100); box-shadow: inset 0 0 0 1px var(--lm-ring-100-edge); }
.lm-ring-key--250 { background: var(--lm-zone-250); box-shadow: inset 0 0 0 1px var(--lm-ring-250-edge); }
.lm-ring-key--500 { background: var(--lm-zone-500); box-shadow: inset 0 0 0 1px var(--lm-ring-500-edge); }

.lm-ring-text {
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--grey-90);
  text-wrap: pretty;
}

.lm-ring-miles {
  font-weight: 700;
  color: var(--blue);
  letter-spacing: 0.04em;
}

/* "Mid-Atlantic" never breaks at its hyphen. The 250 ring label is the
   one legend string carrying a hyphenated compound, and at 390-430px
   the wrap point lands exactly on the hyphen, stranding "Mid-" at a
   line end in the legend row and the markets-card band alike. The
   span moves the break to the space before the word; the label text
   itself is untouched (Jake's structure, see SOURCES.md). */
.lm-nb { white-space: nowrap; }

/* ------- Corridor rows ------- */
/* Not buttons and not links: nothing here toggles anything, and the
   card must not suggest it does. The negative side margin lets the
   desktop hover state and the focus ring breathe into the card's own
   padding while the text stays on the same rail as the ring rows. */
.lm-legend__routes {
  display: grid;
  gap: 2px;
  margin: 26px -12px 0;
  padding-top: 22px;
  border-top: 1px solid rgba(4, 26, 61, 0.15);
}

.lm-corridor {
  display: grid;
  grid-template-columns: 40px 1fr;
  gap: 14px;
  align-items: start;
  padding: 10px 12px;
}

.lm-corridor__key {
  width: 40px;
  height: 18px;
  margin-top: 0.1em;
  background: rgba(4, 26, 61, 0.06);
  display: grid;
  place-items: center;
}

.lm-sample {
  display: block;
  width: 40px;
  height: 12px;
}

.lm-corridor__name {
  display: block;
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 800;
  font-size: 0.8125rem;
  line-height: 1.35;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--blue);
}

.lm-corridor__mode {
  display: block;
  margin-top: 3px;
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.875rem;
  line-height: 1.45;
  color: var(--grey-90);
}

/* Visually hidden, still read: the note the corridor rows point at
   with aria-describedby at the widths where the highlight exists. */
.lm-vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ------- THE ONE INTERACTION ------- */
/* Desktop only. Brightening means the other four soften: nothing is
   ever hidden and nothing is ever off. The rows become focusable only
   here, from main.js, so a phone is never offered a tab stop for an
   interaction it does not have. */
@media (min-width: 1241px) {
  .lm-map--big [data-lm-corridor] {
    transition: opacity 0.18s ease;
  }

  .lm-corridor:focus-visible {
    outline: none;
    background: rgba(4, 26, 61, 0.05);
    box-shadow: 0 0 0 2px var(--white), 0 0 0 4px var(--blue);
  }

  .location-map:has(.lm-corridor--barge:focus-visible) .lm-map--big [data-lm-corridor]:not([data-lm-corridor="barge"]),
  .location-map:has(.lm-corridor--gulf:focus-visible)  .lm-map--big [data-lm-corridor]:not([data-lm-corridor="gulf"]),
  .location-map:has(.lm-corridor--lakes:focus-visible) .lm-map--big [data-lm-corridor]:not([data-lm-corridor="lakes"]),
  .location-map:has(.lm-corridor--east:focus-visible)  .lm-map--big [data-lm-corridor]:not([data-lm-corridor="east"]),
  .location-map:has(.lm-corridor--south:focus-visible) .lm-map--big [data-lm-corridor]:not([data-lm-corridor="south"]) {
    opacity: 0.4;
  }
}

@media (min-width: 1241px) and (hover: hover) and (pointer: fine) {
  .lm-corridor:hover { background: rgba(4, 26, 61, 0.05); }

  .location-map:has(.lm-corridor--barge:hover) .lm-map--big [data-lm-corridor]:not([data-lm-corridor="barge"]),
  .location-map:has(.lm-corridor--gulf:hover)  .lm-map--big [data-lm-corridor]:not([data-lm-corridor="gulf"]),
  .location-map:has(.lm-corridor--lakes:hover) .lm-map--big [data-lm-corridor]:not([data-lm-corridor="lakes"]),
  .location-map:has(.lm-corridor--east:hover)  .lm-map--big [data-lm-corridor]:not([data-lm-corridor="east"]),
  .location-map:has(.lm-corridor--south:hover) .lm-map--big [data-lm-corridor]:not([data-lm-corridor="south"]) {
    opacity: 0.4;
  }
}

/* ------- The six small maps, each in a card ------- */
/* Three per row from 1260px, two from 760px, one below that. The
   floor matters more than the ceiling: 312px is the width the label
   placements were solved at, and below it the labels, which are a
   constant pixel size, grow relative to the geography until the
   solved clearances close up. So 312 is a hard floor, not a
   preference, and every column count is chosen to clear it.

   THE THREE UP BREAKPOINT IS 1260px, NOT 1241px, and the 19px is not
   arbitrary: a desktop scrollbar takes 15px out of the width the
   copy edge resolves against, so a 1241px window only offers 986px
   of content and three columns of it come to a 308px map, under the
   floor. Measured on staging, not calculated. 1260 puts it at 315px.
   This is deliberately NOT the same number as the 1241px gate on the
   legend highlight below; they were equal by accident and only one
   of them is about map width. Do not re-unify them.

   THE GUTTER IS ONE VALUE on both axes (2026-08-19, Ed), so the six
   cards read as one grid rather than as three pairs. It came down
   from 48/32 to 28 because the borders now do the separating that
   white space used to do, and because at the narrow end every pixel
   of gutter comes straight off the map.

   THE CARD (2026-08-19, Ed). Same language as the legend card above:
   white, square, one hairline navy border, no shadow, no tint, no
   hover state. It is a container, not a control, and must never
   suggest otherwise. Grid stretch gives every card in a row the same
   height, the tallest port list sets it, and the flex column inside
   keeps the text top-aligned under the map with the slack falling to
   the bottom, so the row's top AND bottom edges line up.

   THE MAP RUNS FULL WIDTH INSIDE THE CARD, bleeding under the
   padding to the inner edges of the border. That is not a style
   choice, it is the only way to add padding without shrinking the
   map: at 1241px three padded cards cannot hold three 312px maps
   inside the content width, and at 360px a card with 20px of side
   padding leaves 278px. Either would drop the maps under the floor
   above and put the labels back into collision. So the padding
   carries the type and the map keeps exactly the width it had before
   the cards existed. Its own generous internal margin (the landmass
   occupies the middle 84 percent of the viewBox) is what keeps it
   from reading as cramped against the border. */
.lm-minis {
  display: grid;
  grid-template-columns: 1fr;
  gap: 28px;
  margin-top: 72px;
}

@media (min-width: 760px) {
  .lm-minis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 1260px) {
  .lm-minis { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.lm-mini {
  display: flex;
  flex-direction: column;
  background: var(--white);
  border: 0.8px solid var(--blue);
  padding: 22px;
}

/* The bleed. Keep these two values equal to the card's side padding
   or the map stops being the width its labels were placed against. */
.lm-mini > .lm-map {
  margin-left: -22px;
  margin-right: -22px;
}

/* ------- The card's title banner (2026-08-19, Ed) -------
   A solid navy bar across the top of the card, above the map, white
   type centred in it. It bleeds under the card padding to the inner
   edges of the border exactly as the map does, so the card reads as
   header then content rather than as a stack of loose parts. Square,
   flat, no gradient and no radius, per the site's corner rule.

   15px SENTENCE CASE, and the case is the one thing here that was not
   asked for, so here is the arithmetic that forced it. The longest
   title, "Downstream to Gulf via Mississippi River", is 39 characters.
   In the uppercase treatment these headings used to carry it measures
   345.6px at 15px and 368.6px at 16px, against a card interior of
   314.7px at the narrowest three-across width and 336.4px at the
   narrowest two-across. It does not fit on one line in caps at ANY
   size at or above the 15px floor, so the brief's own remedy (drop one
   step, keep all six matching) could not reach it. The three ways out
   were: go under 15px (breaks the floor, and puts the title below its
   own body copy), widen the cards by pushing the column breakpoints up
   (turns 1280 into two-across and 768 into one-across, two layout
   regressions nobody asked for), or drop the capitals. Sentence case
   at 15px measures 282.6px, which clears the narrowest three-across by
   12px and every other side-by-side width by more, with no layout
   change at all. If the capitals matter more than the column counts,
   put text-transform back and raise the two breakpoints to 1420 and
   820; nothing else has to move. */
.lm-mini__name {
  margin: -22px -22px 0;
  display: flex;
  align-items: center;
  min-height: 3.2em;
  padding: 10px 12px;
  background: var(--blue);
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 800;
  font-size: 15px;
  line-height: 1.25;
  letter-spacing: 0.01em;
  color: var(--white); /* 15.7:1 on the navy */
  text-wrap: balance;  /* only ever bites below 415px, where cards are stacked */
}

/* The map keeps its own bleed and takes the gap to the banner, so the
   banner reads as a header bar rather than as a caption stuck to the
   drawing. */
.lm-mini > .lm-map { margin-top: 16px; }

.lm-mini__mode {
  margin-top: 18px;
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--grey-90);
}

/* The canonical, always visible, crawlable record of every port.
   Semicolons rather than commas: several client port names carry
   their own commas, and a comma-joined run reads as twelve ports
   instead of five. */
.lm-mini__ports {
  margin-top: 10px;
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--grey-90);
  text-wrap: pretty;
}

/* The markets card carries no access mode line, so its first band
   paragraph is what follows the map and takes the mode line's gap. */
.lm-mini__band:first-of-type { margin-top: 18px; }

.lm-mini__band {
  margin-top: 10px;
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--grey-90);
  text-wrap: pretty;
}

.lm-mini__miles {
  font-weight: 700;
  color: var(--blue);
  letter-spacing: 0.04em;
}

/* ------- Caption and tagline ------- */
/* Exact client copy from the reference PDF's own disclaimer.
   grey-90: quiet, and 7.9:1 on white (grey-70 misses 4.5). */
.location-map__note {
  margin-top: 48px;
  font-family: 'Source Sans 3', sans-serif;
  font-size: 0.8125rem; /* 13px */
  line-height: 1.5;
  color: var(--grey-90);
}

.location-map__tagline {
  margin-top: 28px;
  font-family: 'Source Sans 3', sans-serif;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  line-height: 1.15;
  font-size: clamp(1.25rem, 2.2vw, 2rem);
  color: var(--blue);
  text-wrap: balance;
}

/* Widow rule, the stronger half: each sentence is nowrap, so the only
   place a line can break is BETWEEN them. */
.location-map__tagline span { white-space: nowrap; }

@media (max-width: 900px) {
  /* Mobile band rhythm: 56px like every other band. */
  .location-map { padding: 56px var(--page-pad); }
  .location-map__stage { gap: 28px; margin-top: 28px; }
  .lm-minis { margin-top: 48px; gap: 24px; }
  /* Same card, one step tighter, on the legend card's mobile padding.
     The bleed follows the padding down: keep the two in step. */
  .lm-mini { padding: 20px; }
  .lm-mini > .lm-map { margin-left: -20px; margin-right: -20px; }
  /* The banner bleeds with the card padding, so it follows it down. */
  .lm-mini__name { margin: -20px -20px 0; }
  .location-map__note { margin-top: 36px; }
  .location-map__tagline { margin-top: 20px; }
  .lm-legend { padding: 22px 20px; }
  .lm-legend__routes { margin: 22px -8px 0; padding-top: 18px; }
  .lm-corridor { padding: 8px; }
}

@media (max-width: 600px) {
  /* The big map is at its narrowest here and carries every corridor
     at once, so its orientation labels and its plate step down. The
     small maps do not: they hold one corridor each and were solved at
     this width already. */
  .lm-map--big .lm-lab { font-size: 10px; }
  .lm-plate { padding: 4px 7px; font-size: 10px; }
}

@media (prefers-reduced-motion: reduce) {
  /* Belt and braces. The html.js gate already withholds all three
     motions, because functions.php only stamps that class when the
     visitor has NOT asked for reduced motion: the ring draw-in never
     applies its undrawn state, and neither pulse nor ripple ever gets
     its animation. This restates it anyway so nothing in this section
     depends on that one line staying put, and it also covers the
     legend highlight transition, which is not gated at all. Everything
     renders in its settled state, instantly. */
  html.js .lm-map--big .lm-pulse,
  html.js .lm-map--big .lm-ripple { animation: none; }
  .lm-map--big [data-lm-corridor] { transition: none; }
}


/* Arrow glyph: the prototype loaded Source Sans 3 from Google Fonts,
   whose latin subset excludes U+2192, so every arrow on the reference
   site actually renders in the platform UI font. Source Sans 3's own
   arrow is visibly narrower. Pin arrows to the system stack so the
   approved look is reproduced deliberately rather than by accident. */
.arrow,
.hero-scroll-label + .hero-scroll-line {
  font-family: Helvetica, Arial, sans-serif;
}
