/* Manion Foods — shared styles. Mobile-first: base rules target phones, media queries scale up.

   ────────────────────────────────────────────────────────────────────────────
   WARM EDITORIAL (2026 redesign)
   ────────────────────────────────────────────────────────────────────────────
   The ground is bone and warm sand rather than white, the display face is a
   high-contrast serif, and green has moved from "the site's colour" to an
   accent that anchors sections and carries every primary action. The point is
   that a 35-year-old family distributor should read as established and
   unhurried, not as a SaaS dashboard.

   Everything below is written against the tokens in :root, which is what makes
   a retone a fifty-line edit rather than a rewrite. Keep it that way — reach
   for a token before a literal.
   ──────────────────────────────────────────────────────────────────────────── */

/* Metric-matched fallbacks. The webfonts load with font-display:swap, so the
   first paint uses these; without the overrides the swap reflowed every
   headline on the page. Numbers are eyeballed against Fraunces/Inter at 1000
   upem — close enough that the swap is a colour change, not a layout pass. */
@font-face {
  font-family: "Bitter Fallback";
  src: local("Georgia"), local("Times New Roman"), local("Times");
  /* Measured in the browser, not estimated: the same string set at 100px is
     1491px in Bitter against 1641px in Georgia — a ratio of 0.909.

     Re-measure whenever the display face changes; this number is specific to
     the pairing, not to slab serifs in general. The face before this one
     measured 0.842, so inheriting its value would have left every headline
     reflowing on swap, which is the exact failure this @font-face prevents. */
  size-adjust: 91%;
  ascent-override: 92%;
  descent-override: 24%;
  line-gap-override: 0%;
}
@font-face {
  font-family: "Inter Fallback";
  src: local("Segoe UI"), local("Helvetica Neue"), local("Arial");
  size-adjust: 100%;
  ascent-override: 96%;
  descent-override: 24%;
  line-gap-override: 0%;
}

:root {
  /* -- Colour ---------------------------------------------------------------
     Every colour token — grounds, ink, greens, dark surfaces, borders — now
     lives in css/tokens.css, which the admin panel links too so the two can no
     longer drift apart. This file keeps only what is specific to the public
     site: elevation, radii, measure and the type scale.

     css/tokens.css must be linked by the page. Not before this file —
     order does not matter, custom properties resolve after the whole cascade
     is assembled — but it must be there, or every colour below resolves to
     nothing and the page renders as unstyled text on white. */


  /* -- Elevation ----------------------------------------------------------
     Editorial layouts separate with rules and ground shifts, not drop
     shadows. What's left is a long, soft, almost-invisible lift used on
     exactly three things: photos, the sticky header, and open menus. */
  --shadow-sm: 0 1px 2px rgba(20, 24, 26, 0.05);
  --shadow: 0 8px 24px -12px rgba(20, 24, 26, 0.18);
  --shadow-lg: 0 24px 60px -24px rgba(20, 24, 26, 0.3);

  /* Crisper than the 16px the site used to run. A high-contrast serif next to
     a 16px pill reads as two different products. */
  --radius: 6px;
  --radius-sm: 4px;
  --radius-lg: 10px;
  --radius-pill: 999px;

  --max-width: 1180px;
  --measure: 68ch;

  /* How far the testimonial scroller's contents sit in from the container edge:
     the 44px arrow plus the 8px wrap gap beside it. Three rules key off this —
     the progress rail's two pseudo-elements and the "Read all reviews" button
     under them — and they have to agree or the button hangs off the left of a
     row it is supposed to sit under. Only applies where the arrows are visible;
     below 640px they are display:none and everything runs to the edge. */
  --quote-rail-inset: 52px;

  /* -- Type ---------------------------------------------------------------
     Bitter for display, Inter for everything else. Both fall back to the
     metric-matched faces above, then to real system stacks — a visitor who
     blocks Google Fonts gets Georgia over Segoe UI, which is still a serif
     display paired with a neutral sans, not a broken page.

     The display face is chosen against the company's own wordmark, which is a
     heavy low-contrast slab with blunt bracketed serifs. The previous choice
     was a high-contrast display serif — elegant, and pulling in exactly the
     opposite direction from the logo sitting above it in the header. Bitter is
     built the same way the wordmark is: low contrast, high x-height, blunt
     bracketed serifs. It also holds up at subhead sizes, which a heavier
     display slab would not. */
  --font-display: "Bitter", "Bitter Fallback", Georgia, "Times New Roman", serif;
  --font: "Inter", "Inter Fallback", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* Fluid scale. Every clamp is min / preferred / max where the preferred term
     carries a rem so the whole scale still responds to browser text size —
     a pure-vw middle term is a zoom trap. */
  --step--1: 0.875rem;
  --step-0: 1.0625rem;
  --step-1: clamp(1.2rem, 1.13rem + 0.35vw, 1.4rem);
  --step-2: clamp(1.45rem, 1.31rem + 0.7vw, 1.85rem);
  --step-3: clamp(1.8rem, 1.55rem + 1.25vw, 2.5rem);
  --step-4: clamp(2.15rem, 1.75rem + 2vw, 3.3rem);
  /* Retuned for Bitter, which sets about 8% wider than the previous display
     face. Both ends had to move, and both were measured rather than guessed:

       desktop  at 72.8px the hero headline wrapped to five lines in its
                column and stranded "people" on its own; 64px gives four.
       mobile   at 43.4px the same thing happened in the 335px column;
                41.6px gives four, so the middle term is tuned to land just
                under that at 375px rather than just over.

     A type scale is tuned to a typeface, not inherited from the last one.
     Re-check both ends if the display face changes again. */
  --step-5: clamp(2rem, 0.43rem + 9.09vw, 4rem);

  /* -0.008em, not the -0.022em a high-contrast serif wanted. Slab serifs have
     wide, blunt terminals that already close the gaps between letters; pull
     them tighter and the serifs start touching at display size. */
  --tracking-tight: -0.008em;
  --tracking-eyebrow: 0.14em;

  /* -- Spacing ------------------------------------------------------------
     4/8 rhythm. Section padding is fluid so a phone doesn't spend a third of
     its viewport on whitespace and a desktop still breathes. */
  --space-2xs: 4px;
  --space-xs: 8px;
  --space-sm: 12px;
  --space-md: 20px;
  --space-lg: 32px;
  --space-xl: 48px;
  --space-2xl: clamp(56px, 7vw, 88px);
  --space-3xl: clamp(72px, 10vw, 128px);

  /* -- Motion -------------------------------------------------------------
     One scale for the whole site, so retuning the feel is a few numbers
     rather than forty-six scattered literals. Curves are asymmetric on
     purpose: bare `ease` is the browser default and reads as unstyled.
     Things entering decelerate, things leaving accelerate.

     --ease-editorial is the expo-out curve that does most of the work in this
     redesign: it covers 80% of its distance in the first third of the
     duration, so a 700ms reveal still feels immediate while the tail gives
     the slow, settled arrival the layout is asking for. */
  /* Interaction feedback runs 120-320ms; reveal and hero run long on purpose.
     Both long values use --ease-editorial (expo-out), so the visible travel is
     over inside ~250ms and the remainder is the settle. Do not shorten them to
     a generic 200-500ms band: they are entrances, not interactions. */
  --dur-fast: 120ms;    /* press feedback, icon flips */
  --dur-base: 200ms;    /* hover states, colour washes */
  --dur-slow: 320ms;    /* panels, dropdowns, result cards */
  --dur-reveal: 700ms;  /* scroll reveal */
  --dur-hero: 900ms;    /* first-paint hero choreography */
  --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-in: cubic-bezier(0.55, 0.06, 0.68, 0.19);
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-editorial: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);

  --header-h: 72px;
}

/* The single reduced-motion switch. Because every transition below is written in
   terms of the tokens above, collapsing them here removes the travel from all of
   it at once — colour still changes, nothing moves. The handful of infinite
   keyframe animations still need their own `animation: none` opt-outs (marquee,
   CTA pulse, pending shimmer), since a duration token can't stop a loop. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-fast: 1ms;
    --dur-base: 1ms;
    --dur-slow: 1ms;
    --dur-reveal: 1ms;
    --dur-hero: 1ms;
  }
  html { scroll-behavior: auto; }
}

* { box-sizing: border-box; }

/* The browser's own [hidden] rule is display:none at the weakest possible
   specificity, so any component rule that sets display beats it and the element
   stays on screen with the attribute set. That bit the contact form: after a
   successful send, js/contact-form.js sets form.hidden = true, but
   `.contact-form { display: grid }` kept the filled-in form visible underneath
   the thank-you panel, one click away from sending the same message twice.
   Four components had already been patched one at a time for this
   (.catalog-clear-btn, .catalog-brand-suggestions, .catalog-results-header);
   this is the same fix once, for anything that toggles the attribute. */
[hidden] { display: none !important; }

.skip-link {
  position: absolute;
  /* Parked at its final `top` and pushed out of frame with a transform instead of
     animating `top` — same effect, no layout pass per frame. */
  top: 12px;
  left: 12px;
  z-index: 100;
  background: var(--green-dark);
  color: var(--white);
  padding: 12px 20px;
  border-radius: 8px;
  font-weight: 700;
  transform: translateY(-200%);
  transition: transform var(--dur-fast) var(--ease-out);
}
.skip-link:focus {
  transform: translateY(0);
}

/* Gated by the prefers-reduced-motion block up in :root — a full-viewport anchor
   scroll is the exact motion that query exists to suppress. */
html { scroll-behavior: smooth; }

/* The mobile nav is a full-width fixed panel parked off-canvas with
   translateX(100%). A transformed box still counts toward the document's
   scrollable width, so without this every page could be swiped sideways into a
   blank second screen — 750px of document on a 375px phone.

   `clip` rather than `hidden` deliberately: `hidden` would turn these into
   scroll containers and break the four `position: sticky` elements on the site,
   the header among them. `clip` suppresses the overflow without doing that. */
html,
body { overflow-x: clip; }

/* Sticky-header clearance for every in-page anchor at once. The header is 72px,
   so an un-offset jump to #delivery-check parked the target underneath it. Doing
   it here rather than per-target also satisfies WCAG 2.2 focus-not-obscured:
   tabbing to something near the top of a scrolled page scrolls it clear of the
   header instead of behind it. */
:target { scroll-margin-top: calc(var(--header-h) + 24px); }
[id] { scroll-margin-top: calc(var(--header-h) + 24px); }

/* -- Focus ----------------------------------------------------------------
   One ring for the whole site. Two tones because a single colour cannot clear
   3:1 against both the bone ground and the forest-green anchor sections: the
   ink ring carries on light surfaces, the bone halo carries on dark ones, and
   on any given background at least one of them is doing the work. */
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--bone) 85%, transparent);
  border-radius: var(--radius-sm);
}
/* Dark surfaces invert it — ink-on-forest would be a ring you cannot see. */
.anchor-dark :focus-visible,
.site-footer :focus-visible,
.cta-band :focus-visible {
  outline-color: var(--bone);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--green-band) 70%, transparent);
}

body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--step-0);
  color: var(--ink);
  background: var(--bone);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Long tokens — item numbers, addresses, the odd pasted URL in a testimonial —
     reflow instead of forcing a horizontal scrollbar on a 320px phone. `anywhere`
     rather than `break-all` so ordinary prose still breaks on word boundaries. */
  overflow-wrap: anywhere;
}

img { max-width: 100%; display: block; }
/* <picture> is only a format-switch wrapper — let the <img> inside it lay out
   exactly as it did before, so no grid/flex rule needs to know about it.
   display:contents also promotes the <source> tags into the parent's flex/grid
   flow, where they'd sit as zero-width items still claiming a gap, so they have
   to be taken back out explicitly. */
picture { display: contents; }
picture > source { display: none; }

/* Paired with the width/height attributes on every <img>: those attributes are
   presentational hints, so wherever CSS sets only a width the hinted height
   would otherwise win and lock the image to its intrinsic height. */
.hero-media-photo,
.about-photo img { height: auto; }

/* Body links are underlined by default rather than on hover. On a bone ground a
   green-only link is a colour-alone signal; the offset underline keeps it legible
   without the heavy default that sits on the descenders. */
a { color: var(--green); text-decoration: none; }
a:hover { text-decoration: underline; }

.prose a,
p > a,
li > a:not(.btn):not(.card-link) {
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--green) 35%, transparent);
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
  transition: text-decoration-color var(--dur-base) var(--ease-out);
}
.prose a:hover,
p > a:hover,
li > a:not(.btn):not(.card-link):hover {
  text-decoration-color: var(--green);
}

/* -- Display type ---------------------------------------------------------
   Bitter, weight 600. No optical-size or variation axes here: the display
   face has changed twice and each time the previous face left settings behind
   that were inert on the next one. Anything face-specific belongs in the
   :root token block, not scattered through the heading rules.

   Tracking is looser than it was. Heavy slab serifs already carry a lot of ink
   per line, and the tight negative tracking that stopped a high-contrast serif
   looking dated makes a slab look cramped instead. Line-height is up slightly
   for the same reason — the blunt serifs need a little more room between lines
   before they start to knit together. */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  line-height: 1.14;
  margin: 0 0 0.5em;
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
  color: var(--ink);
  /* Progressive: browsers that have it avoid a one-word last line on a short
     headline. Everything else wraps normally — no nbsp hacks. */
  text-wrap: balance;
}
h1 { font-size: var(--step-5); }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-1); line-height: 1.2; }
h4 { font-size: var(--step-0); }

/* Sub-headings inside cards and list rows sit in the sans — a serif at 20px
   next to 17px sans body starts to look like two unrelated documents. The
   display face is reserved for things that are actually display size. */
h3.ui-heading, h4.ui-heading,
.step-copy h3, .why-us-item h3, .faq-item summary,
.footer-col h4, .team-card .name, .carry-block h2 {
  font-family: var(--font);
  letter-spacing: -0.01em;
}

p { margin: 0 0 1em; color: var(--ink-soft); }
/* Long-form copy gets a real measure. 68ch lands at roughly 70 characters at
   17px, inside the 60-75 band; `max-width` rather than `width` so it still
   collapses on a phone. */
.prose p, .section-head p, .hero-sub { max-width: var(--measure); }

/* The eyebrow component lived here: a tracked-out uppercase label with a short
   rule, sitting above every section heading on the site. It is gone.

   Twenty-one of them, and on inspection each one named the section it was
   already inside — "In their words" above "What our customers say", "How it
   works" above "From first call to first delivery". A label that repeats the
   heading beneath it is not structure, it is furniture, and the h2 was doing
   the work by itself. The two that carried a genuine fact ("6,600 items",
   "Every other year") were folded into the sentences around them, where a
   reader actually encounters them. */

.container {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 20px;
}
@media (min-width: 720px) { .container { padding: 0 32px; } }
@media (min-width: 1180px) { .container { padding: 0 40px; } }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* Buttons */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  /* 15px vertical + 1.05 line-height on 16px text clears 48px, so the button
     itself is the touch target and nothing needs a padded hit area. */
  padding: 15px 28px;
  min-height: 48px;
  border-radius: var(--radius-sm);
  font-family: var(--font);
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: -0.006em;
  line-height: 1.05;
  border: 1.5px solid transparent;
  cursor: pointer;
  text-decoration: none;
  overflow: hidden;
  isolation: isolate;
  touch-action: manipulation;
  /* Colour transitions are deliberately slower than the lift, so the fill reads
     as a wash across the button rather than a snap. */
  transition: transform var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-standard),
              color var(--dur-base) var(--ease-standard),
              border-color var(--dur-base) var(--ease-standard);
}

/* The wipe. A pseudo-element scaled from 0 on the X axis, origin flipping left
   on enter and right on leave, so the fill sweeps in and retreats the way it
   came instead of blinking out. transform + opacity only, and it sits behind
   the label via z-index rather than by painting over it — a background-position
   animation here would repaint the whole button every frame. */
.btn::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--green);
  transform: scaleX(0);
  transform-origin: right center;
  transition: transform var(--dur-slow) var(--ease-editorial);
}
.btn:hover::before,
.btn:focus-visible::before {
  transform: scaleX(1);
  transform-origin: left center;
}
/* Nothing should sweep when the user has asked for stillness — the colour
   change below still fires, so the state is never ambiguous. */
@media (prefers-reduced-motion: reduce) {
  .btn::before { transition: none; }
}
/* -2px, not -1px: a single pixel sits under the threshold where a lift reads as
   intentional and just looks like the button twitched. The shadow is what sells
   it as rising. :focus-visible gets the same treatment — a keyboard user was
   getting the colour change and none of the movement. */
.btn:hover,
.btn:focus-visible { text-decoration: none; transform: translateY(-2px); box-shadow: var(--shadow); }
/* Press cancels the lift. Without this the button stays raised while held,
   which reads as unresponsive on a phone where there is no hover state to
   have explained the lift in the first place. */
.btn:active { transform: translateY(0); box-shadow: var(--shadow-sm); transition-duration: var(--dur-fast); }

/* Every button converges on the same bright --green fill on hover and keyboard
   focus: an outlined button fills in, a filled one brightens. Both resting
   states (transparent, and the deeper --green-dark) stay clearly distinct from
   that one hover colour, so a hovered button is never mistaken for its
   un-hovered neighbour in a CTA pair. The 2px border is already on .btn at
   rest, so nothing shifts when its colour appears. */
.btn-primary { background: var(--green-deep); color: var(--paper); border-color: var(--green-deep); }
.btn-outline { background: transparent; color: var(--green-deep); border-color: var(--border-strong); }
.btn-primary:hover,
.btn-primary:focus-visible,
.btn-outline:hover,
.btn-outline:focus-visible { color: var(--paper); border-color: var(--green); }

/* Ghost — the third tier, for "read more" style actions that shouldn't compete
   with the one primary CTA a section is allowed. No wipe: it would make a
   tertiary action the loudest thing on hover. */
.btn-ghost {
  background: transparent;
  color: var(--ink);
  border-color: transparent;
  padding-inline: 14px;
}
.btn-ghost::before { display: none; }
.btn-ghost:hover,
.btn-ghost:focus-visible { background: var(--sand-deep); color: var(--ink); box-shadow: none; }

/* On the forest-green anchor sections, where the green wipe would be invisible.

   Green at rest, white on hover — the same swap the hero's two buttons got, and
   here for the same reason: this was bone with a --paper wipe, so hovering took
   it from near-white to white and read as nothing happening.

   --green-on-band, not the hero's --green-on-dark. Every button wearing this
   class sits on --green-band: the seven closing CTA bands, plus the mastheads on
   404 and what-we-carry, which are the same green since the chrome went dark.
   --green-on-dark measures 3.82:1 there, which is the reason the band was given
   its own lighter accent in the first place. This one is 4.62:1 against the band,
   with the forest label reading 6.83:1 on it, and the label stays forest through
   the hover so only the fill moves. The hero keeps --green-on-dark via its own
   rule further down; that button is on coal, not on the band. */
.btn-on-dark { background: var(--green-on-band); color: var(--green-forest); border-color: var(--green-on-band); }
.btn-on-dark::before { background: var(--bone); }
.btn-on-dark:hover,
.btn-on-dark:focus-visible { color: var(--green-forest); border-color: var(--bone); }
.btn-outline-on-dark { background: transparent; color: var(--bone); border-color: color-mix(in srgb, var(--bone) 42%, transparent); }
.btn-outline-on-dark::before { background: color-mix(in srgb, var(--bone) 14%, transparent); }
.btn-outline-on-dark:hover,
.btn-outline-on-dark:focus-visible { color: var(--paper); border-color: var(--bone); }

.btn-block { width: 100%; }
.btn-lg { padding: 18px 34px; font-size: 1.0625rem; min-height: 56px; }

/* Header */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(255, 255, 255, 0.98);
  border-bottom: 1px solid var(--border);
  /* Elevation only once the header is actually stuck. Resting at the top of the
     page it should sit flush in the layout; over a scrolled white section a 1px
     border alone let it dissolve into the content. Toggled by main.js off a
     sentinel element, so this is one transition on a class flip — not a shadow
     recalculated per scroll event. */
  transition: box-shadow var(--dur-base) var(--ease-standard);
}
.site-header.is-stuck { box-shadow: 0 2px 14px rgba(0, 40, 24, 0.10); }
.site-header .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 68px;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand img { height: 40px; width: auto; }

/* Tap-to-call, phones and tablets only. The whole site argues "call us and a real
   person picks up", but below 1180px that number sat two taps deep behind the menu
   (Contact Us -> the number) or in the footer, seven screens down. margin-left:auto
   is what keeps the brand hard left: the header is space-between, so without it a
   third child would get spread to the middle instead of pairing with the toggle.
   Hidden from 1180px up, where the Contact Us dropdown already carries both numbers. */
.header-call {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  /* Auto width, not a 44px square: the button now carries a label. min-width
     keeps it a comfortable target when the number is hidden and only "Call"
     shows. Padding is asymmetric so the icon does not look wedged. */
  min-width: 44px;
  gap: 7px;
  padding: 0 12px 0 9px;
  border-radius: 999px;
  font-weight: 600;
  font-size: 0.9rem;
  white-space: nowrap;
  height: 44px;
  margin-left: auto;
  color: var(--green-dark);
}
.header-call svg { width: 22px; height: 22px; }

/* The number is never removed from the DOM, only hidden visually, so the
   accessible name stays "Call 800-473-2353" at every width while the visible
   label shortens to "Call" where there is no room for it. */
@media (max-width: 519px) {
  .header-call-number {
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
}
.header-call:hover { color: var(--green); text-decoration: none; }

/* The banner logo is 277px wide at its normal 40px height, which plus two 44px
   buttons and the container padding overflows a 375px phone. Trimming the logo
   on the narrowest screens is what buys the call button its room.

   Trimmed again when the call button gained its label: at 32px the logo is
   221px wide, and 221 + a 76px pill + a 44px toggle + 40px of padding leaves
   nothing between the logo and the button — measured gap was 0, with the mark
   itself starting to compress. 28px puts it at 193px and restores the gap. */
@media (max-width: 479px) {
  .brand img { height: 28px; }
  .site-header .container { gap: 10px; }
}

.nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: none;
  background: none;
  cursor: pointer;
  color: var(--green-dark);
}
.nav-toggle svg { width: 26px; height: 26px; }
.nav-toggle-line { transform-origin: 12px 12px; transition: transform var(--dur-fast) var(--ease-standard), opacity var(--dur-fast) var(--ease-standard); }
.nav-toggle[aria-expanded="true"] .nav-toggle-line-top { transform: rotate(45deg) translateY(6px); }
.nav-toggle[aria-expanded="true"] .nav-toggle-line-mid { opacity: 0; }
.nav-toggle[aria-expanded="true"] .nav-toggle-line-bottom { transform: rotate(-45deg) translateY(-6px); }

.site-nav {
  position: fixed;
  top: 68px;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 40;
  background: var(--white);
  transform: translateX(100%);
  /* translateX alone only moves the panel off-screen — it stays visible to the
     tab order and to screen readers, so eleven links sat off the right edge and
     still took focus. visibility takes them out of both. It flips instantly, but
     only after the slide-out has finished, or the panel would vanish mid-animation;
     on the way in it flips at 0s so the slide-in is actually seen. */
  visibility: hidden;
  transition: transform var(--dur-slow) var(--ease-in), visibility 0s linear var(--dur-slow);
  overflow-y: auto;
}
.site-nav.is-open {
  transform: translateX(0);
  visibility: visible;
  transition: transform var(--dur-slow) var(--ease-out), visibility 0s linear 0s;
}

/* No scrim here on purpose. A dimming layer behind the panel would be invisible:
   this nav is a full-width overlay (left:0/right:0 above), not a side sheet, so it
   covers the viewport edge to edge and nothing of the page behind it ever shows.
   A scrim only earns its place if the panel is narrowed to a sheet first. */
/* The panel is fixed and covers the page, so the page behind it must not scroll.
   Lifted again at 1180px, where .site-nav is a static bar and .is-open is inert —
   otherwise resizing a phone-width window up with the menu open would leave the
   body permanently locked. */
body.nav-open { overflow: hidden; }
.site-nav > ul {
  list-style: none;
  margin: 0;
  padding: 24px 20px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.site-nav a {
  display: block;
  padding: 14px 8px;
  color: var(--text);
  font-weight: 700;
  font-size: 1.15rem;
  border-bottom: 1px solid var(--border);
}
.site-nav a:hover { color: var(--green); text-decoration: none; }
.site-nav .nav-cta { margin-top: 12px; padding: 0 8px; }
.site-nav .nav-login { margin-top: 8px; padding: 0 8px; }

/* "Customer Login" — secondary outline button, paired with the primary "Contact Us" CTA */
/* Selector includes .site-nav a for specificity: otherwise the generic
   ".site-nav a { border-bottom: 1px solid var(--border) }" divider rule
   (used for the plain stacked menu links) wins on the bottom edge only,
   since it's more specific than ".nav-login-link" alone — producing a
   mismatched flat gray edge instead of the full pill outline. */
.site-nav a.nav-login-link {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  border-radius: var(--radius);
  border: 2px solid var(--green-dark);
  color: var(--green-dark);
  padding: 12px 26px;
  font-weight: 700;
  font-size: 1rem;
  white-space: nowrap;
  /* Matches the .btn-outline timing so header and body buttons feel the same. */
  transition: background var(--dur-base) var(--ease-standard), color var(--dur-base) var(--ease-standard), border-color var(--dur-base) var(--ease-standard);
}
/* Fills to the same --green as .btn-outline — not --green-dark, which would
   make it a twin of the un-hovered "Contact Us" button beside it. The icon
   strokes currentColor, so it turns white along with the label. */
.site-nav a.nav-login-link:hover,
.site-nav a.nav-login-link:focus-visible { background: var(--green); color: var(--white); border-color: var(--green); text-decoration: none; }
.nav-login-link svg { flex: none; width: 18px; height: 18px; }

/* Nav dropdown ("About", "Contact Us") — accordion on mobile, popover on desktop */
.nav-dropdown { position: relative; }
.nav-dropdown-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  border: none;
  background: none;
  cursor: pointer;
  font-family: var(--font);
  text-align: left;
  padding: 14px 8px;
  color: var(--text);
  font-weight: 700;
  font-size: 1.15rem;
  border-bottom: 1px solid var(--border);
}
.nav-dropdown-toggle:hover { color: var(--green); }
.nav-dropdown-arrow { flex: none; width: 15px; height: 15px; transition: transform var(--dur-base) var(--ease-standard); }
.nav-dropdown-toggle[aria-expanded="true"] .nav-dropdown-arrow { transform: rotate(180deg); }

.nav-dropdown-menu {
  margin: 0;
  padding: 0;
  padding-left: 16px;
  /* Accordion via grid-row interpolation. 0fr -> 1fr resolves to the content's
     real height at animation time, so nothing is measured in JS and the panel
     never travels through empty space. The inner list is the single grid item
     and carries the clip; min-height:0 lets it shrink below its content. */
  display: grid;
  grid-template-rows: 0fr;
  /* grid-template-rows alone only collapses the box — the links stay in the tab
     order and the accessibility tree. visibility takes them out, flipped after
     the collapse finishes so the accordion still animates. */
  visibility: hidden;
  transition: grid-template-rows var(--dur-slow) var(--ease-in), visibility 0s linear var(--dur-slow);
}
.nav-dropdown-list {
  list-style: none;
  margin: 0;
  padding: 0;
  min-height: 0;
  overflow: hidden;
}
.nav-dropdown.is-open .nav-dropdown-menu {
  grid-template-rows: 1fr;
  visibility: visible;
  transition: grid-template-rows var(--dur-slow) var(--ease-out), visibility 0s linear 0s;
}
.nav-dropdown-menu a {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 8px;
  color: var(--text-muted);
  font-weight: 600;
  font-size: 1.05rem;
  border-bottom: 1px solid var(--border);
}
.nav-dropdown-menu a:hover { color: var(--green); text-decoration: none; }
.nav-dropdown-menu a svg { flex: none; width: 16px; height: 16px; }

/* "Contact Us" — a split button. The label is a real link straight to the
   contact page; the caret is the only part that opens the quick-access
   dropdown (phone numbers, hours, Facebook). overflow:hidden on the wrapper
   is what keeps both halves inside one pill shape without either needing
   its own radius. */
.nav-cta-split {
  display: flex;
  align-items: stretch;
  border-radius: var(--radius);
  overflow: hidden;
}
/* Selector matches .nav-login-link's own scoping just above: ".site-nav a"
   carries a border and its own padding at every width, and only a selector
   this specific reliably beats it (a bare .nav-cta-label does not). */
.site-nav a.nav-cta-label {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 1;
  background: var(--green-dark);
  color: var(--white);
  font-weight: 700;
  padding: 14px 26px;
  font-size: 1rem;
  border-bottom: none;
  transition: background var(--dur-base) var(--ease-standard), color var(--dur-base) var(--ease-standard);
}
.site-nav a.nav-cta-label:hover,
.site-nav a.nav-cta-label:focus-visible { background: var(--green); color: var(--white); text-decoration: none; }
/* Specificity has to beat both the generic .nav-dropdown-toggle rule and the
   1180px desktop override below, or the caret inherits the label's full-width
   pill sizing instead of staying a narrow tap target. */
.nav-cta .nav-dropdown-toggle.nav-cta-caret {
  flex: none;
  width: 44px;
  padding: 0;
  justify-content: center;
  background: var(--green-dark);
  color: var(--white);
  border-left: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: 0;
  transition: background var(--dur-base) var(--ease-standard), color var(--dur-base) var(--ease-standard);
}
.nav-cta .nav-dropdown-toggle.nav-cta-caret:hover,
.nav-cta .nav-dropdown-toggle.nav-cta-caret:focus-visible { background: var(--green); color: var(--white); }
.nav-cta .nav-dropdown-menu { padding-left: 0; }

/* Hero */
.hero {
  padding: 56px 0 48px;
  background: linear-gradient(180deg, var(--green-light-2) 0%, var(--white) 100%);
  text-align: center;
}
.hero-eyebrow {
  display: inline-block;
  color: var(--green-dark);
  background: var(--green-light);
  padding: 6px 14px;
  border-radius: var(--radius-sm);
  font-weight: 700;
  font-size: 0.82rem;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  margin-bottom: 18px;
}
.hero h1 {
  font-size: 2rem;
  letter-spacing: -0.02em;
  margin-bottom: 16px;
}
.hero-sub {
  font-size: 1.08rem;
  max-width: 46ch;
  margin: 0 auto 28px;
}
.hero-ctas {
  display: flex;
  flex-direction: column;
  gap: 12px;
  align-items: stretch;
  max-width: 320px;
  margin: 0 auto;
}
.hero-cta-label {
  font-weight: 700;
  font-size: 0.9rem;
  color: var(--green-dark);
  margin-bottom: -2px;
}
/* The two buttons' own wrapper — split out from .hero-ctas so the label can
   stay put above them while the split-hero desktop query below turns only
   this row, not the label, sideways. Stacked here at mobile exactly as the
   three used to sit as flat siblings, just with the 12px gap now split into
   two levels (label-to-row, row-internal) instead of one. */
.hero-cta-buttons {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 12px;
}

/* The "Already a customer?" line under the hero CTAs. Deliberately quieter than the
   buttons above it — same 320px column, hairline rule, muted text — so it reads as a
   second door rather than competing with the primary path. */
.hero-existing {
  text-align: center;
  max-width: 320px;
  margin: 20px auto 0;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}
.hero-existing-label {
  display: block;
  font-weight: 700;
  font-size: 0.82rem;
  color: var(--text-muted);
  margin-bottom: 8px;
}
.hero-existing-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px 14px;
  font-size: 0.9rem;
}
.hero-existing-links a {
  color: var(--green-dark);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  /* Comfortable thumb target (34px tall) without the links looking like buttons. */
  display: inline-block;
  padding: 6px 0;
}
.hero-existing-links a:hover { color: var(--green-darker); }

/* Hero — split layout (content + photo panel) */
.hero-split .hero-grid {
  display: grid;
  grid-template-columns: 1fr;
  align-items: center;
  gap: 32px;
}
.hero-media-photo {
  display: block;
  width: 100%;
  /* The file itself is now pre-cropped to the 2048x1272 truck-lineup band
     (see optimize-assets.js), so no CSS crop is needed — the ratio is kept
     only to reserve the right box before the image arrives. */
  aspect-ratio: 2048 / 1272;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.vendor-strip { margin-top: 44px; text-align: center; }
.vendor-strip-label {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 700;
  color: var(--text-muted);
  margin-bottom: 20px;
}
.vendor-logo-marquee {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.vendor-logos {
  display: flex;
  width: max-content;
  animation: vendor-marquee 90s linear infinite;
}
/* Hover and focus-within stop it for anyone with a pointer or a keyboard;
   .is-paused is what the off-screen observer in main.js drives. A visible
   pause button also lived here (WCAG 2.2.2, which wants a stop mechanism for
   auto-starting motion running past five seconds) and was removed on request
   — a touch user now has no way to stop these logos. */
.vendor-logo-marquee:hover .vendor-logos,
.vendor-logo-marquee:focus-within .vendor-logos,
.vendor-logo-marquee.is-paused .vendor-logos {
  animation-play-state: paused;
}
.vendor-logo-set {
  display: flex;
  flex: none;
  align-items: center;
  gap: 36px;
  padding-right: 36px;
}
.vendor-logo {
  height: 40px;
  width: auto;
  max-width: 130px;
  object-fit: contain;
  flex: none;
}
@keyframes vendor-marquee {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .vendor-logos { animation: none; }
  .vendor-logo-marquee { overflow-x: auto; }
  .vendor-logo-set[aria-hidden] { display: none; }
}
.vendor-strip-note {
  margin-top: 20px;
  /* The default paragraph bottom margin sat under this line and added ~14px to
     whatever floor the section set, so the band measured even in the stylesheet
     and looked bottom-heavy on screen. The section padding owns that gap. */
  margin-bottom: 0;
  font-size: 0.85rem;
  color: var(--text-muted);
}
.vendor-strip-note a { font-weight: 700; }

/* Scroll reveal — progressive enhancement handled entirely by main.js: it only
   adds .reveal-pending (and later .is-visible, on intersection) when
   IntersectionObserver exists and the visitor hasn't asked for reduced motion,
   so a no-JS or reduced-motion visitor always sees full content immediately
   rather than a page stuck at opacity 0. */
[data-reveal].reveal-pending {
  opacity: 0;
  transform: translateY(22px);
  /* --reveal-delay is set per element by main.js from its index among its siblings,
     so a row of four cards arrives as a sequence instead of one blob fade.
     --ease-editorial rather than --ease-out: the expo tail is what separates a
     reveal that feels composed from one that feels like a jQuery fade. */
  transition: opacity var(--dur-reveal) var(--ease-editorial) var(--reveal-delay, 0ms),
              transform var(--dur-reveal) var(--ease-editorial) var(--reveal-delay, 0ms);
}
[data-reveal].reveal-pending.is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* Variants, opted into per element with data-reveal="…". The plain rise above
   is the default and covers most of the site; these are for the handful of
   places where the motion should describe the thing arriving. */

/* Photos and map panels wipe up behind a clip edge instead of sliding, so the
   image looks like it is being uncovered rather than flown in. clip-path is
   compositor-friendly and, unlike a mask image, needs no extra asset. */
[data-reveal="wipe"].reveal-pending {
  clip-path: inset(14% 0 0 0);
  transform: translateY(14px) scale(1.02);
  /* The clip edge only exists on this variant, so only it pays for the slot. */
  transition: opacity var(--dur-reveal) var(--ease-editorial) var(--reveal-delay, 0ms),
              transform var(--dur-reveal) var(--ease-editorial) var(--reveal-delay, 0ms),
              clip-path var(--dur-reveal) var(--ease-editorial) var(--reveal-delay, 0ms);
}
[data-reveal="wipe"].reveal-pending.is-visible {
  clip-path: inset(0 0 0 0);
  transform: translateY(0) scale(1);
}

/* Rules, dividers and the steps spine draw themselves left-to-right. */
[data-reveal="draw"].reveal-pending {
  opacity: 1;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-reveal) var(--ease-editorial) var(--reveal-delay, 0ms);
}
[data-reveal="draw"].reveal-pending.is-visible { transform: scaleX(1); }

/* No travel — for things already large enough that 22px of movement reads as a
   jolt rather than an arrival (full-bleed anchor sections, the hero photo). */
[data-reveal="fade"].reveal-pending { transform: none; }

/* Hero choreography. Same hidden state as the scroll reveal, but keyed off its
   own attribute because these elements are already on screen at load and are
   released by main.js on a rAF rather than by an observer. */
[data-reveal-hero].reveal-pending {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity var(--dur-hero) var(--ease-editorial) var(--reveal-delay, 0ms),
              transform var(--dur-hero) var(--ease-editorial) var(--reveal-delay, 0ms);
}
[data-reveal-hero].reveal-pending.is-visible { opacity: 1; transform: translateY(0); }

/* Interior mastheads — the quieter half of the same grammar. 12px of travel
   against the hero's 18 and --dur-reveal against --dur-hero, so a page someone
   lands on repeatedly does not perform the way the front page does. See the
   module in js/main.js for why this is selector-driven rather than opted into
   per page: the masthead is site-wide chrome, and nothing in the markup has to
   remember to ask for it. Scoped to the direct children of the masthead
   container so it can never reach the page body below. */
.page-hero > .container > .reveal-pending,
.doc-hero > .container > .reveal-pending {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity var(--dur-reveal) var(--ease-editorial) var(--reveal-delay, 0ms),
              transform var(--dur-reveal) var(--ease-editorial) var(--reveal-delay, 0ms);
}
.page-hero > .container > .reveal-pending.is-visible,
.doc-hero > .container > .reveal-pending.is-visible { opacity: 1; transform: translateY(0); }

/* Per-line display headline reveal. main.js splits the heading into
   .reveal-line spans; each rides up from behind its own overflow clip, which
   is why the lines appear to be printed rather than faded. Falls back to the
   parent's plain rise if the split never ran. */
/* The clip edge has to sit below the descenders, not on the baseline, or
   Fraunces' p/y/g get sliced off — at line-height 1.08 the line box is shorter
   than the glyphs. Padding pushes the boundary down; the matching negative
   margin takes the space back, so the headline's vertical rhythm is unchanged
   and only the clipping region moves. */
.reveal-line-mask {
  display: block;
  overflow: hidden;
  padding-bottom: 0.22em;
  margin-bottom: -0.22em;
}
.reveal-line {
  display: block;
  transform: translateY(105%);
  transition: transform var(--dur-hero) var(--ease-editorial) var(--reveal-delay, 0ms);
}
.is-visible .reveal-line,
.reveal-lines-ready.is-visible .reveal-line { transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .reveal-line { transform: none; }
}

/* -- Sections -------------------------------------------------------------
   Three grounds, alternating down the page: bone (default), sand (.section-alt),
   and the forest-green anchor (.anchor-dark) that appears at most twice per
   page. The hairline on .section-alt is what makes the ground change read as an
   editorial rule rather than a colour accident. */
section { padding: var(--space-2xl) 0; }
.section-alt {
  background: var(--sand);
  border-top: 1px solid var(--sand-deep);
  border-bottom: 1px solid var(--sand-deep);
}

.section-head { margin-bottom: var(--space-xl); max-width: var(--measure); }
.section-head h2 { font-size: var(--step-3); margin-bottom: 0; }
.section-head p { margin: var(--space-sm) 0 0; font-size: var(--step-1); color: var(--ink-soft); line-height: 1.5; }
/* Centred is now the opt-in, not the default. A left-aligned head against a
   generous right margin is the single biggest thing separating this from the
   centred-everything template look. */
.section-head-center { margin-inline: auto; text-align: center; }
.section-head-center p { margin-inline: auto; }

/* The "see the rest of them" button that closes a section whose body is a grid
   — the team teaser, the testimonial scroller. It was an inline
   style="margin-top: var(--space-lg)" on the wrapping <p> in two places on the
   homepage, which is the one shape of technical debt this stylesheet can't
   absorb: a token used outside the cascade can't be retuned from here. */
.section-cta { margin-top: var(--space-lg); }

/* Last-line paragraphs that close a band. Each one had the browser default bottom
   margin, and because .container carries no padding or border that margin collapses
   straight through it and stacks on top of the section's padding — so a band
   declared 72/72 measured 73/90 on screen and read as bottom-heavy. Same defect the
   vendor strip had. The section padding owns the floor; these own nothing below them.
   .section-cta (homepage testimonial teaser), .team-finder-link (team masthead),
   .wwc-footnote (What We Carry) are each the last child of their container. */
.section-cta,
.team-finder-link,
.wwc-footnote { margin-bottom: 0; }

/* A section stacked directly under a page hero, which has already paid the top
   padding. Was style="padding-top:0" on contact.html. */
.section-flush-top { padding-top: 0; }

/* The forest anchor. Used for the closing CTA and, on longer pages, one
   mid-page break — any more and it stops being an anchor. */
.anchor-dark {
  background: var(--green-band);
  color: var(--bone);
  border: 0;
}
.anchor-dark h1, .anchor-dark h2, .anchor-dark h3, .anchor-dark h4 { color: var(--bone); }
.anchor-dark p { color: color-mix(in srgb, var(--bone) 80%, var(--green-band)); }
.anchor-dark a:not(.btn) { color: var(--bone); text-decoration-color: color-mix(in srgb, var(--bone) 45%, transparent); }

/* -- Rule -----------------------------------------------------------------
   The hairline that separates stacked blocks inside a section. Its own element
   so it can carry data-reveal="draw". */
.rule { height: 1px; background: var(--sand-deep); border: 0; margin: var(--space-xl) 0; }
.anchor-dark .rule { background: color-mix(in srgb, var(--bone) 22%, transparent); }

/* Why Choose Us — left-aligned intro + a plain text list, deliberately
   NOT an icon-in-a-rounded-square card grid. That pattern (and the
   centered heading + centered subtitle every section opens with) is
   exactly what makes a page read as templated/AI-generated; this section
   breaks both by left-aligning its head and dropping icons/cards/shadows
   entirely in favor of a thin color accent bar. */
.why-us-grid {
  display: grid;
  gap: 28px;
}
.why-us-intro { max-width: 46ch; }
.why-us-intro h2 { font-size: 1.6rem; margin-bottom: 10px; }
.why-us-list {
  display: grid;
  grid-template-columns: 1fr;
  gap: 26px;
}
.why-us-item h3 { font-size: 1.05rem; margin-bottom: 6px; }
.why-us-item p { margin: 0; font-size: 0.96rem; color: var(--text-muted); }
.why-us-item a.card-link { display: inline-block; margin-top: 8px; font-weight: 700; font-size: 0.92rem; }

/* A customer's own words sitting right under the claim they back up — a
   dashed rule rather than a boxed/shadowed card, rather than introducing a new
   pattern for just three items. (Superseded further down by the green-spine
   treatment; this is the base the later rule overrides.) */
.why-us-quote {
  margin: 10px 0 0;
  padding: 10px 0 0;
  border-top: 1px dashed var(--border);
  font-size: 0.9rem;
  font-style: normal;
  color: var(--text);
}
.why-us-quote cite {
  display: block;
  margin-top: 4px;
  font-style: normal;
  font-weight: 700;
  font-size: 0.78rem;
  color: var(--text-muted);
}

/* The pre-redesign version of the steps list lived here: a flex row with a
   filled numeral, a hairline between rows and a background-plus-slide on
   hover. All of it is superseded by the ".step-list" component further down,
   and leaving the dead rules in place meant the new component had to spend
   four declarations turning them back off. */

/* Common Questions — native <details>, so keyboard and screen-reader support
   come from the browser rather than from JS we'd have to keep correct. Styled
   with the same hairline rules and 720px column as the steps list above: this
   page already carries a card grid, a numbered list and a pull-quote row, and a
   fourth boxed pattern is what starts making a page read as templated.
   Collapsed, all seven fit in roughly the height of one open answer. */
.faq-list {
  display: flex;
  flex-direction: column;
  max-width: 720px;
  margin: 0 auto;
}
.faq-item { border-top: 1px solid var(--border); }
.faq-list .faq-item:last-child { border-bottom: 1px solid var(--border); }
.faq-item summary {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 18px 0;
  cursor: pointer;
  font-weight: 700;
  font-size: 1.02rem;
  line-height: 1.35;
  color: var(--text);
  list-style: none;
}
/* Safari still paints its own disclosure triangle without this. */
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary:hover { color: var(--green-dark); }
.faq-item summary:focus-visible {
  outline: 2px solid var(--green-dark);
  outline-offset: 2px;
  border-radius: 4px;
}
.faq-icon {
  flex: none;
  width: 18px;
  height: 18px;
  margin-top: 3px;
  color: var(--green);
  /* Rotation timing and the open state live in the FAQ redesign block further
     down, keyed to .is-closing so the arrow turns with the panel. */
}
.faq-answer {
  padding: 0 0 20px 32px;
  font-size: 0.96rem;
  color: var(--text-muted);
}
.faq-answer p { margin: 0 0 10px; }
.faq-answer p:last-child { margin-bottom: 0; }
.faq-note {
  max-width: 720px;
  margin: 24px auto 0;
  text-align: center;
  font-size: 0.94rem;
}

@media (prefers-reduced-motion: reduce) {
  .faq-icon { transition: none; }
}

/* Testimonials */
.quote-scroller-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
}
.quote-scroller {
  flex: 1;
  min-width: 0;
  display: flex;
  gap: 18px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  padding: 4px 4px 10px;
  scrollbar-width: none;
}
.quote-scroller::-webkit-scrollbar { display: none; }
.quote-scroller:focus-visible { outline: 2px solid var(--green-dark); outline-offset: 4px; }

/* ── Testimonial scroll progress ────────────────────────────────────────────
   Ten cards and roughly three and a half screens of horizontal scroll, with the
   scrollbar hidden two rules above and the arrows display:none below 639px — so
   on a phone there was nothing to say how far through the set you were. The next
   card peeking past the right edge (a generous 159px at 594px wide, 338px at
   1280) already says "there is more"; what it cannot say is "there are ten of
   these and you are on the second".

   A scroll-driven animation rather than a scroll listener: the fill is tied to
   the scroller's own inline progress, so it costs no JavaScript, no state, and
   no work per frame. @supports because the right fallback is no rule at all — a
   bar that could not follow the scroll would be worse than none, and the peek
   still does its job underneath.

   Two pseudo-elements rather than new markup: ::before is the track, ::after the
   fill. Both are children of the wrap, which is what lets timeline-scope reach
   them — .quote-scroller cannot expose its timeline to a sibling by itself.

   Deliberately NOT suppressed under prefers-reduced-motion. This is not
   decorative motion; it is a scrollbar in another shape, and it moves only in
   direct response to the reader's own scrolling. Suppressing it would take away
   an affordance from the people most likely to want it. */
@supports (animation-timeline: scroll()) and (timeline-scope: --mf-probe) {
  .quote-scroller-wrap {
    position: relative;
    timeline-scope: --quote-scroll;
  }
  .quote-scroller { scroll-timeline: --quote-scroll inline; }

  .quote-scroller-wrap::before,
  .quote-scroller-wrap::after {
    content: "";
    position: absolute;
    bottom: 0;
    /* Clears the arrow and the wrap's own gap on each side; the token is shared
       with the "Read all reviews" button so the two line up. */
    left: var(--quote-rail-inset);
    right: var(--quote-rail-inset);
    height: 2px;
    border-radius: var(--radius-pill);
    pointer-events: none;
  }
  .quote-scroller-wrap::before { background: var(--sand-deep); }
  .quote-scroller-wrap::after {
    background: var(--green);
    transform: scaleX(0);
    transform-origin: left center;
    animation: quote-scroll-progress linear both;
    animation-timeline: --quote-scroll;
  }
  /* The arrows are hidden here, so the rule runs the full width instead. */
  @media (max-width: 639px) {
    .quote-scroller-wrap::before,
    .quote-scroller-wrap::after { left: 0; right: 0; }
  }
}
@keyframes quote-scroll-progress { to { transform: scaleX(1); } }

/* Pull-quote style, not another white-card-with-shadow — matches the thin
   accent-bar language used for "Why Choose Us" instead of repeating a
   third boxed-card grid down the same page. */
.quote-card {
  flex: 0 0 82%;
  max-width: 340px;
  scroll-snap-align: start;
  padding: 2px 4px 2px 20px;
  border-left: 3px solid var(--green-light);
}
.quote-mark { font-family: Georgia, "Times New Roman", serif; font-size: 3rem; color: var(--green-light); line-height: 0.5; margin-bottom: 10px; font-weight: 800; }
.quote-card p { color: var(--text); font-size: 1rem; margin-bottom: 14px; }
.quote-name { font-weight: 800; font-size: 0.92rem; color: var(--text); }
.quote-role { font-size: 0.85rem; color: var(--text-muted); }

.quote-arrow {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: none;
  background: var(--green-dark);
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-standard), opacity var(--dur-fast) var(--ease-standard), transform var(--dur-fast) var(--ease-out);
}
.quote-arrow svg { width: 20px; height: 20px; }
.quote-arrow:hover { background: var(--green-darker); }
.quote-arrow:active { transform: scale(0.94); }
.quote-arrow[disabled] { opacity: 0.3; cursor: default; pointer-events: none; }

/* Testimonials page — same pull-quote card as the homepage scroller, just
   reflowed into a static grid instead of horizontal scroll. */
.testimonial-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 32px 40px;
}
.testimonial-grid .quote-card {
  flex: none;
  max-width: none;
  padding: 2px 4px 2px 22px;
}

/* Team teaser (home) */
.team-teaser-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
  max-width: 420px;
  margin: 0 auto 28px;
}
.team-teaser-card { text-align: center; }
.team-teaser-card img {
  width: 84px; height: 84px;
  border-radius: 50%;
  object-fit: cover;
  margin: 0 auto 10px;
  border: 3px solid var(--white);
  box-shadow: var(--shadow);
}
.team-teaser-card .name { font-weight: 700; font-size: 0.88rem; }
.team-teaser-card .title { font-size: 0.78rem; color: var(--text-muted); }

/* Team page grid — compact 2-up on mobile so a 20+ person roster stays scannable */
.team-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
}
.team-card {
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px 10px;
  text-align: center;
  box-shadow: var(--shadow);
}
.team-card img {
  width: 82px; height: 82px;
  border-radius: 50%;
  object-fit: cover;
  margin: 0 auto 10px;
}
.team-card h3 { font-size: 0.95rem; margin-bottom: 3px; }
.team-card .role {
  color: var(--green-dark);
  font-weight: 700;
  font-size: 0.76rem;
  line-height: 1.3;
}
.team-phone {
  display: inline-block;
  margin-top: 6px;
  padding: 4px;
  color: var(--text-muted);
  font-size: 0.78rem;
  text-decoration: none;
}
.team-phone:hover { color: var(--green-dark); text-decoration: underline; }

/* A shortcut to the address checker for someone who just wants their own
   rep's number, not a scroll past 20 other people to find it. */
.team-finder-link {
  font-weight: 700;
  font-size: 0.95rem;
}
.team-finder-link a { text-decoration: underline; text-underline-offset: 3px; }

/* Team department groupings */
.team-group { margin-bottom: 36px; }
.team-group:last-child { margin-bottom: 0; }
.team-group-heading {
  font-size: 1.05rem;
  color: var(--green-dark);
  margin-bottom: 14px;
  padding-bottom: 8px;
  border-bottom: 2px solid var(--green-light);
}

/* Delivery area — every named town, as real text rather than something only
   visible after a JS fetch and an address search (finding CT-1). The list
   itself is generated — see tools/generate-town-list.js — so this only ever
   styles it, never contains a hand-typed town name. */
.town-list-groups {
  display: grid;
  grid-template-columns: 1fr;
  gap: 32px;
  max-width: 760px;
  margin: 0 auto;
}
.town-list-group h3 {
  font-size: 1.05rem;
  color: var(--green-dark);
  margin-bottom: 12px;
  padding-bottom: 8px;
  border-bottom: 2px solid var(--green-light);
}
/* column-width rather than a fixed column-count: the browser fits as many
   ~110px columns as the available width allows, so Wisconsin's 42 towns and
   Minnesota's 16 both read as a compact index without a breakpoint apiece. */
.town-list {
  list-style: none;
  margin: 0;
  padding: 0;
  columns: 110px;
  column-gap: 20px;
  font-size: 0.92rem;
  color: var(--text-muted);
}
.town-list li { padding: 4px 0; break-inside: avoid; }

@media (min-width: 640px) {
  .town-list-groups { grid-template-columns: 1fr 1fr; gap: 40px; }
}

/* About page — history + photo */
.about-history-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 28px;
  align-items: center;
}
.about-photo img {
  width: 100%;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
}
.about-history-text h2 { font-size: 1.5rem; margin-bottom: 12px; }

/* Delivery / contact section */
.delivery-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 32px;
  align-items: start;
}
.delivery-map { text-align: center; }
#delivery-map-canvas {
  width: 100%;
  height: 420px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  z-index: 1;
}
/* Mute the basemap so the green coverage area and the day-coloured stops carry
   the colour. Only the tile pane is filtered — our own layers sit in other panes. */
#delivery-map-canvas .leaflet-tile-pane { filter: saturate(0.3) brightness(1.05); }
.delivery-map-note {
  margin: 10px 0 0;
  font-size: 0.82rem;
  color: var(--text-muted);
}

/* Home page: a static illustration of the delivery range, with a button to the
   live address checker on its own page (delivery-area.html) rather than the
   interactive map itself. */
.delivery-static { text-align: center; }
.delivery-static img {
  width: 100%;
  max-width: 640px;
  height: auto;
  margin: 0 auto;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  /* Stated explicitly because <picture> serves two different crops here. The
     img's width/height attributes describe the desktop one, so on a phone the
     browser would reserve a 639:441 box and then reflow to the 380:441 crop
     once it loads — a visible jump on a lazy-loaded image. */
  aspect-ratio: 639 / 441;
}
@media (max-width: 640px) {
  .delivery-static img {
    aspect-ratio: 380 / 441;
    max-width: 380px;   /* its native width; past that it is just soft */
  }
}
.delivery-static .btn { margin-top: 18px; }
/* A slow breathing ring on the one button this whole section exists to drive
   traffic to — subtle enough to read as "alive," not "blinking ad." Pauses on
   hover/focus so it never fights the button's own lift transition. */
/* The ring lives on a pseudo-element and only ever scales and fades. Animating the
   button's own box-shadow — which is what this did — is a paint property, so every
   frame of a 2.6s infinite loop repainted the button and the area around it, on the
   homepage, for every visitor, forever. The shadow below is painted once and then
   handed to the compositor. */
@keyframes cta-pulse {
  0% { opacity: 0.75; transform: scale(1); }
  70%, 100% { opacity: 0; transform: scale(1.16); }
}
.delivery-static .btn-primary { position: relative; }
.delivery-static .btn-primary::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: 0 0 0 2px rgba(0, 134, 83, 0.45);
  pointer-events: none;
  animation: cta-pulse 2.6s var(--ease-out) infinite;
}
.delivery-static .btn-primary:hover::after,
.delivery-static .btn-primary:focus-visible::after,
.delivery-static.is-paused .btn-primary::after { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .delivery-static .btn-primary::after { animation: none; content: none; }
}
/* Warehouse pin. Appears only after a search — the resting map is the shaded area
   and nothing else. */
.delivery-hq-marker {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--green-darker);
  border: 3px solid var(--white);
  box-shadow: 0 0 0 2px var(--green-darker), 0 2px 6px rgba(0, 40, 24, 0.45);
}

.delivery-check-heading { font-size: 1.1rem; margin-bottom: 12px; }
.delivery-check-form {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 14px;
}
.delivery-address-field { position: relative; }
.delivery-check-form input[type="text"] {
  width: 100%;
  padding: 13px 40px 13px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-family: var(--font);
  font-size: 1rem;
  color: var(--text);
}
.delivery-check-form input[type="text"]:focus {
  outline: 2px solid var(--green-dark);
  outline-offset: 1px;
}
/* The address type-ahead's dropdown, shared by the delivery page's checker and the
   home page hero (js/address-suggest.js). Named for the module rather than either
   page since the two now render the same list. It positions against whichever
   ancestor is relative — .delivery-address-field there, .hero-check-row here — and
   stays a white popover on both, which is what a menu over the dark hero wants to
   be anyway. */
.address-suggest-listbox {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 20;
  margin: 0;
  padding: 6px;
  list-style: none;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  max-height: 260px;
  overflow-y: auto;
  text-align: left;
}
/* Flipped above the field when there isn't room below it. js/address-suggest.js
   adds this after measuring, and also caps max-height to whatever space the
   chosen direction actually has — so the list is never taller than the gap it
   is sitting in, whichever way it opens. See placeListbox() there for why the
   home page needs this and the delivery page doesn't. */
.address-suggest-listbox.is-above {
  top: auto;
  bottom: calc(100% + 4px);
}
.address-suggest-listbox li {
  padding: 10px 12px;
  border-radius: 6px;
  font-size: 0.92rem;
  color: var(--text);
  cursor: pointer;
}
.address-suggest-listbox li:hover,
.address-suggest-listbox li.is-active {
  background: var(--green-light);
  color: var(--green-dark);
}
/* "Searching…" / "no matches" — a status line, not something to pick. */
.address-suggest-listbox li.address-suggest-msg {
  color: var(--text-muted);
  cursor: default;
  font-size: 0.88rem;
}
.address-suggest-listbox li.address-suggest-msg:hover {
  background: transparent;
  color: var(--text-muted);
}
/* "Check "…" as typed" — the reader's own words, set apart from the places
   the geocoder offered so it reads as the first choice rather than a match. */
.address-suggest-listbox li.address-suggest-typed {
  font-weight: 600;
  border-bottom: 1px solid var(--border);
  border-radius: 6px 6px 0 0;
}
.delivery-check-result {
  display: none;
  padding: 14px 16px;
  border-radius: var(--radius-sm);
  font-weight: 700;
  font-size: 0.95rem;
  margin-bottom: 18px;
}
.delivery-check-result.is-visible { display: block; }
/* This is the moment the whole page exists to produce, and it used to snap in with
   no transition at all. The animation is keyed to .is-visible rather than to a
   transition because the element starts at display:none, which transitions cannot
   cross; delivery-map.js restarts it on each search so a second identical answer
   still registers as a new one. */
@keyframes result-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
.delivery-check-result.is-visible,
.delivery-day-result.is-visible { animation: result-in var(--dur-slow) var(--ease-out) both; }

/* The pending tier already said "Checking …" in text, but nothing moved, so over a
   slow connection it was indistinguishable from a button that did nothing. A sweep
   across the card reads as work in progress without claiming a percentage. */
@keyframes result-pending-sweep {
  from { transform: translateX(-100%); }
  to { transform: translateX(100%); }
}
.delivery-check-result.is-pending { position: relative; overflow: hidden; }
.delivery-check-result.is-pending::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(0, 134, 83, 0.10), transparent);
  /* linear on purpose: an eased loop pauses at each end and reads as a stutter. */
  animation: result-pending-sweep 1.1s linear infinite;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .delivery-check-result.is-visible,
  .delivery-day-result.is-visible { animation: none; }
  .delivery-check-result.is-pending::after { content: none; }
}

.delivery-check-result.is-yes { background: var(--green-light); color: var(--green-dark); }
/* A near miss is a lead, not a rejection. Red was the only red on the page and it read
   as a stamp of refusal to exactly the customer we most want picking up the phone, so
   the close tier gets amber and the far tier a neutral grey. */
.delivery-check-result.is-close { background: #fdf4e3; color: #7d5410; }
.delivery-check-result.is-far { background: #f2f4f3; color: var(--text); }
/* A definitive no. Firmer than the "call us anyway" tier above it — a left rule and
   full-strength text — but not alarm red: we aren't telling them they did something
   wrong, we're telling them where our trucks go. */
.delivery-check-result.is-out {
  background: #f2f4f3;
  color: var(--text);
  border-left: 4px solid #8d9a93;
}
.delivery-check-result.is-pending { background: var(--green-light-2); color: var(--text-muted); }
.delivery-check-result.is-error { background: var(--green-light-2); color: var(--text-muted); }

.delivery-day-result {
  display: none;
  padding: 14px 16px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--white);
  font-size: 0.92rem;
  color: var(--text-muted);
  margin-bottom: 18px;
}
.delivery-day-result.is-visible { display: block; }
/* Near-miss variant: the callback button alone, with no card around it. */
.delivery-day-result.is-bare { padding: 0; border: 0; background: none; }
.delivery-day-result.is-bare .delivery-callback { margin-top: 0; }
.delivery-day-result strong { color: var(--green-dark); }
.delivery-day-result .delivery-day-note {
  display: block;
  margin-top: 6px;
  font-size: 0.85rem;
}

.delivery-rep-card {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
.delivery-rep-card img {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  object-fit: cover;
  flex: none;
  border: 2px solid var(--white);
  box-shadow: var(--shadow);
}
.delivery-rep-card-info { min-width: 0; }
.delivery-rep-card-title {
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--green-dark);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.delivery-rep-card-name { font-size: 0.92rem; font-weight: 700; color: var(--text); }
.delivery-rep-card-phone { font-size: 0.85rem; }
.delivery-rep-card-phone a { color: var(--green-dark); font-weight: 700; text-decoration: none; }
.delivery-rep-card-phone a:hover { text-decoration: underline; }

/* The action that belongs to a "yes" — name, business, phone and a submit button,
   not a mailto: link, so nobody needs a configured mail app to reach us. */
.delivery-callback-form { margin-top: 14px; display: grid; gap: 12px; }
.delivery-day-result.is-bare .delivery-callback-form { margin-top: 0; }
.delivery-callback-field { display: grid; gap: 6px; }
.delivery-callback-field label {
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--text);
}
.delivery-callback-optional { font-weight: 400; color: var(--text-muted); }
.delivery-callback-field input {
  width: 100%;
  font-family: inherit;
  font-size: 1rem;
  color: var(--text);
  background: var(--white);
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.delivery-callback-field input:focus {
  outline: none;
  border-color: var(--green);
  box-shadow: 0 0 0 3px var(--green-light);
}
.delivery-callback-field input[aria-invalid="true"] { border-color: #b3261e; }
/* Stacked, not side by side: the rep's name varies the button text length ("Have
   Marty Manion call you" vs. a shorter name), and splitting the row was squeezing
   the phone field down to a few characters wide no matter whose name it was. */
.delivery-callback-row { display: flex; flex-direction: column; gap: 8px; }
.delivery-callback-submit { width: 100%; }
.delivery-callback-status { margin: 0; font-size: 0.85rem; }
.delivery-callback-status.is-error { color: #b3261e; font-weight: 600; }
.delivery-callback-status.is-note { color: var(--text-muted); }
.delivery-callback-sent { margin: 14px 0 0; font-size: 0.92rem; font-weight: 600; color: var(--green-dark); }

/* The caveat sits above the fold in the resting state, before the reader has asked us
   anything — full-strength body text made hedging the first thing they read. */
.delivery-check-note {
  font-size: 0.85rem;
  color: var(--text-muted);
  margin-bottom: 18px;
}

/* The sticky header is 69px tall, so an un-offset jump to #delivery-check parked the
   address field underneath it. */
.delivery-check { scroll-margin-top: 90px; }

/* CTA band — the old pale, centred version. In the 2026 redesign .cta-band is a
   full-bleed forest-green <section> instead of a centred block inside one, and
   the rules that make it that live in the component layer at the foot of this
   file. Nothing is left here: keeping `text-align: center` and a 1.5rem h2
   around would only fight those rules from a weaker position in the cascade. */

/* Simple content pages (catalog / credit application) */
.doc-hero { text-align: center; padding: 56px 0 40px; }
.doc-hero-compact { padding: 56px 0 8px; }
.doc-icon {
  width: 72px; height: 72px;
  margin: 0 auto 20px;
  border-radius: 18px;
  background: var(--green-light);
  color: var(--green);
  display: flex; align-items: center; justify-content: center;
}
.doc-icon svg { width: 36px; height: 36px; }
.doc-hero h1 { font-size: 1.8rem; }
.doc-hero p { max-width: 48ch; margin: 0 auto 26px; }
.doc-hero-compact p { margin-bottom: 0; }

.doc-download { padding: 8px 0 56px; }

.doc-card {
  max-width: 560px;
  margin: 0 auto 20px;
  background: var(--green-light-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 28px 24px;
  text-align: center;
}
.doc-card .filename { font-weight: 700; color: var(--green-dark); margin-bottom: 6px; }
.doc-card .meta { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 20px; }

.doc-alt {
  max-width: 560px;
  margin: 0 auto 24px;
  text-align: center;
  font-size: 0.92rem;
  color: var(--text-muted);
}
.doc-alt a { font-weight: 700; }

.doc-note {
  max-width: 560px;
  margin: 0 auto;
  text-align: center;
  font-size: 0.92rem;
}

/* Credit application inline PDF preview */
.doc-preview-inline {
  display: block;
  width: 100%;
  height: 55vh;
  min-height: 320px;
  max-height: 520px;
  margin-bottom: 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--white);
}
.doc-preview-fallback {
  margin: 0 0 20px;
  text-align: center;
  font-size: 0.82rem;
  color: var(--text-muted);
}

/* Catalog search */
.catalog-search { padding: 8px 0 56px; }
.catalog-search h2 { font-size: 1.4rem; margin-bottom: 4px; }
.catalog-search-meta { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 20px; }

/* Search controls + result count stay visible while the (long) results
   list scrolls beneath them, so a search can be tweaked without scrolling
   back up — mirrors how most mobile search UIs keep the query in view. */
.catalog-search-sticky {
  position: sticky;
  top: 69px;
  z-index: 30;
  background: var(--white);
  padding: 10px 0 8px;
  box-shadow: 0 8px 12px -10px rgba(15, 40, 25, 0.25);
}

.catalog-search-controls {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-bottom: 16px;
}
.catalog-field { position: relative; }
.catalog-field label {
  display: block;
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--green-dark);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin-bottom: 6px;
}
.catalog-input-wrap { position: relative; }
.catalog-search-controls input[type="text"] {
  width: 100%;
  padding: 13px 40px 13px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-family: var(--font);
  font-size: 1rem;
  color: var(--text);
  background: var(--white);
}
.catalog-search-controls input[type="text"]:focus {
  outline: 2px solid var(--green-dark);
  outline-offset: 1px;
}
.catalog-clear-btn {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--text-muted);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
}
.catalog-clear-btn:hover { background: var(--green-light); color: var(--green-dark); }
.catalog-clear-btn[hidden] { display: none; }
/* The circle stays 28px — big enough for a pointer, and a 44px disc inside a 48px
   field would read as a second button rather than a way to empty the one it sits
   in. The touch target is the one that grows: an invisible 44px square centred on
   it, which is the minimum a thumb needs and the same rule the card links and the
   map controls were widened to meet. It overhangs the circle by 8px on each side,
   which lands inside the field vertically and in empty gap horizontally.
   Shared by all three of these: the catalog search, the delivery-area address
   field, and the home page hero. */
.catalog-clear-btn::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

.catalog-brand-suggestions {
  list-style: none;
  margin: 4px 0 0;
  padding: 6px;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 20;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  max-height: 260px;
  overflow-y: auto;
}
.catalog-brand-suggestions[hidden] { display: none; }
.catalog-brand-suggestions li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 8px;
  font-size: 0.95rem;
  color: var(--text);
  cursor: pointer;
}
/* Now that one box searches items, brands and numbers alike, a bare name in the
   dropdown is ambiguous — this says which of the three you'd be picking. */
.catalog-suggestion-tag {
  flex: none;
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--green-dark);
  background: var(--green-light);
  border-radius: 4px;
  padding: 3px 7px;
}
.catalog-brand-suggestions li:hover,
.catalog-brand-suggestions li.is-active {
  background: var(--green-light);
  color: var(--green-dark);
}

.catalog-result-count { font-size: 0.85rem; color: var(--text-muted); margin-bottom: 12px; }
.catalog-codes-note { font-size: 0.82rem; color: var(--text-muted); margin: 0 0 10px; }
.catalog-browse-link { font-size: 0.88rem; color: var(--text-muted); margin: 0 0 18px; }

/* The one thing a returning customer actually needs — a phone number — sitting
   in the same sticky band as the search itself, so scrolling through results
   never puts it out of reach. */
.catalog-order-cta {
  margin: 10px 0 0;
  padding-top: 10px;
  border-top: 1px dashed var(--border);
  font-size: 0.88rem;
  font-weight: 600;
  color: var(--green-dark);
}
.catalog-order-cta a { color: inherit; text-decoration: underline; }

/* Mobile: stacked card, no fixed columns, no header (avoids side-to-side scrolling) */
.catalog-results-header { display: none; }
.catalog-results-header[hidden] { display: none; }

/* Finding 08. This list used to carry max-height:720px with its own overflow-y:auto
   at every width. On a 375x812 phone that inner scroller was 89% of the screen
   holding 18,800px of content, so a thumb swiping to leave the page scrolled the
   list instead and the page underneath never moved. Mobile-first now: the list
   flows with the document here, and the cap comes back at 860px where a bounded
   pane next to a mouse wheel is the right shape. `overflow: visible` rather than
   just dropping max-height — overflow-x:hidden alone would force overflow-y to a
   scroll container again. The query still stays in view while scrolling: that is
   .catalog-search-sticky's job, not this element's. */
.catalog-results {
  list-style: none;
  margin: 0;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-height: 420px;
  overflow: visible;
  background: var(--green-light-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.catalog-results:focus-visible {
  outline: 3px solid var(--green-dark);
  outline-offset: 2px;
}
.catalog-result-card {
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 14px 16px;
  box-shadow: var(--shadow);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 12px;
  row-gap: 4px;
}
.catalog-result-desc { flex: 1 1 100%; order: -1; font-weight: 600; color: var(--text); }
/* Item number doubles as a tap-to-copy button — a returning customer's real
   task is collecting these to phone in. Reset to look like the plain text it
   replaced, then add just enough affordance (underline, color swap) to read
   as clickable without disrupting the row's layout. */
.catalog-result-number {
  font-family: inherit;
  font-size: 0.8rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  text-align: left;
  cursor: pointer;
}
.catalog-result-number:hover,
.catalog-result-number:focus-visible { color: var(--green-dark); text-decoration: underline; }
.catalog-result-number:focus-visible { outline: 2px solid var(--green-dark); outline-offset: 2px; }
.catalog-result-number.is-copied { color: var(--good); font-weight: 700; }
.catalog-result-number:disabled { cursor: default; }
.catalog-result-pack { font-size: 0.8rem; color: var(--text-muted); }
.catalog-result-brand { font-size: 0.8rem; color: var(--green-dark); font-weight: 600; }

.catalog-empty-state,
.catalog-error-state {
  text-align: center;
  color: var(--text-muted);
  padding: 32px 16px;
}
/* "Nothing matched" is an answer too, and it arrived with no transition while the
   rows it replaced were about to gain one. Same keyframe the delivery result uses. */
.catalog-empty-state { animation: result-in var(--dur-slow) var(--ease-out) both; }

/* Search results arrive in a short cascade rather than all at once. js/catalog-search.js
   sets --row-delay per row and only tags the first dozen — see the note in render()
   for why both the count and the delay are capped. */
@keyframes catalog-row-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}
.catalog-result-card.is-new {
  animation: catalog-row-in var(--dur-slow) var(--ease-out) both;
  animation-delay: var(--row-delay, 0ms);
}

/* Waiting on the 421KB catalogue. The sweep is the delivery checker's pending
   treatment reused verbatim — the same problem (a control that looks inert while
   the network works) deserves the same answer, and the keyframe already exists. */
.catalog-loading {
  position: relative;
  overflow: hidden;
  text-align: center;
  color: var(--text-muted);
  padding: 32px 16px;
  border-radius: var(--radius-sm);
  background: var(--paper);
  border: 1px solid var(--border);
}
.catalog-loading::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(0, 134, 83, 0.10), transparent);
  animation: result-pending-sweep 1.1s linear infinite;
  pointer-events: none;
}
/* A duration token can't stop a loop, and `both` fill would leave a staggered row
   parked at opacity 0 if its animation never ran — so both need explicit opt-outs. */
@media (prefers-reduced-motion: reduce) {
  .catalog-empty-state,
  .catalog-result-card.is-new { animation: none; }
  .catalog-loading::after { content: none; }
}

/* Resting state, before anything is typed. Sits inside the results box rather
   than replacing it, so the list doesn't collapse and then jump back open on
   the first keystroke. */
.catalog-prompt {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 48px 20px;
  margin: auto 0;
}
.catalog-prompt-lead {
  font-weight: 800;
  font-size: 1.15rem;
  color: var(--text);
  margin: 0 0 6px;
}
.catalog-prompt-hint {
  font-size: 0.95rem;
  color: var(--text-muted);
  max-width: 42ch;
  margin: 0 0 28px;
}
.catalog-prompt-chip-label {
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin: 0 0 12px;
}
.catalog-prompt-chips {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
}
.catalog-prompt-chip {
  font-family: var(--font);
  font-size: 0.92rem;
  font-weight: 600;
  color: var(--green-dark);
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 9px 18px;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-standard), border-color var(--dur-fast) var(--ease-standard);
}
.catalog-prompt-chip:hover {
  background: var(--green-light);
  border-color: var(--green-dark);
}
.catalog-prompt-chip:focus-visible {
  outline: 2px solid var(--green-dark);
  outline-offset: 2px;
}

/* What We Carry — real, crawlable brand text linking into pre-filled catalog
   searches. Accent-bar blocks rather than another icon-card grid, matching the
   restraint the rest of the site already commits to. */
.wwc-section {
  padding-left: 18px;
  border-left: 3px solid var(--green-light);
  margin-bottom: 40px;
}
.wwc-section:last-of-type { margin-bottom: 28px; }
.wwc-section h2 { font-size: 1.3rem; margin-bottom: 6px; }
.wwc-section > p { margin: 0 0 16px; color: var(--text-muted); max-width: 60ch; }
.wwc-see-all { font-weight: 700; font-size: 0.92rem; }
.wwc-footnote { color: var(--text-muted); font-size: 0.92rem; max-width: 60ch; }

/* Footer */
.site-footer {
  background: var(--green-darker);
  color: rgba(255,255,255,0.88);
  padding: 44px 0 24px;
}
.footer-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 32px;
  margin-bottom: 32px;
}
.footer-logo {
  color: var(--white);
  font-size: 1.35rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  margin-bottom: 14px;
}
.footer-brand p { color: rgba(255,255,255,0.65); font-size: 0.9rem; max-width: 32ch; }
.footer-social { display: flex; gap: 12px; margin-top: 16px; }
.footer-social a {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: rgba(255,255,255,0.1);
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background var(--dur-fast) var(--ease-standard);
}
.footer-social a:hover { background: rgba(255,255,255,0.22); }
.footer-social svg { width: 18px; height: 18px; }
.footer-col h4 {
  color: var(--white);
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 14px;
}
.footer-col ul { list-style: none; margin: 0; padding: 0; }
/* Finding 16. Footer links measured 20px tall on a phone, stacked 10px apart —
   and the footer is where the phone number, email and address live, so these are
   links people actually reach for. Padding on the anchor (not the li) keeps the
   trailing "(toll-free)" text on the same baseline. 40px tall on a 4px gap; the
   640px block below puts the tighter desktop rhythm back. */
.footer-col li { margin-bottom: 4px; font-size: 0.92rem; }
.footer-col a { color: rgba(255,255,255,0.82); display: inline-block; padding: 11px 0; }
.footer-col a:hover { color: var(--white); }
.footer-bottom {
  border-top: 1px solid rgba(255,255,255,0.14);
  padding-top: 20px;
  font-size: 0.82rem;
  color: rgba(255,255,255,0.55);
  text-align: center;
}

/* Tablet+ */
@media (min-width: 640px) {
  /* Touch targets are a phone problem; put the tighter footer rhythm back where
     the pointer is a mouse. */
  .footer-col li { margin-bottom: 10px; }
  .footer-col a { padding: 0; }
  .why-us-list { grid-template-columns: repeat(2, 1fr); column-gap: 32px; }
  .quote-card { flex-basis: 320px; }
  .testimonial-grid { grid-template-columns: repeat(2, 1fr); }
  .footer-grid { grid-template-columns: 1.4fr 1fr 1fr; }
  .delivery-check-form { flex-direction: row; }
  .delivery-address-field { flex: 1; }
  .team-grid { grid-template-columns: repeat(3, 1fr); gap: 18px; }
  .team-card { padding: 24px 16px; }
  .team-card img { width: 104px; height: 104px; margin-bottom: 14px; }
  .team-card h3 { font-size: 1.05rem; }
  .team-card .role { font-size: 0.85rem; }
  .team-group-heading { font-size: 1.2rem; }

  .catalog-search-controls .catalog-field { flex: 1 1 auto; }

  /* Finding 08: the bounded results pane belongs to tablet and up, where there's
     a mouse wheel and the page isn't the scroll surface a thumb reaches for. The
     sticky column header below only works inside a scroll container, so the two
     have to arrive together. */
  .catalog-results {
    max-height: 720px;
    overflow-y: auto;
    overflow-x: hidden;
  }

  /* Tablet+: switch to an aligned column table with a sticky header */
  .catalog-results-header {
    display: grid;
    position: sticky;
    /* .catalog-results has 16px of padding; sticky offset is measured from
       that padding edge, so without canceling it via negative margin a
       16px sliver of the row scrolling underneath peeks out above the
       header. Pulling the header flush to the container's true edge (and
       restoring the lost space as its own top padding) closes that gap. */
    top: -16px;
    margin: -16px -16px 2px;
    background: var(--green-light);
    border: 1px solid transparent;
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
    /* Horizontal padding is 16px (container inset) + 16px (own) = 32px, to
       match the row cards' inset (container's 16px padding + the card's
       own 16px padding) now that the negative margin above cancels the
       container's padding just for this element. */
    padding: 26px 32px 10px;
    font-size: 0.72rem;
    font-weight: 700;
    color: var(--green-dark);
    text-transform: uppercase;
    letter-spacing: 0.03em;
  }
  .catalog-results-header[hidden] { display: none; }
  .catalog-results-header,
  .catalog-result-card {
    grid-template-columns: 80px 1fr 110px 150px;
    column-gap: 16px;
    display: grid;
    align-items: center;
  }
  .catalog-result-desc { flex-basis: auto; order: 0; }
}

/* Weekly Sale Flyer — a floating button linking straight to the PDF
   (assets/documents/weekly-flyer.pdf), shown only within its configured
   date range. Swapping the flyer is just replacing that file. */
.flyer-tab {
  position: fixed;
  right: calc(16px + env(safe-area-inset-right, 0px));
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  z-index: 92;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 18px;
  border: none;
  border-radius: 999px;
  background: var(--green-dark);
  color: var(--white);
  text-decoration: none;
  white-space: nowrap;
  box-shadow: 0 10px 28px rgba(0, 40, 24, 0.38);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-standard),
              transform var(--dur-fast) var(--ease-out),
              opacity var(--dur-fast) var(--ease-standard),
              visibility var(--dur-fast) var(--ease-standard);
}

/* Held back until the hero is behind you — see holdBackUntilHeroPasses() in
   js/weekly-flyer.js for why. The attribute is only set on pages that have a
   hero, so everywhere else the tab behaves exactly as it always has.

   :not(.is-revealed) rather than a plain hidden state, so the revealed tab
   carries no transform of its own and :hover keeps its lift. visibility is in
   here with opacity so the button is not a focus stop while it is invisible. */
.flyer-tab[data-reveal-on-scroll]:not(.is-revealed) {
  opacity: 0;
  visibility: hidden;
  transform: translateY(10px);
  pointer-events: none;
}

.flyer-tab:hover,
.flyer-tab:focus-visible { background: var(--green-darker); transform: translateY(-2px); color: var(--white); text-decoration: none; }
.flyer-tab:active { transform: translateY(0) scale(0.97); }
.flyer-tab-icon { flex: none; width: 20px; height: 20px; }
.flyer-tab-text { display: flex; flex-direction: column; line-height: 1.2; }
.flyer-tab-title { font-weight: 800; font-size: 0.92rem; letter-spacing: 0.01em; }
.flyer-tab-dates { font-size: 0.7rem; font-weight: 600; color: var(--green-light); }

/* The flyer overlay — the sale sheet shown on the page instead of handed to the
   browser as a bare PDF. Built by js/weekly-flyer.js on the same file check that
   raises the button, so it only ever exists when there is a flyer to put in it.

   The frame holds the PDF itself rather than markup rebuilt from it, which is
   what keeps Savannah's Monday job down to replacing one file. */
.flyer-overlay {
  position: fixed;
  inset: 0;
  z-index: 120; /* over the sticky header (50) and the floating tab (92) */
  display: grid;
  place-items: center;
  padding: max(16px, env(safe-area-inset-top, 0px)) 16px max(16px, env(safe-area-inset-bottom, 0px));
}
.flyer-overlay[hidden] { display: none; }

.flyer-overlay-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(3, 24, 15, 0.62);
  animation: flyer-fade var(--dur-base) var(--ease-standard);
}
/* The frost is decoration a phone GPU should not pay for: a backdrop blur
   re-samples the whole viewport on every frame of the fade. Desktop only. */
@media (min-width: 1180px) and (hover: hover) {
  .flyer-overlay-backdrop {
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
  }
}

.flyer-dialog {
  position: relative;
  display: flex;
  flex-direction: column;

  /* The sheet is one page of letter portrait, so height is the binding
     constraint and width follows from it. Fitting a whole portrait page into a
     landscape frame leaves the page floating in the viewer's grey with a third
     of the dialog unused on either side; deriving the width from the height
     instead lets the page hug its own window.

     --flyer-chrome is everything stacked above and below the page itself: this
     dialog's header and footer, plus the PDF viewer's own toolbar inside the
     frame. It is a measured approximation, not a guarantee — if the footer wraps
     on a narrow viewport the real chrome is taller and the page lands slightly
     narrower than the frame. That direction is harmless: #view=Fit means the
     whole page is on screen either way, and the miss shows up as a thin grey
     margin rather than a crop. */
  --flyer-h: min(92vh, 1100px);
  --flyer-chrome: 186px;
  --flyer-page-ratio: 0.773; /* 8.5 / 11 */

  height: var(--flyer-h);
  width: min(100%, calc((var(--flyer-h) - var(--flyer-chrome)) * var(--flyer-page-ratio)));
  min-width: 320px;
  background: var(--paper);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  animation: flyer-rise var(--dur-slow) var(--ease-editorial);
}

.flyer-dialog-head {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 16px 14px 20px;
  background: var(--green-dark);
  color: var(--white);
}
.flyer-dialog-heading { display: flex; flex-direction: column; gap: 2px; }
.flyer-dialog-title {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 800;
  letter-spacing: 0.01em;
  color: var(--white);
}
.flyer-dialog-dates {
  margin: 0;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--green-light);
}

.flyer-dialog-close {
  flex: none;
  display: grid;
  place-items: center;
  /* 44px: the tap target stays a full target on a phone even though the glyph
     inside it is 18px. */
  width: 44px;
  height: 44px;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--white);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-standard);
}
.flyer-dialog-close svg { width: 18px; height: 18px; }
.flyer-dialog-close:hover,
.flyer-dialog-close:focus-visible { background: rgba(255, 255, 255, 0.16); }

.flyer-dialog-frame {
  flex: 1 1 auto;
  width: 100%;
  min-height: 0; /* without this the frame refuses to shrink inside the flex column */
  border: none;
  background: var(--sand);
}

.flyer-dialog-foot {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  /* Centred rather than pushed apart: the dialog is now only as wide as the page
     it frames, so these two wrap onto separate lines more often than not, and a
     space-between row that has wrapped reads as two stray left-aligned items. */
  justify-content: center;
  text-align: center;
  padding: 12px 20px;
  border-top: 1px solid var(--sand-deep);
  background: var(--bone);
}
.flyer-dialog-fallback {
  margin: 0;
  font-size: 0.85rem;
  color: var(--ink-soft);
}

body.flyer-open { overflow: hidden; }

/* The tab that opened the overlay sits behind a 62%-opaque scrim, which leaves it
   dimmed but legible in the corner — the button you just pressed, still asking to
   be pressed. It reuses the same transition the scroll-reveal rule above set on
   it, so it fades rather than blinking out.

   Deliberately opacity and not visibility, unlike the scroll-reveal rule. The tab
   is where focus goes when the overlay closes, and visibility is in that shared
   transition — so a hidden tab is still computed `hidden` for the 120ms after the
   class comes off, and .focus() on it silently does nothing. Keyboard users would
   press Escape and land on <body>. Being invisible is all this rule needs to do;
   openOverlay() marks the tab inert, which is what actually takes it out of the
   tab order while the dialog is up. */
body.flyer-open .flyer-tab {
  opacity: 0;
  pointer-events: none;
}

/* Two more moments the tab gets out of the way. It sat on top of the open
   mobile menu, still tappable, and on a phone it clipped the callback form's
   "Have ___ call you" button — the one button on the site that turns a
   visitor into a lead. body.form-active is set by weekly-flyer.js from the
   moment focus enters a form until that form scrolls off screen.
   visibility is fine here: nothing restores focus to the tab from either. */
body.nav-open .flyer-tab,
body.form-active .flyer-tab {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

/* Both animations collapse to nothing under prefers-reduced-motion, because the
   duration tokens they read are redefined to 1ms there — see the motion scale at
   the top of this file. */
@keyframes flyer-fade { from { opacity: 0; } }
@keyframes flyer-rise {
  from { opacity: 0; transform: translateY(12px) scale(0.985); }
}

@media (max-width: 620px) {
  /* Full-bleed on a phone: a centred card with margins wastes the only axis the
     flyer needs, and the rounded corners stop reading as a card anyway. */
  .flyer-overlay { padding: 0; }
  .flyer-dialog { width: 100%; height: 100%; border-radius: 0; }
  .flyer-dialog-foot { justify-content: center; }
}

@media (min-width: 860px) {
  .hero { padding: 84px 0 64px; }
  .hero h1 { font-size: 2.8rem; }
  .hero-sub { font-size: 1.2rem; }
  .hero-split .hero-grid {
    grid-template-columns: 1.05fr 0.95fr;
    gap: 56px;
    text-align: left;
  }
  .hero-split .hero-sub { margin: 0 0 28px; }
  /* Findings 01/02: the grid above went left-aligned, but these two blocks kept
     the mobile centering that made sense in a single narrow column — leaving
     the headline flush left and the buttons floating in the middle of a much
     wider one. max-width: none lets the row size to its buttons instead of
     the old 320px mobile column, which is what makes a side-by-side row
     possible without either button losing its own padding to a squeeze. */
  .hero-split .hero-ctas { margin: 0; text-align: left; max-width: none; }
  .hero-split .hero-cta-buttons { flex-direction: row; align-items: center; }
  .hero-split .hero-existing { text-align: left; margin: 20px 0 0; max-width: none; }
  .hero-split .hero-existing-links { justify-content: flex-start; }

  section { padding: 72px 0; }
  .why-us-grid { grid-template-columns: 0.85fr 1.15fr; gap: 56px; }
  /* Finding 09. The address field now comes first in the markup, because on a phone
     "Do We Deliver to You?" is a question whose answer is an input box, and 420px of
     map used to sit between the heading and the field. Reordering the DOM rather than
     using `order` on the mobile view keeps tab order matching what is on screen where
     the two are stacked; at this width they are side by side and the map goes back to
     the left visually, which is the layout that was designed. */
  .delivery-grid { grid-template-columns: 1.3fr 1fr; }
  .delivery-map { order: -1; }
  #delivery-map-canvas { height: 480px; }
  .about-history-grid { grid-template-columns: 1fr 1fr; gap: 48px; }
  .team-grid { grid-template-columns: repeat(5, 1fr); gap: 20px; }
  .testimonial-grid { grid-template-columns: repeat(3, 1fr); }
  .team-card img { width: 112px; height: 112px; }
  .team-group { margin-bottom: 48px; }
  .doc-hero { padding: 84px 0 56px; }
  .doc-hero-compact { padding: 84px 0 8px; }
  .doc-download { padding: 8px 0 72px; }
}

/* The full horizontal nav needs more room than the general 860px desktop breakpoint
   provides — below ~1180px the labels were wrapping to two lines and going
   left-aligned. Keep the off-canvas menu until there's enough width to lay every
   item out on one line.

   1180 was measured against a six-item nav. It is now five (Home, Product Catalog,
   Delivery Area, About, Contact Us) — Credit Application moved out earlier, and
   Customer Login is commented out in the markup until the ordering portal exists.
   At 1300px the bar measures 277px of brand + 627px of nav + 40px padding = 944px,
   so this breakpoint is carrying roughly 230px of slack and tablets get a hamburger
   where the full bar would now fit. Worth re-measuring properly and lowering — but
   not while Customer Login is due to come back and claim ~165px of that again. */
@media (min-width: 1180px) {
  .brand { flex-shrink: 0; }
  .nav-toggle { display: none; }
  .header-call { display: none; }
  .site-nav {
    position: static;
    transform: none;
    /* The panel rules above hide the nav until .is-open; here it is the ordinary
       horizontal bar, so it is always visible and the body is never locked. */
    visibility: visible;
    background: none;
    overflow: visible;
  }
  body.nav-open { overflow: visible; }
  .site-nav > ul {
    flex-direction: row;
    align-items: center;
    padding: 0;
    gap: 4px;
  }
  .site-nav > ul > li { flex-shrink: 0; }
  .site-nav a {
    border-bottom: none;
    padding: 10px 12px;
    font-size: 0.98rem;
    white-space: nowrap;
  }
  .site-nav .nav-cta { margin-top: 0; padding: 0 0 0 8px; }
  .nav-cta .nav-dropdown-toggle { width: auto; }
  .nav-cta .nav-dropdown-menu { left: auto; right: 0; }
  .site-nav .nav-login { margin-top: 0; padding: 0 0 0 8px; flex: none; }
  .site-nav a.nav-login-link { width: auto; padding: 10px 22px; }
  .nav-cta { flex: none; }

  .nav-dropdown-toggle {
    width: auto;
    padding: 10px 12px;
    border-bottom: none;
    font-size: 0.98rem;
    white-space: nowrap;
  }
  .nav-dropdown-menu {
    position: absolute;
    top: 100%;
    left: 16px;
    z-index: 60;
    min-width: 170px;
    display: block;
    grid-template-rows: none;
    overflow: visible;
    padding: 6px;
    background: var(--white);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-6px);
    transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out), visibility var(--dur-fast) var(--ease-out);
  }
  .nav-dropdown.is-open .nav-dropdown-menu {
    opacity: 1;
    visibility: visible;
    /* The accordion rule above is a 0fr/1fr grid — same specificity, and this
       block only overrides what it names. The popup sizes itself, so cut the
       coupling explicitly. */
    display: block;
    grid-template-rows: none;
    /* Restated so the accordion transition added to the base .is-open rule (which
       has the same specificity but sits earlier) does not leak in and kill the fade. */
    transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out), visibility var(--dur-fast) var(--ease-out);
    transform: translateY(0);
  }
  .nav-dropdown-menu a {
    border-bottom: none;
    border-radius: 8px;
    padding: 10px 12px;
    font-size: 0.95rem;
  }
  .nav-dropdown-menu a:hover { background: var(--green-light); color: var(--green-dark); }
}

/* ---- Finding 16, continued: the other undersized targets ----
   "Our Team →" and "Apply now →" measured 23px tall, and Leaflet ships its zoom
   buttons at 30×30. Both are below the 44px minimum and both are things people
   tap. Kept to phone widths for the card links, where thumbs are the input. */
.why-us-item a.card-link,
.step-copy a.card-link {
  margin-top: 2px;
  padding: 11px 0;
}
@media (min-width: 640px) {
  .why-us-item a.card-link,
  .step-copy a.card-link { margin-top: 8px; padding: 4px 0; }
}

/* Leaflet's own stylesheet sets 30×30; ours loads after it, so this wins without
   !important. Applied at every width — the map is dragged and zoomed by touch on
   tablets too. */
.leaflet-touch .leaflet-bar a,
.leaflet-bar a {
  width: 44px;
  height: 44px;
  line-height: 44px;
}

/* ---- Finding 17: the testimonial carousel on a phone ----
   Two 44px arrows sat inside the track, leaving 231px of a 375px screen, so each
   card rendered 183px wide. Below 640px the arrows are hidden and the track takes
   the full width — swipe and scroll-snap already do the job, and the scroller is
   still a focusable region with arrow-key scrolling for keyboard users. Scoped as
   a max-width query on purpose: this rule sits at the end of the file, so a base
   rule here would have overridden the 640px block and hidden the arrows entirely. */
@media (max-width: 639px) { .quote-arrow { display: none; } }

/* Card height and where the attribution sits, both answered only from 640px up.

   From 640px up, two or more cards sit side by side and their ragged bottoms are
   the thing you notice before you read any of them. Flex children stretch by
   default, so letting that happen sizes every card to the tallest quote in the
   set — currently Jim Polinsky's — and the row reads as one band.

   That alone leaves the attribution wherever the quote happened to stop, so a
   short quote strands its name in the middle of a full-height card. The auto
   margin above the name eats the slack instead and drops the block to the floor.

   Names still would not line up, because a role that wraps to two lines pushes
   its own name up a line — "General Manager, Minnesuing Acres — Lake Nebagamon,
   WI" against "Owner, Gronks Grill and Bar — Superior, WI". Reserving two lines
   under every name is what actually puts Jim's name level with Sandy's. Two is
   the most any role in the set wraps to at these widths; the em value is the
   same measure for browsers without `lh`, and a role longer than two lines
   would simply take the space it needs and sit a line lower.

   Below 640px there is one card on screen at a time — no ragged bottoms to tidy,
   nothing to line a name up against, and stretching there only hangs a couple of
   hundred pixels of empty card under a nine-word quote, which is what put the
   flex-start rule in the file in the first place. The whole treatment stays
   inside the query rather than relying on the stretched card to make it inert,
   because the reserved role space would not be inert. */
.quote-scroller { align-items: flex-start; }
@media (min-width: 640px) {
  .quote-scroller { align-items: stretch; }
  .quote-scroller .quote-card { display: flex; flex-direction: column; }
  .quote-scroller .quote-name { margin-top: auto; }
  .quote-scroller .quote-role {
    min-height: 3.2em;   /* 2 × the 1.6 line-height */
    min-height: 2lh;
  }

  /* "Read all reviews" starts at the container edge, which puts it under the
     arrow rather than under the cards — the row above it starts a full arrow
     further in. Same inset as the progress rail, from the same token, so the
     button's left edge lands on the rail's and on the first card's. The
     adjacent-sibling selector keeps this to the button that closes this
     scroller; .section-cta is a general class. */
  .quote-scroller-wrap + .section-cta { margin-left: var(--quote-rail-inset); }
}

/* ---- Finding 15: is the office actually open right now ----
   Sits at the top of the Contact dropdown (the moment before someone calls) and
   beside the hours already printed in the footer. Deliberately not in the header
   bar itself: at 375px that row is already logo + call + hamburger with no room
   left, which finding 03 ran into. Both slots ship `hidden` and are revealed by
   js/main.js, so a stale or wrong status can never render without the script. */
.nav-hours {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 10px 8px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
  font-size: 0.9rem;
  color: var(--text-muted);
}
/* flex-shrink: 0, not white-space: nowrap — in a flex row, overflow-wrap: anywhere
   (site-wide, above) lets the browser shrink a text item's box below its own content
   width and then break the word mid-letter to fit. Refusing to shrink at all removes
   the overflow that triggers that break, the same fix already used on .hours-dot. */
.nav-hours b { color: var(--text); font-weight: 700; flex-shrink: 0; }
.nav-hours.is-open b { color: var(--green-dark); }

.hours-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  font-size: 0.86rem;
  color: rgba(255,255,255,0.7);
}
.hours-status b { color: var(--white); font-weight: 700; flex-shrink: 0; }

/* The dot is decoration — the words carry the meaning, so colour is never the only
   channel and this stays readable for a colour-blind reader. */
.hours-dot {
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.55;
}
.is-open .hours-dot { background: var(--green); opacity: 1; }
/* Except in the footer, where that green dot sits on --green-band and measures
   1.45:1 against it — green on green, a status light you cannot see lit. The band
   has its own accent for exactly this and it comes out at 4.62:1, well past the
   3:1 a non-text mark needs.

   Scoped by .hours-status because that class is the footer slot specifically; the
   Contact dropdown (.nav-hours) and the contact page (.contact-hours-status) show
   the same component on white, where --green is already right. */
.hours-status.is-open .hours-dot { background: var(--green-on-band); }
.footer-hours { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; }
.hours-detail { font-weight: 400; }

/* ---- Finding 14: a real contact page ----
   Contact used to be a dropdown holding a mailto:, a tel: and Facebook. contact.html
   puts the hours, address, map, both phone numbers and a short message form on one
   page. Everything below is scoped to that page's own classes, so nothing here can
   reach the rest of the site.

   Note the file-ordering rule that bites here: these rules sit AFTER the 640px and
   1180px blocks above, so the desktop layout for them has to be its own query at the
   very bottom of this file rather than an addition to those blocks. */
.contact-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 28px;
  align-items: start;
}

.contact-panel-heading { font-size: 1.25rem; margin-bottom: 16px; }

.contact-details {
  background: var(--green-light-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 24px 20px;
}
.contact-detail-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 20px;
}
.contact-detail-list > li {
  display: flex;
  gap: 12px;
  align-items: flex-start;
}
.contact-detail-list > li > svg {
  flex: none;
  width: 20px;
  height: 20px;
  margin-top: 2px;
  color: var(--green);
}
.contact-detail-body { min-width: 0; line-height: 1.7; }
.contact-detail-label {
  display: block;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 2px;
}
.contact-detail-strong {
  font-weight: 700;
  color: var(--text);
  font-size: 1.02rem;
}
a.contact-detail-strong { color: var(--green-dark); }
.contact-detail-note { color: var(--text-muted); font-size: 0.86rem; }
.contact-detail-link {
  /* Block so it always starts its own line — inline-block let it ride up beside
     "Superior, WI 54880" once the column was wide enough. width:fit-content keeps
     the hit area on the text rather than spanning the whole card. */
  display: block;
  width: fit-content;
  margin-top: 6px;
  font-weight: 700;
  font-size: 0.92rem;
  /* Finding 16 again: an inline text link is the thing people tap to get driving
     directions, so give it a thumb's worth of height rather than a line's. */
  padding: 10px 0;
  line-height: 1;
}

/* The live open/closed status, third rendering of it after the nav row and the
   footer. Here it sits inside an uppercase label, so it sets its own case back. */
.contact-hours-status {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-left: 8px;
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--text-muted);
}
.contact-hours-status b { color: var(--text); font-weight: 700; flex-shrink: 0; }
.contact-hours-status.is-open b { color: var(--green-dark); }

/* Form */
.contact-form-intro { font-size: 0.95rem; margin-bottom: 20px; }
.contact-form { display: grid; gap: 16px; }
.contact-field { display: grid; gap: 6px; }
.contact-field label {
  font-weight: 700;
  font-size: 0.9rem;
  color: var(--text);
}
.contact-req { color: var(--green); }
.contact-field input,
.contact-field textarea {
  width: 100%;
  font-family: inherit;
  font-size: 1rem;
  color: var(--text);
  background: var(--white);
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.contact-field textarea { resize: vertical; min-height: 120px; }
.contact-field input:focus,
.contact-field textarea:focus {
  outline: none;
  border-color: var(--green);
  box-shadow: 0 0 0 3px var(--green-light);
}
.contact-field input[aria-invalid="true"],
.contact-field textarea[aria-invalid="true"] { border-color: #b3261e; }

.contact-field-row { display: grid; grid-template-columns: 1fr; gap: 16px; }
.contact-field-note {
  margin: -6px 0 0;
  font-size: 0.84rem;
  color: var(--text-muted);
}

/* Errors carry a word as well as a colour — red alone is not a channel every
   reader has. Ships hidden and is revealed by js/contact-form.js. */
.contact-error {
  display: block;
  font-size: 0.86rem;
  font-weight: 600;
  color: #b3261e;
}
.contact-form-status { margin: 0; font-size: 0.9rem; }
.contact-form-status.is-error { color: #b3261e; font-weight: 600; }
.contact-form-status.is-note { color: var(--text-muted); }

/* Same treatment as the delivery result: this replaces the form the moment it
   submits, and appearing instantly made a successful send look like a page jump. */
.contact-form-sent {
  background: var(--green-light);
  border: 1px solid var(--green);
  border-radius: var(--radius);
  padding: 24px 20px;
  animation: result-in var(--dur-slow) var(--ease-out) both;
}
@media (prefers-reduced-motion: reduce) {
  .contact-form-sent { animation: none; }
}
.contact-form-sent h3 { font-size: 1.1rem; color: var(--green-dark); }
.contact-form-sent p { margin: 0; }
.contact-form-sent:focus { outline: 2px solid var(--green-dark); outline-offset: 3px; }

/* Map. Capped well short of the container: a single pin on a single building does
   not need 1140px of basemap. */
.contact-map {
  max-width: 860px;
  margin: 36px auto 0;
  text-align: center;
}
#contact-map-canvas {
  width: 100%;
  height: 300px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  z-index: 1;
}
/* Matches the delivery map: mute the basemap so the green pin is the only strong
   colour on it. */
#contact-map-canvas .leaflet-tile-pane { filter: saturate(0.3) brightness(1.05); }
.contact-map-note {
  margin: 10px 0 0;
  font-size: 0.85rem;
  color: var(--text-muted);
}

@media (min-width: 640px) {
  .contact-field-row { grid-template-columns: 1fr 1fr; }
}

@media (min-width: 860px) {
  /* Details on the left, form on the right — the same order the markup is in, so
     tab order and reading order still agree once the two sit side by side. The
     form takes the wider column because it is the thing with five inputs in it. */
  .contact-grid { grid-template-columns: 0.85fr 1.15fr; gap: 40px; }
  .contact-details { padding: 28px 26px; }
  .contact-panel-heading { font-size: 1.4rem; }
  #contact-map-canvas { height: 360px; }
  .contact-map { margin-top: 48px; }
}

/* ════════════════════════════════════════════════════════════════════════════
   WARM EDITORIAL COMPONENT LAYER — 2026 redesign
   ────────────────────────────────────────────────────────────────────────────
   Appended deliberately. Everything above is the original stylesheet, which
   still owns the functional widgets (delivery map, catalog search, forms) and
   has already been retoned by the token swap at the top of the file. This block
   restyles the *editorial* surfaces — header, hero, sections, cards, quotes,
   footer — and being last it wins the cascade without needing !important.

   It carries its own breakpoints for the same reason: rules earlier in the file
   sit inside @media blocks, so a mobile-first rule added down here would
   otherwise lose to a desktop rule written six hundred lines up.

   Breakpoints: 640 (roomy phone) / 900 (tablet) / 1180 (desktop nav + full grid)
   ════════════════════════════════════════════════════════════════════════════ */

/* ── Header ────────────────────────────────────────────────────────────────
   Sits on the bone ground with no border at rest, so it reads as part of the
   page rather than a bar bolted to the top. The hairline only arrives once
   .is-stuck lands (main.js, sentinel observer) — the header earns its
   separation by scrolling, it doesn't start with it. */
.site-header {
  background: color-mix(in srgb, var(--bone) 88%, transparent);
  border-bottom: 1px solid transparent;
  box-shadow: none;
  transition: border-color var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-out);
}

/* The blur is desktop-only, and that is a hard constraint rather than a taste
   call. A backdrop-filter other than `none` makes the header a containing block
   for its position:fixed descendants, and .site-nav is one of them. Below
   1180px that nav is the off-canvas panel pinned with top:68/bottom:0, so it
   resolved against the 73px header instead of the viewport and opened as a 4px
   sliver — 342px of links scrolled out of reach, the menu effectively dead on
   every phone. At 1180px and up .site-nav is position:static (desktop block
   above), so the header can carry the blur there for free. */
@media (min-width: 1180px) {
  .site-header {
    -webkit-backdrop-filter: saturate(140%) blur(12px);
    backdrop-filter: saturate(140%) blur(12px);
  }
}
.site-header.is-stuck {
  background: color-mix(in srgb, var(--bone) 94%, transparent);
  border-bottom-color: var(--sand-deep);
  box-shadow: 0 1px 24px -12px rgba(20, 24, 26, 0.35);
}
.site-header .container { min-height: var(--header-h); }

/* Nav links: the underline grows from the centre rather than appearing whole.
   Scoped to .site-nav > ul so it never reaches the dropdown menus, which have
   their own row treatment. The extra .site-header ancestor in the next rule
   isn't decorative — it's what out-specifies the global "li > a gets a
   permanent underline" rule (line ~320) so these links sit bare at rest and
   only the ::after bar below marks hover/current, instead of stacking both. */
.site-nav > ul > li > a {
  position: relative;
  font-weight: 500;
  font-size: 0.95rem;
  color: var(--ink);
  letter-spacing: -0.006em;
}
.site-header .site-nav > ul > li > a {
  text-decoration: none;
}
.site-nav > ul > li > a:hover { text-decoration: none; color: var(--green); }
.site-nav > ul > li > a::after {
  content: "";
  position: absolute;
  left: 14px;
  right: 14px;
  bottom: 8px;
  height: 1.5px;
  background: var(--green);
  transform: scaleX(0);
  transform-origin: center;
  transition: transform var(--dur-base) var(--ease-editorial);
}
.site-nav > ul > li > a:hover::after,
.site-nav > ul > li > a:focus-visible::after,
.site-nav > ul > li > a[aria-current="page"]::after { transform: scaleX(1); }
/* Current page is marked in ink weight as well as the rule — a colour-and-a-line
   signal, not a line alone. */
.site-nav > ul > li > a[aria-current="page"] { font-weight: 600; }

.nav-dropdown-toggle { font-weight: 500; font-size: 0.95rem; color: var(--ink); }
.nav-dropdown-menu {
  background: var(--paper);
  border: 1px solid var(--sand-deep);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}

/* ── Hero ──────────────────────────────────────────────────────────────────
   Asymmetric, left-aligned, no gradient. The old hero centred everything on a
   green wash, which is the single most template-looking pattern on the web.
   This one runs the headline wide against a bone ground and lets the photo
   carry the right side. */
.hero {
  position: relative;
  padding: var(--space-2xl) 0 var(--space-xl);
  background: var(--bone);
  text-align: left;
  overflow: clip;
}
/* A very faint sand field behind the top of the page, cut on a diagonal. Pure
   decoration, so it is pointer-transparent and hidden from assistive tech by
   being a pseudo-element with no content. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 62%;
  background: var(--sand);
  clip-path: polygon(0 0, 100% 0, 100% 78%, 0 100%);
  pointer-events: none;
}
.hero > .container { position: relative; }

/* The one kicker left on the site. It survives because it states a fact the
   headline does not — where the company is and how long it has been there —
   rather than naming the section it sits in.

   Set in sentence case at reading size, not tracked-out capitals. Caps and
   letterspacing would make a sentence look like a label again, and this is a
   sentence. It sits on the baseline grid as ordinary type, slightly quieter
   than the body copy below the headline. */
.hero-eyebrow {
  display: block;
  background: none;
  padding: 0;
  border-radius: 0;
  margin-bottom: var(--space-sm);
  font-family: var(--font);
  font-size: 0.95rem;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--green);
}

.hero h1 {
  font-size: var(--step-5);
  letter-spacing: var(--tracking-tight);
  margin-bottom: var(--space-md);
  max-width: 15ch;
  text-wrap: balance;
}
.hero-sub {
  font-size: var(--step-1);
  line-height: 1.5;
  max-width: 46ch;
  margin: 0 0 var(--space-lg);
  color: var(--ink-soft);
}

/* CTA group — label on its own line, buttons full-width on a phone. */
.hero-ctas {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  align-items: stretch;
  max-width: none;
  margin: 0;
}
/* "New customer?" and "Already a customer?" are questions put to the reader,
   not labels on a control. Tracked-out capitals made them read as system
   chrome, which is also why they were easy to skip. Sentence case, at a size
   you would actually read. */
.hero-cta-label {
  font-family: var(--font);
  font-weight: 600;
  font-size: 0.92rem;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-soft);
  margin-bottom: 0;
}
.hero-cta-buttons { flex-direction: column; gap: var(--space-sm); align-items: stretch; }

/* -- The hero's address check (punchlist #2) -------------------------------
   The one interactive thing on the page, so it gets the weight: a full-width
   field on a dark ground, with the submit sitting flush against it as one
   object rather than a button floating beside a box. */
/* z-index 30, and it is the form that carries it rather than the listbox.

   The form is a data-reveal-hero element, so once the entrance has run it is left
   holding transform: matrix(1,0,0,1,0,0) — an identity transform, invisible, but
   still enough to make it a stacking context. That traps the suggestion
   dropdown's own z-index inside the form: no value the listbox can carry lifts it
   past anything outside. And the three blocks the dropdown opens over —
   .hero-secondary, .proof-rail, .hero-existing — are reveal elements too, so they
   are also transformed, also z-index auto, and they come later in the DOM. Later
   wins, and "or browse the catalog" printed straight through the middle of the
   address list.

   Raising the whole form's context is what fixes it; the listbox's 20 then only
   has to beat things inside the form. Below the 50 the sticky header carries, so
   a list opening upward still passes under the header rather than over it. */
.hero-check { margin-top: var(--space-lg); max-width: 30rem; position: relative; z-index: 30; }

.hero-check-label {
  display: block;
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--bone);
  margin-bottom: var(--space-xs);
}
/* Relative so .address-suggest-listbox hangs off the whole row rather than the
   input alone — the list is absolutely positioned, so it leaves the flex layout
   and the input/button pair is unaffected. Spanning the row rather than the field
   means the dropdown's edges line up with the control that produced it. */
.hero-check-row { display: flex; gap: var(--space-xs); position: relative; }
/* Wraps the input by itself so the clear button can hang inside the field. The row
   is the input AND the Check button; an absolutely positioned x anchored to it
   lands over the button instead of over the text it clears. */
.hero-check-field { position: relative; flex: 1; min-width: 0; display: flex; }
.hero-check-row input {
  flex: 1;
  min-width: 0;
  /* Right pad matches .delivery-check-form's: room for the clear button so a long
     address runs under the caret, not under the x. */
  padding: 13px 40px 13px 15px;
  font: inherit;
  font-size: 1rem;   /* 16px exactly: anything smaller makes iOS zoom on focus */
  color: var(--ink);
  background: var(--paper);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
}
.hero-check-row input::placeholder { color: var(--ink-faint); }
.hero-check-row input:focus-visible {
  outline: 2px solid var(--green-on-dark);
  outline-offset: 2px;
}
.hero-check-row .btn { flex: none; }
/* Check gets the same swap as the callback button below it: green at rest, white
   on hover. .btn-on-dark is bone with a --paper wipe, so on hover it went from
   near-white to white and read as inert — and here it also sat as a white block
   flush against a white input, with only the gap between them saying where one
   ended and the other began.

   Scoped to the hero rather than fixed on .btn-on-dark itself. That class is also
   the button on every closing CTA band, where the ground is --green-band rather
   than coal and a bright green fill has a different problem to solve. Worth doing
   there too, but as its own decision, not as a side effect of this one. */
.hero-check-row .btn-on-dark {
  background: var(--green-on-dark);
  color: var(--green-forest);
  border-color: var(--green-on-dark);
}
.hero-check-row .btn-on-dark::before { background: var(--bone); }
.hero-check-row .btn-on-dark:hover,
.hero-check-row .btn-on-dark:focus-visible {
  color: var(--green-forest);
  border-color: var(--bone);
}

/* Starts hidden and empty; hero-check.js unhides it with the answer. The left
   rule carries the verdict, so yes/no is legible before the sentence is read. */
.hero-check-result {
  margin-top: var(--space-md);
  max-width: 34rem;
  padding: var(--space-sm) 0 var(--space-sm) var(--space-md);
  border-left: 3px solid var(--bone-dim);
  font-size: 0.95rem;
  line-height: 1.5;
  color: var(--bone-dim);
}
.hero-check-result strong { color: var(--bone); }
.hero-check-result[data-state="yes"] { border-left-color: var(--green-on-dark); }
.hero-check-result[data-state="close"] { border-left-color: #d9a441; }
.hero-check-result[data-state="no"],
.hero-check-result[data-state="error"] { border-left-color: var(--sand-deep); }
.hero-check-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
  margin-top: var(--space-xs);
}
.hero-check-actions a {
  color: var(--green-on-dark);
  font-weight: 600;
  text-decoration-color: color-mix(in srgb, var(--green-on-dark) 45%, transparent);
}

/* The delivery day, the rep card and the callback form — the ending a "yes" now
   gets here instead of on the next page.

   The markup is js/callback-form.js's, identical to what delivery-area.html
   renders, and the component layer up the file styles it for that page: a bone
   ground, ink labels, a green submit. Inside the hero the ground is coal, so
   this block re-grounds the colours and nothing else. Boxes, spacing, states and
   behaviour stay whatever the shared component says they are, which is the point
   of it being shared. */
.hero-check-day { display: block; margin-top: var(--space-2xs); }
.hero-check-day-note {
  display: block;
  margin-top: 2px;
  font-size: 0.85rem;
  color: var(--bone-dim);
}

.hero-check-result .delivery-rep-card { border-top-color: var(--coal-line); }
.hero-check-result .delivery-rep-card img {
  border-color: color-mix(in srgb, var(--bone) 70%, transparent);
  box-shadow: none;   /* a drop shadow on coal is a smudge, not an edge */
}
.hero-check-result .delivery-rep-card-title { color: var(--green-on-dark); }
.hero-check-result .delivery-rep-card-name { color: var(--bone); }
.hero-check-result .delivery-rep-card-phone a { color: var(--green-on-dark); }

.hero-check-result .delivery-callback-field label { color: var(--bone); }
.hero-check-result .delivery-callback-optional { color: var(--bone-dim); }
/* Matched to the address field a few pixels above rather than to the light
   page's version of itself. Two differently-styled inputs that close together,
   both asking the same reader for something, read as a widget bolted onto the
   hero instead of part of it. */
.hero-check-result .delivery-callback-field input {
  color: var(--ink);
  background: var(--paper);
  border-color: transparent;
}
.hero-check-result .delivery-callback-field input:focus {
  outline: 2px solid var(--green-on-dark);
  outline-offset: 2px;
  border-color: transparent;
  box-shadow: none;   /* the light page's green halo is invisible on coal */
}
/* #b3261e is a red for white paper. On coal it goes muddy and stops reading as
   an error at all, so the invalid state moves to the outline and a lighter red. */
.hero-check-result .delivery-callback-field input[aria-invalid="true"] {
  border-color: transparent;
  outline: 2px solid #ff9f97;
  outline-offset: 2px;
}
/* Green at rest, white on hover. It started the other way round, which gave the
   hover nowhere to go: the wipe underneath every .btn is near-white too, so a bone
   button hovering to bone changed nothing a reader could see.

   --green-on-dark, not --green. The flat green pill measures 2.7:1 against coal,
   under the 3:1 a control's own edge needs — the same measurement that put the
   bone treatment on the header's CTA. This green is 7.2:1 on coal, and the forest
   label reads 5.6:1 on it.

   The label stays forest in both states, so only the fill moves: forest on green,
   then forest on the white the wipe brings in. One less thing changing at once,
   and it keeps the hover legible for the whole 320ms the sweep takes. */
.hero-check-result .delivery-callback-submit {
  background: var(--green-on-dark);
  color: var(--green-forest);
  border-color: var(--green-on-dark);
}
/* The wipe is the hover fill — no background is set on :hover, so under
   prefers-reduced-motion (where .btn::before loses its transition) it still lands
   white, just instantly. */
.hero-check-result .delivery-callback-submit::before { background: var(--bone); }
.hero-check-result .delivery-callback-submit:hover,
.hero-check-result .delivery-callback-submit:focus-visible {
  color: var(--green-forest);
  border-color: var(--bone);
}
.hero-check-result .delivery-callback-status.is-note { color: var(--bone-dim); }
.hero-check-result .delivery-callback-status.is-error { color: #ff9f97; }
.hero-check-result .delivery-callback-status a { color: var(--green-on-dark); }
.hero-check-result .delivery-callback-sent { color: var(--green-on-dark); }

.hero-secondary {
  margin: var(--space-md) 0 0;
  font-size: 0.95rem;
  color: var(--bone-dim);
}
.hero-secondary a { color: var(--green-on-dark); font-weight: 600; }

.hero-existing {
  text-align: left;
  max-width: none;
  margin: var(--space-lg) 0 0;
  padding-top: var(--space-md);
  border-top: 1px solid var(--sand-deep);
}
.hero-existing-label {
  font-family: var(--font);
  font-weight: 600;
  font-size: 0.92rem;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-soft);
}
.hero-existing-links { justify-content: flex-start; gap: 4px 20px; }
.hero-existing-links a { color: var(--green); font-weight: 500; }

/* Hero photo. Squared off to a tall-ish portrait on desktop so it reads as a
   plate on a page rather than a banner, with the sand card sitting behind it
   offset — the one piece of visual "construction" in the layout. */
.hero-media { position: relative; }
.hero-media-photo {
  aspect-ratio: 4 / 3;
  width: 100%;
  object-fit: cover;
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
/* The offset plate. Inset on the near sides only so it peeks out bottom-right. */
.hero-media::after {
  content: "";
  position: absolute;
  inset: 18px -18px -18px 18px;
  z-index: -1;
  border: 1.5px solid var(--green);
  border-radius: var(--radius);
  opacity: 0.5;
  pointer-events: none;
}


/* ── Proof rail ────────────────────────────────────────────────────────────
   The three-figure credibility bar under the hero. Tabular figures so the
   numbers line up as a row rather than drifting, and each stat sits behind a
   hairline that draws itself in. */
.proof-rail {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-md) var(--space-lg);
  margin-top: var(--space-2xl);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--sand-deep);
  text-align: center;
}
/* On a phone this rail is competing for the fold with the mobile action bar
   below it, which takes 59px off the usable viewport. The desktop spacing put
   88px of air above the numbers; this buys most of that back so the figures
   land in the first screen rather than behind the bar. */
@media (max-width: 720px) {
  .proof-rail { margin-top: var(--space-md); padding-top: var(--space-sm); }
}
.proof-item b {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(1.9rem, 1.4rem + 2vw, 2.6rem);
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--green);
  font-variant-numeric: tabular-nums;
}
.proof-item span {
  display: block;
  margin-top: 6px;
  font-size: 0.9rem;
  line-height: 1.35;
  color: var(--ink-soft);
}

/* ── Vendor strip ──────────────────────────────────────────────────────────
   Logos sit at a low opacity and come up to full on hover, so the strip reads
   as texture at a glance and as brands when you look at it. */
.vendor-strip { margin-top: var(--space-2xl); text-align: center; }
/* "Proudly stocking brands like" is a sentence. It was set in tracked capitals
   because it sits above a row of logos and that is what goes above a row of
   logos — which is the definition of a default. */
.vendor-strip-label {
  font-size: 0.92rem;
  letter-spacing: 0;
  text-transform: none;
  font-weight: 400;
  color: var(--ink-faint);
  margin-bottom: var(--space-md);
}
/* Full colour, at rest. These were greyscale at 55% opacity and came up to
   colour on hover — a treatment that only exists for people using a mouse. On
   a phone there is no hover, so every logo in the strip stayed washed out
   permanently, which is most of the traffic seeing the worse version.

   They are also brand marks. Tyson, Hormel and Coca-Cola have specific colours
   for a reason, and desaturating someone else's logo to suit our page is the
   kind of thing their brand guidelines exist to prevent. */
.vendor-logo { opacity: 1; }
/* Feather the ends so logos enter and leave the strip instead of being sliced
   off at a hard edge. */
.vendor-logo-marquee {
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.vendor-strip-note { font-size: 0.9rem; color: var(--ink-faint); }

/* ── Editorial two-column ──────────────────────────────────────────────────
   Sticky intro on the left, content list on the right. Used by "Why choose us"
   and reused on the interior pages. */
.why-us-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-xl); }
.why-us-intro h2 { font-size: var(--step-3); }
.why-us-intro > p { font-size: var(--step-1); color: var(--ink-soft); max-width: 38ch; }

/* -- The team block, merged into the intro column (punchlist #6) -----------
   Two portraits across, not the four the standalone teaser used. This sits in
   the 0.85fr column, where four across render at roughly 90px each — which is
   precisely the failure .team-teaser-grid's own comment records undoing when
   the section was last touched. The selector is (0,2,0) so it beats the bare
   .team-teaser-grid rules wherever they appear, media queries included. */
.why-us-faces { margin-top: var(--space-lg); }
.why-us-faces .team-teaser-grid {
  grid-template-columns: repeat(2, 1fr);
  max-width: 300px;
  margin: 0;
}
.why-us-faces-note {
  font-size: 0.9rem;
  color: var(--ink-soft);
  margin: var(--space-md) 0;
  max-width: 36ch;
}

.why-us-list { display: flex; flex-direction: column; }
/* One vertical rule visible at a time, but not always the same one. At rest the
   quote carries its own green spine and the item's bar sits at --green-light,
   faint enough on this ground to read as an edge rather than a rule. Hovering
   hands the rule over: the item's bar comes up to full --green and the quote's
   spine goes transparent. Two rules of the same green running parallel down one
   block is what read as a doubled border, so they are never both up at once.

   The handover is staggered rather than crossfaded, in both directions: whichever
   rule is leaving goes in --dur-fast with no delay, and the one arriving waits
   --dur-fast before starting. A straight crossfade would have put both of them on
   screen at half strength for the length of the transition — briefly the exact
   doubling this is here to avoid. The timings live in the base rules (the leaving
   state) and the :hover rules (the arriving state), so the stagger holds on the
   way out as well as the way in, and both tokens collapse to 1ms under
   prefers-reduced-motion, which turns the whole thing into a straight swap.

   Colour, not width: the spine keeps its 2px and goes transparent, so nothing
   reflows on hover. */
.why-us-item {
  padding: var(--space-lg) 0 var(--space-lg) var(--space-md);
  border-top: 1px solid var(--sand-deep);
  border-left: 3px solid var(--green-light);
  background: none;
  border-radius: 0;
  box-shadow: none;
  transition: border-left-color var(--dur-fast) var(--ease-standard);
}
/* :focus-within alongside :hover so tabbing into the "Meet the team" link inside
   an item gets the same handover a pointer does. */
.why-us-item:hover,
.why-us-item:focus-within {
  border-left-color: var(--green);
  transition: border-left-color var(--dur-base) var(--ease-standard) var(--dur-fast);
}
.why-us-item:hover .why-us-quote,
.why-us-item:focus-within .why-us-quote {
  border-left-color: transparent;
  transition: border-left-color var(--dur-fast) var(--ease-standard);
}
.why-us-item:first-child { border-top: 0; padding-top: 0; }
.why-us-item h3 { font-size: var(--step-1); margin-bottom: 8px; color: var(--ink); }
.why-us-item p { margin-bottom: 0; }

/* Customer words sitting under the claim they back up. A pull-quote with a
   green spine at rest — see the handover note on .why-us-item above: the spine
   yields to the item's bar on hover so the two are never up together. */
/* Upright, not italic. Only the three upright weights of the display face are
   loaded, so `font-style: italic` was not producing an italic at all — the
   browser was mechanically slanting the upright face, which gives you the
   distorted stems and unchanged letterfits of a fake oblique. Confirmed by
   measurement: the italic and upright strings set to exactly the same width.

   Loading the real italic would fix it at the cost of two more font files, but
   these quotes do not need it. The green spine, the indent and the cite line
   already say "this is somebody else talking", and a slab serif set upright is
   the sturdier reading of the two. */
.why-us-quote {
  margin: var(--space-md) 0 0;
  padding: 0 0 0 var(--space-md);
  border-left: 2px solid var(--green);
  /* The delay is the arriving half of the handover described on .why-us-item. */
  transition: border-left-color var(--dur-base) var(--ease-standard) var(--dur-fast);
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-style: normal;
  line-height: 1.5;
  color: var(--ink);
  background: none;
  border-radius: 0;
}
.why-us-quote cite {
  display: block;
  margin-top: 8px;
  font-family: var(--font);
  font-style: normal;
  font-size: 0.82rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--ink-faint);
}

/* The inline "→" link used across cards and rows. The arrow travels on hover;
   the label does not, so the text stays readable mid-transition. */
.card-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: var(--space-sm);
  font-weight: 600;
  font-size: 0.92rem;
  color: var(--green);
  text-decoration: none;
  padding: 6px 0;
}
/* No appended "→". The arrow was decoration standing in for affordance: these
   are links, they are green, they underline on hover, and the arrow told the
   reader nothing the link did not. It also gave every one of them the same
   sliding hover, which is the sort of uniform flourish that reads as generated.
   The underline grows instead — a smaller gesture, and one that belongs to
   links specifically rather than to everything on the page. */
.card-link {
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 0.22em;
  text-decoration-thickness: 1.5px;
  transition: text-decoration-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.card-link:hover,
.card-link:focus-visible { text-decoration: underline; text-decoration-color: currentColor; color: var(--green-deep); }

/* ── Step list ─────────────────────────────────────────────────────────────
   A process hung off a timeline: the rule runs down the far left on its own
   and each step reaches out to its icon with a short arm. Nothing is drawn
   over anything, which is what makes the icons read as steps hanging off a
   line rather than beads threaded on it.

   This replaced outlined display numerals in a 72px rail. They were the
   loudest thing in the section — 50px of serif numbering headings set at 22 —
   and they were doing a job the reading order already does. The sequence now
   lives in the markup: these lists are <ol>, so a screen reader is told there
   are four steps instead of reading four paragraphs that begin "01".

   Each page carries its own inline <symbol> sprite for the icons it uses.
   External sprite files would be tidier, but <use href="file.svg#id"> still
   does not resolve in Safari, so it would silently render nothing there. */
.steps-list { list-style: none; margin: 0; padding: 0; }
.step-row {
  position: relative;
  display: grid;
  grid-template-columns: 48px 1fr;
  column-gap: var(--space-md);
  padding: 0 0 var(--space-xl) 28px;
}
.step-row:last-child { padding-bottom: 0; }
/* Drawn per row rather than once down the list, so the run ends at the last
   step's arm of its own accord instead of trailing past it into the copy
   underneath. A timeline that trails off reads as an unfinished list. */
.step-row::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--sand-deep);
}
.step-row:last-child::before { bottom: auto; height: 24px; }
/* The arm runs the full width of the indent, so it stops exactly where the
   icon's box starts. A shorter stub pointing into the gap reads as a tick on
   a ruler rather than as a connection to anything. */
.step-row::after {
  content: "";
  position: absolute;
  left: 0;
  top: 24px;
  width: 28px;
  height: 1px;
  background: var(--sand-deep);
}

/* The spine draws itself as each step arrives, instead of standing fully painted
   before anyone has read step one. This section's argument is sequence — four
   steps, about a week — so a timeline that assembles in order is the one piece of
   motion on the page that describes its own content rather than decorating it.
   It is also why the rows carry data-reveal="fade" and not the default rise: with
   the row travelling 22px as well, the draw and the lift compete and neither
   reads. The rows hold still, and the line is the only thing moving.

   Hidden state keyed to .reveal-pending, drawn state to .is-visible. That is the
   contract js/main.js sets up: .reveal-pending is only ever applied when the
   reveal is actually going to run, so a visitor with no JS, no
   IntersectionObserver, or reduced motion requested gets a spine that was simply
   always there rather than one stuck at scale 0. The two selectors have equal
   specificity, so .is-visible wins on source order while both are present, and
   still holds once .reveal-pending comes off at transitionend.

   transform-origin and the transition sit on the base rule, not on
   .reveal-pending: that class is dropped the moment the row's own fade ends, and
   a transition declared only there would disappear from under a draw still in
   flight, snapping the rest of the line into place. */
.step-row::before,
.step-row::after {
  transform-origin: left top;
  transition: transform var(--dur-reveal) var(--ease-editorial) var(--reveal-delay, 0ms);
}
.step-row.reveal-pending::before { transform: scaleY(0); }
.step-row.is-visible::before { transform: scaleY(1); }
/* The arm waits for the run to reach it — 180ms past the row's own slot in the
   stagger, so the vertical is already travelling when its branch starts. Drawn on
   the same clock the two read as a single L being stamped, which loses the sense
   of a line running down the page and putting out arms as it passes. */
.step-row.reveal-pending::after,
.step-row.is-visible::after { transition-delay: calc(var(--reveal-delay, 0ms) + 180ms); }
.step-row.reveal-pending::after { transform: scaleX(0); }
.step-row.is-visible::after { transform: scaleX(1); }
/* A span, not the <svg> itself: width and height do not apply to a
   non-replaced inline box, and the svg inside is sized off this one. */
.step-icon {
  display: block;
  width: 48px;
  height: 48px;
  color: var(--green);
}
/* Line art at 1.5 to sit with the hairline rules already on the page — a set
   of solid green shapes at this size would carry more weight than the
   headings they belong to. Set here rather than on the symbols so the whole
   set stays in one hand.

   NB the sibling comment further up the file: the old flex-and-numeral rules
   this replaces have been deleted rather than overridden. */
.step-icon svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.step-copy h3 { font-size: var(--step-1); margin-bottom: 6px; }
/* The list itself is full width, so the measure has to be held here. The old
   version capped the whole component at 720px and centred it; that centred a
   left-aligned section head over an off-centre list on the wider pages. */
.step-copy p { margin-bottom: 0; max-width: 58ch; }
.step-copy a.card-link { display: inline-block; margin-top: var(--space-xs); font-weight: 600; }

/* ── Category cards ────────────────────────────────────────────────────────
   The "what we carry" teaser. Photo-led, with the label riding on a gradient
   scrim over the image so it stays legible whatever the photo does. */
.cat-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-md);
}
.cat-card {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--green-forest);
  color: var(--bone);
  text-decoration: none;
  /* Square, not 4:3. At four across these were 260x195 — the smallest images
     on the page, and visibly shorter than the 260x260 team portraits directly
     below them, which made the two grids look like a mistake rather than a
     rhythm. Square gives the photographs a third more height and lines the two
     rows up.

     The source files are 4:3, so `object-fit: cover` on the <img> below trims
     the left and right edges. These are photographs of cases and shelves with
     no single subject, so losing a little from each side costs nothing — which
     would not be true of the team portraits. */
  aspect-ratio: 1;
  isolation: isolate;
}
.cat-card:hover { text-decoration: none; }
.cat-card img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Scale on hover, never width/height — this is the one card on the site that
     moves, and it has to stay on the compositor. transform only: a filter
     transition here would drop the photo to main-thread paint on some browsers. */
  transition: transform var(--dur-slow) var(--ease-editorial);
}
.cat-card:hover img,
.cat-card:focus-visible img { transform: scale(1.05); }
/* The scrim. Measured against the darkest and lightest photos in the set, not
   guessed: at 82% forest over the bottom third, bone label text clears 7:1 on
   both. */
.cat-card::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
              rgba(3, 48, 31, 0) 35%,
              rgba(3, 48, 31, 0.55) 62%,
              rgba(3, 48, 31, 0.88) 100%);
  transition: opacity var(--dur-slow) var(--ease-out);
}
.cat-card-body {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 1;
  padding: var(--space-md);
}
.cat-card-title {
  display: block;
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--bone);
}
.cat-card-meta {
  display: block;
  margin-top: 4px;
  font-size: 0.85rem;
  color: color-mix(in srgb, var(--bone) 76%, var(--green-forest));
}
/* Placeholder state — a category whose photo hasn't been dropped in yet still
   has to look deliberate rather than broken. */
.cat-card.is-placeholder { background: var(--green-deep); }
.cat-card.is-placeholder::after { background: linear-gradient(180deg, rgba(3,48,31,0.15), rgba(3,48,31,0.8)); }

/* ── Team teaser ───────────────────────────────────────────────────────────
   Portraits go duotone at rest and resolve to full colour on hover, which
   evens out twenty headshots shot in different light and different years —
   the single cheapest thing that makes a mixed photo set look art-directed. */
.team-teaser-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-md);
  /* Both of these undo the previous design rather than express this one. The
     original teaser was four 84px circular avatars centred under a heading, so
     it carried `max-width: 420px; margin: 0 auto`. Left standing, that cap
     squeezed the whole four-column grid into 420px and rendered each portrait
     at 90px — a quarter of the intended size, on the section whose argument is
     that you can see who you'll be dealing with. */
  max-width: none;
  margin: 0;
}
.team-teaser-card {
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  padding: 0;
  text-align: left;
}
/* Full colour. The duotone-at-rest treatment that used to be here was solving
   the wrong problem: it evened out a photo set shot years apart, but it did it
   by making everyone look washed out until a mouse touched them, and on a
   phone that "until" never arrives.

   These are photographs of the people a customer will actually speak to. That
   is the entire argument of the section, and it is not helped by rendering
   them as artefacts. If the set still looks uneven, the fix is re-shooting the
   odd ones, not draining the colour from all of them. */
.team-teaser-card picture,
.team-teaser-card img {
  width: 100%;
  /* height:auto is load-bearing, not tidying. The original stylesheet sizes
     these as small circular avatars with an explicit `height`, and an explicit
     height beats `aspect-ratio` — so without this the portrait rendered as a
     full-width box only 84px tall, and `object-fit: cover` sliced the top and
     bottom off every face to fill it. */
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--radius);
}
.team-teaser-card .name {
  margin-top: var(--space-sm);
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--ink);
}
.team-teaser-card .title {
  font-size: 0.85rem;
  color: var(--ink-faint);
  letter-spacing: 0.01em;
}

/* ── Testimonials ──────────────────────────────────────────────────────────
   Pull quotes on sand cards with an oversized serif quote mark. */
.quote-card {
  background: var(--paper);
  border: 1px solid var(--sand-deep);
  border-radius: var(--radius);
  box-shadow: none;
  padding: var(--space-lg) var(--space-md) var(--space-md);
  transition: border-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
.quote-card:hover { border-color: var(--border-strong); box-shadow: var(--shadow); }
.quote-mark {
  font-family: var(--font-display);
  font-size: 3.2rem;
  line-height: 0.6;
  color: var(--green);
  opacity: 0.28;
  display: block;
  margin-bottom: 6px;
}
/* Upright for the same reason as .why-us-quote above — no italic face is
   loaded, so this was a synthesised oblique. The quote mark carries it. */
.quote-card p {
  font-family: var(--font-display);
  font-size: 1.05rem;
  line-height: 1.5;
  font-style: normal;
  color: var(--ink);
}
.quote-name { font-weight: 600; color: var(--ink); font-size: 0.92rem; }
.quote-role { color: var(--ink-faint); font-size: 0.85rem; }
.quote-arrow {
  background: var(--paper);
  border: 1px solid var(--border-strong);
  color: var(--ink);
  border-radius: var(--radius-pill);
  transition: background var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out), opacity var(--dur-base) var(--ease-out);
}
.quote-arrow:hover:not(:disabled) { background: var(--green); border-color: var(--green); color: var(--paper); }
.quote-arrow:disabled { opacity: 0.35; }

/* ── FAQ ───────────────────────────────────────────────────────────────────
   Hairline rows, no cards. The chevron rotates; the answer is revealed by the
   browser's own <details> toggle, so it works with JS off. */
/* margin-inline resets the `margin: 0 auto` the earlier layer set. That centred the
   column under a left-aligned heading and paragraph, so the block started an inch
   right of the words introducing it. The measure cap stays; only the centring goes. */
.faq-list { border-top: 1px solid var(--sand-deep); max-width: 78ch; margin-inline: 0; }
.faq-item {
  border: 0;
  border-bottom: 1px solid var(--sand-deep);
  border-radius: 0;
  background: none;
  box-shadow: none;
}
.faq-item summary {
  padding: var(--space-md) 40px var(--space-md) 0;
  font-family: var(--font);
  font-weight: 600;
  font-size: 1.02rem;
  letter-spacing: -0.01em;
  color: var(--ink);
  min-height: 48px;
  transition: color var(--dur-base) var(--ease-out);
}
.faq-item summary:hover { color: var(--green); }
/* Both open-state rules exclude .is-closing. js/main.js animates the panel shut
   before it drops the [open] attribute — the panel needs a measured height to
   collapse from, and removing [open] first would take that away — so [open] is
   still set for the whole 320ms of the close. Keyed to [open] alone, the chevron
   would sit rotated through the entire collapse and then flip back after the
   panel had already gone, which is the same out-of-step feel the animation was
   added to fix, only reversed. .is-closing goes on the moment the close starts,
   so the arrow turns with the panel. */
.faq-item[open]:not(.is-closing) summary { color: var(--green); }
.faq-icon { transition: transform var(--dur-slow) var(--ease-editorial); }
.faq-item[open]:not(.is-closing) .faq-icon { transform: rotate(180deg); }
.faq-answer { padding: 0 0 var(--space-md); }
.faq-answer p { margin-bottom: 0; max-width: 68ch; }
/* Same two overrides as the list above it, for the same reason — it is the last
   line of that block, not a caption centred under it. */
.faq-note { margin-top: var(--space-lg); margin-inline: 0; text-align: left; color: var(--ink-faint); font-size: 0.95rem; }

/* ── Closing CTA band ──────────────────────────────────────────────────────
   The forest anchor that ends most pages. */
.cta-band {
  background: var(--green-band);
  color: var(--bone);
  padding: var(--space-3xl) 0;
  border: 0;
  position: relative;
  overflow: clip;
}
/* A single oversized outlined glyph bleeding off the right edge — the page's
   one moment of graphic confidence. Decorative, so it is hidden from AT and
   sits under the content. */
.cta-band::before {
  content: "M";
  position: absolute;
  right: -0.12em;
  bottom: -0.34em;
  font-family: var(--font-display);
  font-size: clamp(14rem, 32vw, 26rem);
  font-weight: 600;
  line-height: 1;
  color: transparent;
  -webkit-text-stroke: 1.5px color-mix(in srgb, var(--bone) 16%, transparent);
  pointer-events: none;
  user-select: none;
}
.cta-band > .container { position: relative; }
.cta-band h2 { font-size: var(--step-4); max-width: 18ch; color: var(--bone); }
.cta-band p { font-size: var(--step-1); max-width: 52ch; color: color-mix(in srgb, var(--bone) 80%, var(--green-band)); }
.cta-band-actions { display: flex; flex-direction: column; gap: var(--space-sm); margin-top: var(--space-lg); align-items: stretch; }

/* ── Page hero (interior pages) ────────────────────────────────────────────
   One shared masthead for every page that isn't the homepage. */
.page-hero,
.doc-hero {
  background: var(--sand);
  border-bottom: 1px solid var(--sand-deep);
  padding: var(--space-2xl) 0 var(--space-xl);
  text-align: left;
}
.page-hero h1,
.doc-hero h1 {
  font-size: var(--step-4);
  max-width: 18ch;
  margin-bottom: var(--space-sm);
}
.page-hero-sub,
.doc-hero p {
  font-size: var(--step-1);
  line-height: 1.5;
  max-width: 56ch;
  color: var(--ink-soft);
  margin-bottom: 0;
}
.page-hero-actions { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-top: var(--space-lg); }

/* Breadcrumb. Sits above the h1 on interior pages — three levels of hierarchy
   is where WCAG-adjacent guidance starts asking for one, and it also gives the
   masthead something to sit against. */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0 0 var(--space-md);
  padding: 0;
  list-style: none;
  font-size: 0.85rem;
  color: var(--ink-faint);
}
.crumbs li { display: flex; align-items: center; gap: 8px; }
.crumbs li + li::before { content: "/"; color: var(--border-strong); }
/* inline-flex with a min-height, not vertical padding: on an inline box the
   padding paints outside the line box without growing the hit target, so the
   link measured 14px tall and failed the WCAG 2.2 24px minimum. */
.crumbs a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  color: var(--ink-faint);
}
.crumbs a:hover { color: var(--green); }
.crumbs [aria-current="page"] { color: var(--ink); font-weight: 500; }

/* ── Footer ────────────────────────────────────────────────────────────────
   Forest ground, editorial columns, generous rules. */
.site-footer {
  background: var(--green-band);
  color: color-mix(in srgb, var(--bone) 80%, var(--green-band));
  padding: var(--space-2xl) 0 var(--space-lg);
}
.site-footer a { color: color-mix(in srgb, var(--bone) 88%, var(--green-band)); }
.site-footer a:hover { color: var(--bone); }
.footer-logo {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--bone);
  margin-bottom: var(--space-sm);
}
.footer-brand p { color: color-mix(in srgb, var(--bone) 72%, var(--green-band)); max-width: 40ch; }
.footer-col h4 {
  font-family: var(--font);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: color-mix(in srgb, var(--bone) 68%, var(--green-band));
  margin-bottom: var(--space-sm);
}
.footer-bottom {
  margin-top: var(--space-xl);
  padding-top: var(--space-md);
  border-top: 1px solid color-mix(in srgb, var(--bone) 18%, transparent);
  color: color-mix(in srgb, var(--bone) 66%, var(--green-band));
  font-size: 0.85rem;
}

/* ── Prose ─────────────────────────────────────────────────────────────────
   Long-form body copy on about / menu-development / what-we-carry. */
.prose { max-width: var(--measure); }
.prose > * + * { margin-top: 1.1em; }
.prose p { font-size: var(--step-0); line-height: 1.7; }
.prose h2 { margin-top: 1.6em; }
.prose h3 { margin-top: 1.4em; }
.prose ul, .prose ol { padding-left: 1.2em; color: var(--ink-soft); }
.prose li { margin-bottom: 0.4em; }
/* Opening paragraph of a long page gets set larger — a standing editorial
   convention that costs one class and instantly signals "considered". */
.prose-lead { font-size: var(--step-1); line-height: 1.55; color: var(--ink); }

/* Figure + caption, used wherever a photo sits inside prose. */
.figure { margin: var(--space-xl) 0; }
.figure img { width: 100%; border-radius: var(--radius); box-shadow: var(--shadow); }
.figure figcaption {
  margin-top: var(--space-sm);
  font-size: 0.85rem;
  color: var(--ink-faint);
  max-width: 56ch;
}

/* A .badge component was defined here and never placed on a single page —
   written because a design system "should have" one. Removed rather than left
   waiting for a use that had not turned up in the design. */

/* ═══ 640px — roomy phone ═══════════════════════════════════════════════ */
@media (min-width: 640px) {
  .hero-cta-buttons { flex-direction: row; align-items: center; }
  .hero-ctas { flex-direction: row; align-items: center; gap: var(--space-md); flex-wrap: wrap; }
  .hero-cta-label { margin-bottom: 0; }
  .cta-band-actions { flex-direction: row; align-items: center; flex-wrap: wrap; }
  .proof-rail { grid-template-columns: repeat(3, 1fr); }
  .cat-grid { grid-template-columns: repeat(2, 1fr); }
}

/* ═══ 900px — tablet ════════════════════════════════════════════════════ */
@media (min-width: 900px) {
  .hero { padding: var(--space-3xl) 0 var(--space-2xl); }
  .hero-media-photo { aspect-ratio: 5 / 4; }

  .why-us-grid { grid-template-columns: 0.85fr 1.15fr; gap: clamp(40px, 6vw, 88px); }
  /* The intro used to pin here while its list scrolled past. That was written
     for a two-element column — a heading and a paragraph — and the merge
     (punchlist #6) gave it the four team portraits as well.

     Measured after the merge: the column is 913px tall at this width. Pinned in
     a 900px viewport it exceeds the screen, and a sticky block taller than the
     viewport never scrolls its own tail into view — the "Meet the full team"
     button at its foot becomes unreachable. Gating on min-height doesn't save
     it either: the bar would have to sit near 1100px, which is taller than most
     laptops, so the effect would almost never engage.

     It also no longer buys much. The list beside it is ~1100px, so pinning a
     913px column wins about 200px of travel. Removed rather than tuned. */

  .team-teaser-grid { grid-template-columns: repeat(4, 1fr); }
  .cat-grid { grid-template-columns: repeat(4, 1fr); }
  /* The indent grows with the room for it: 44px of margin before the icon
     instead of 28, and the arm lengthens to match so it still lands on the
     icon's edge. */
  .steps-list { max-width: 860px; }
  .step-row { grid-template-columns: 56px 1fr; column-gap: var(--space-lg); padding-left: 44px; }
  .step-row:last-child::before { height: 28px; }
  .step-row::after { top: 28px; width: 44px; }
  .step-icon { width: 56px; height: 56px; }
  .page-hero, .doc-hero { padding: var(--space-3xl) 0 var(--space-2xl); }
}

/* Single column until 1100. This has to be stated rather than assumed: the
   original stylesheet splits the hero at 860px, and a media query adds no
   specificity, so without an explicit reset here that older rule would still
   put the photo alongside the headline between 860 and 1100 — which is the
   exact range where the headline strands its last word. */
.hero-split .hero-grid { grid-template-columns: 1fr; }

/* ═══ 1100px — the hero splits ══════════════════════════════════════════
   The photo used to move alongside the headline at 900px. That halved the
   headline's measure — 736px to 411px — while the display size stayed at its
   64px ceiling, so between roughly 800 and 1050 the last word of the headline
   dropped onto a line of its own.

   Measured across 320-1600px rather than fixed at one width: splitting here
   instead, with the existing 13-15ch cap on the h1, wraps cleanly at every
   step of that sweep. Below this the hero is a single column and the photo
   sits under the copy, which is the right call on a tablet anyway. */
@media (min-width: 1100px) {
  .hero-split .hero-grid {
    grid-template-columns: 1.05fr 0.95fr;
    gap: clamp(40px, 6vw, 80px);
    align-items: center;
  }

  /* "If you're on the map" — copy left, map right, on the same terms as the hero
     above it. Stacked, the two halves of one answer sat a full screen apart: the
     question in the heading scrolled off before the map that answers it arrived.
     Splits at 1100 rather than 900 because the map holds place names — Duluth,
     Hibbing, Bemidji — and a 900px viewport leaves the column near 400px, where
     they go the same soft the mobile crop was cut to avoid. Here it lands at ~540. */
  .delivery-split > .container {
    display: grid;
    grid-template-columns: 1fr 1.1fr;
    gap: clamp(40px, 6vw, 80px);
    align-items: center;
  }
  /* The stacked layout used this to hold the map off the copy. Side by side the
     gap does that job, and the margin would only push the left column up. */
  .delivery-split .section-head { margin-bottom: 0; }
}

/* ═══ 1180px — desktop ══════════════════════════════════════════════════ */
/* This block held `.hero h1 { max-width: 13ch }` — the rule that put the check
   field under the fold on a 1366x768 laptop. Removed rather than overridden:
   .hero is used by exactly one page (index.html, always .hero-dark), so the cap
   governed a single element, and leaving a contradicting one 1200 lines above
   its replacement is a trap for whoever reads it next. Section 6 at the foot of
   this file carries the measurements and the explicit cap that replaces it. */



/* ════════════════════════════════════════════════════════════════════════════
   WARM EDITORIAL — interior pages
   Continues the component layer above: about / team / testimonials /
   what-we-carry / catalog / credit / delivery / contact.
   ════════════════════════════════════════════════════════════════════════════ */

/* ── Alternating photo + text ──────────────────────────────────────────────
   about.html and menu-development.html. Photo first in the DOM on every row;
   the reversed variant swaps the columns visually at desktop only, so reading
   order and tab order still follow the markup on a phone. */
.about-history-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-lg);
  align-items: center;
}
.about-history-text h2 { font-size: var(--step-3); margin-bottom: var(--space-sm); }
.about-history-text p { max-width: 56ch; }
.about-history-text > p:last-of-type { margin-bottom: 0; }
.about-photo img {
  width: 100%;
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  /* One ratio for every photo on the page regardless of what came out of the
     camera, so the alternating rows keep a rhythm instead of jumping height. */
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

/* menu-development.html's single photo is a portrait poster (a customer's
   menu design) rather than a landscape photo. There's no second row on this
   page to keep rhythm with, so it displays at its own ratio instead of the
   4:3 crop above, which was cutting the poster off top and bottom. */
.menu-dev-photo img {
  aspect-ratio: 640 / 829;
  object-fit: contain;
}

/* ── Team ──────────────────────────────────────────────────────────────────
   Same duotone-at-rest treatment as the homepage teaser, applied to the full
   roster where the mixed photo set is most obvious. */
.team-group { margin-bottom: var(--space-2xl); }
.team-group-heading {
  font-family: var(--font);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--ink-faint);
  padding-bottom: var(--space-sm);
  margin-bottom: var(--space-lg);
  border-bottom: 1px solid var(--sand-deep);
}
.team-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-md);
}
.team-card {
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  padding: 0;
  text-align: left;
}
/* Full colour, same reasoning as the homepage teaser above. `height: auto` for
   the same reason too — the original rules pin these to 82/104/112px squares
   across three breakpoints, and any of those heights left standing squashes
   the portrait into a letterbox that crops the face. */
.team-card img {
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--radius);
}
.team-card .name {
  margin-top: var(--space-sm);
  font-family: var(--font);
  font-size: 0.98rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.team-card .role { font-size: 0.85rem; color: var(--ink-faint); }
.team-card .team-phone { font-size: 0.85rem; }
.team-finder-link { margin-top: var(--space-md); font-size: 0.95rem; }

/* ── Testimonials page ─────────────────────────────────────────────────────
   Masonry-ish columns rather than a rigid grid: the quotes vary from one line
   to six, and a grid forces every card to the tallest in its row. */
.testimonial-grid {
  columns: 1;
  column-gap: var(--space-md);
}
.testimonial-grid > * {
  break-inside: avoid;
  margin-bottom: var(--space-md);
}

/* ── What we carry ─────────────────────────────────────────────────────────
   Brand logos linking into pre-filled catalog searches, each with the brand name
   in real text underneath. That text is what the page ranks on and what a screen
   reader announces, so the logos are alt="" — a filled alt would read every brand
   twice. Brands with no usable logo are the same tile minus the image. */
.wwc-section {
  padding: var(--space-xl) 0;
  border-top: 1px solid var(--sand-deep);
}
.wwc-section:first-child { border-top: 0; padding-top: 0; }
.wwc-section h2 { font-size: var(--step-2); margin-bottom: var(--space-xs); }
.wwc-section > p { max-width: 60ch; }
.wwc-logos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--space-xs);
  list-style: none;
  padding: 0;
  margin: var(--space-md) 0 0;
}
.wwc-logos a {
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 10px;
  min-height: 104px;
  padding: 16px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--paper);
  text-align: center;
  text-decoration: none;
  transition: border-color var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out);
}
/* The global `li > a:not(.btn):not(.card-link)` underline rule outranks a bare
   .wwc-logos a, and a tile is a box rather than a run of prose. Match that
   selector's specificity to switch the underline back off. */
.wwc-logos li > a:not(.btn):not(.card-link) { text-decoration: none; }
/* Logos arrive at wildly different aspect ratios. A fixed 44px band with
   object-fit lets each sit as large as it can inside the same space, so the names
   below land on a common line. The width/height attributes on the img are there
   only to reserve the box and stop the grid reflowing as images load. A tile with
   no logo has no band at all, so its name centres on its own. */
.wwc-logos a > picture { display: block; width: 100%; }
.wwc-logo {
  display: block;
  width: 100%;
  height: 44px;
  object-fit: contain;
}
.wwc-logo-name {
  color: var(--ink);
  font-size: 0.85rem;
  font-weight: 600;
  line-height: 1.3;
}
.wwc-logos a:hover,
.wwc-logos a:focus-visible {
  border-color: var(--green);
  box-shadow: 0 3px 12px rgb(0 0 0 / 0.08);
  transform: translateY(-2px);
}
@media (prefers-reduced-motion: reduce) {
  .wwc-logos a:hover,
  .wwc-logos a:focus-visible { transform: none; }
}
.wwc-see-all { display: inline-block; margin-top: var(--space-md); font-weight: 600; font-size: 0.92rem; }
.wwc-footnote { margin-top: var(--space-xl); color: var(--ink-faint); font-size: 0.95rem; }

/* ── Document cards (catalog PDF, credit application) ──────────────────── */
.doc-card {
  background: var(--paper);
  border: 1px solid var(--sand-deep);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: var(--space-lg) var(--space-md);
  margin-top: var(--space-lg);
  max-width: 560px;
}
.doc-card .filename {
  font-family: var(--font);
  font-weight: 600;
  font-size: 1.05rem;
  color: var(--ink);
  letter-spacing: -0.01em;
}
.doc-card .meta { font-size: 0.88rem; color: var(--ink-faint); margin: 4px 0 var(--space-md); }
.doc-icon { color: var(--green); margin-bottom: var(--space-md); }
.doc-icon svg { width: 32px; height: 32px; }
.doc-note, .doc-alt { max-width: 60ch; color: var(--ink-soft); font-size: 0.95rem; }
.doc-download { padding-top: 0; }
.doc-preview-inline {
  width: 100%;
  height: 420px;
  border: 1px solid var(--sand-deep);
  border-radius: var(--radius-sm);
  margin-bottom: var(--space-md);
  background: var(--sand);
}

/* ── Catalog search ────────────────────────────────────────────────────────
   Retoned, not restructured — the behaviour here is load-bearing. */
.catalog-search-sticky { background: var(--bone); border-bottom: 1px solid var(--sand-deep); }
/* A nine-word form label. Capitals are for short field names, not sentences —
   at this length they stop being a label and start being shouting. */
.catalog-field label {
  font-family: var(--font);
  font-size: 0.92rem;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink-soft);
}
.catalog-results { border-color: var(--sand-deep); }
.catalog-result-card, .catalog-results > li { border-color: var(--sand-deep); }
.catalog-results-header {
  background: var(--sand);
  color: var(--ink-faint);
  font-size: 0.78rem;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  font-weight: 600;
}
/* Item numbers, pack sizes and prices are columns of digits — tabular figures
   stop them shimmying as results re-render. */
.catalog-result-number, .catalog-result-card, .catalog-results li { font-variant-numeric: tabular-nums; }

/* ── Inputs ────────────────────────────────────────────────────────────────
   One treatment for every text input on the site. 16px minimum so iOS does not
   zoom the viewport on focus, and 48px tall so the field is its own touch
   target. */
.contact-field input,
.contact-field textarea,
#delivery-address,
#catalog-search-input {
  font-family: var(--font);
  font-size: 1rem;
  min-height: 48px;
  padding: 13px 15px;
  background: var(--paper);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--ink);
  transition: border-color var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out);
}
/* The two fields that carry a clear button need the room for it. Without this the
   text runs under the x, which was true on both before the target around it grew.
   The contact fields in the rule above have no button, so they keep even padding.
   40px is what the hero field uses. */
#delivery-address,
#catalog-search-input { padding-right: 40px; }

.contact-field input::placeholder,
.contact-field textarea::placeholder,
#delivery-address::placeholder,
#catalog-search-input::placeholder { color: var(--ink-faint); }
.contact-field input:focus,
.contact-field textarea:focus,
#delivery-address:focus,
#catalog-search-input:focus {
  border-color: var(--green);
  box-shadow: 0 0 0 3px var(--green-wash);
  outline: none;
}
.contact-field label {
  font-family: var(--font);
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -0.005em;
}

/* ── Delivery checker ──────────────────────────────────────────────────────
   The single most valuable widget on the site. Given a paper card and a green
   spine so it reads as the page's primary object rather than one panel of two. */
.delivery-check {
  background: var(--paper);
  border: 1px solid var(--sand-deep);
  border-left: 3px solid var(--green);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--space-lg) var(--space-md);
}
.delivery-check-heading { font-size: var(--step-2); margin-bottom: var(--space-md); }
.delivery-check-note { font-size: 0.9rem; color: var(--ink-faint); }
#delivery-map-canvas, #contact-map-canvas {
  border-radius: var(--radius);
  border: 1px solid var(--sand-deep);
  overflow: hidden;
}
.delivery-map-note { font-size: 0.88rem; color: var(--ink-faint); }
.town-list-groups h3 {
  font-family: var(--font);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* ── Contact ───────────────────────────────────────────────────────────── */
.contact-details {
  background: var(--sand);
  border: 1px solid var(--sand-deep);
  border-radius: var(--radius);
  box-shadow: none;
}
.contact-panel-heading { font-size: var(--step-2); }
.contact-detail-label {
  display: block;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 2px;
}
.contact-detail-strong { font-weight: 600; color: var(--ink); }
.contact-detail-note { font-size: 0.85rem; color: var(--ink-faint); }
.contact-detail-list svg { color: var(--green); }

/* ── Weekly flyer tab ──────────────────────────────────────────────────── */
/* Coal, matching the header — not --green-deep, which is the exact colour of the
   closing CTA band and the footer. The tab is fixed to the bottom right of every
   page, so it spent the whole end of each one sitting invisibly on its own colour;
   the only thing separating it from the band behind it was its drop shadow.
   Against coal the white label runs about 17:1, and the green hover still reads as
   a lift rather than a colour it was already wearing. */
.flyer-tab { background: var(--coal); border-radius: var(--radius-sm); }
.flyer-tab:hover, .flyer-tab:focus-visible { background: var(--green); }

/* ═══ 640px ═════════════════════════════════════════════════════════════ */
@media (min-width: 640px) {
  .team-grid { grid-template-columns: repeat(3, 1fr); }
  .testimonial-grid { columns: 2; }
}

/* ═══ 900px ═════════════════════════════════════════════════════════════ */
@media (min-width: 900px) {
  .about-history-grid {
    grid-template-columns: 0.9fr 1.1fr;
    gap: clamp(40px, 6vw, 80px);
  }
  /* Visual swap only. The photo stays first in the DOM on every row, so a
     screen reader and a keyboard still get photo-then-text throughout and the
     alternation is purely a sighted-reader rhythm. */
  .about-history-grid-reverse { grid-template-columns: 1.1fr 0.9fr; }
  .about-history-grid-reverse .about-photo { order: 2; }
  .about-history-grid-reverse .about-history-text { order: 1; }

  .team-grid { grid-template-columns: repeat(4, 1fr); }
  .testimonial-grid { columns: 3; }
  .doc-preview-inline { height: 560px; }
}

/* ═══ 1180px ════════════════════════════════════════════════════════════ */
@media (min-width: 1180px) {
  /* Four across, not five. Five made each portrait about 200 CSS px wide, and
     these are photographs of people — the faces were the smallest thing in a
     section whose entire point is that you can see who you will be talking to.
     Four gives roughly 265px.

     There is a ceiling here worth knowing about: the source files are 240x240.
     At 265px they are already being scaled up slightly, and on a 2x screen
     they were being stretched well past their real resolution even at the old
     size. Going wider than four columns' worth would start to look obviously
     soft. Sharper portraits need bigger source files, not more CSS. */
  .team-grid { grid-template-columns: repeat(4, 1fr); gap: var(--space-lg); }
}


/* ── Print ─────────────────────────────────────────────────────────────────
   Someone at a restaurant prints the delivery page or the FAQ. Drop the
   furniture, keep the words, and make link targets visible on paper. */
@media print {
  .site-header, .site-footer, .flyer-tab, .quote-arrow, .nav-toggle,
  .hero::before, .cta-band::before, .vendor-strip { display: none !important; }
  body { background: #fff; color: #000; font-size: 11pt; }
  section { padding: 12pt 0; break-inside: avoid; }
  .faq-item[open] .faq-answer, .faq-answer { display: block !important; }
  a[href^="http"]::after,
  a[href^="tel"]::after { content: " (" attr(href) ")"; font-size: 9pt; color: #444; }
  .anchor-dark, .cta-band { background: #fff !important; color: #000 !important; }
  .anchor-dark h2, .cta-band h2, .anchor-dark p, .cta-band p { color: #000 !important; }
}


/* ════════════════════════════════════════════════════════════════════════════
   DARK HERO (option B) — homepage only, gated on body.has-dark-hero
   ────────────────────────────────────────────────────────────────────────────
   The trucks photo goes full-bleed behind the headline under a gradient scrim,
   the header rides on top of it, and the page returns to bone the moment the
   hero ends. Dark is the entrance, not the building: the catalog, the delivery
   checker and the credit application all stay light, because long-form reading
   and forms get measurably worse on a dark ground.

   Everything is scoped to .has-dark-hero / .hero-dark, so removing one class
   from <body> and one from <section> reverts the homepage completely.
   ════════════════════════════════════════════════════════════════════════════ */

/* Colour tokens for the dark chrome live in css/tokens.css with the rest of
   the palette — see the header of that file for why. */

/* ── The section ─────────────────────────────────────────────────────────── */
.hero-dark {
  position: relative;
  isolation: isolate;
  background: var(--coal);
  color: var(--bone);
  overflow: clip;
  /* Was `calc(68px + …)` to clear the header. It never needed to: the header is
     position:sticky, which stays in flow, so the hero already starts below it —
     the extra 68px was clearing an overlap that does not happen. */
  padding-top: var(--space-2xl);
}
/* The sand diagonal is a light-ground device; it has no job here. */
.hero-dark::before { display: none; }

.hero-bg {
  position: absolute;
  inset: 0;
  z-index: -2;
  pointer-events: none;
}
.hero-bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Holds the truck fronts in frame as the container gets shorter and wider. */
  object-position: 52% 46%;
}
/* Two layers: a horizontal wash heaviest where the copy sits, and a vertical
   one that grounds the proof rail at the bottom edge. Gradients, not
   backdrop-filter — a blur over a full-bleed photo repaints the whole area on
   every scroll frame and is the most expensive thing you can put on a
   mid-range Android. */
.hero-scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /* Alphas are the measured floor, not a guess. Sampling the brightest pixel
     of the photo that falls behind the copy (a white truck panel, rgb 233 237
     240) and compositing this scrim over it puts the green link — the tightest
     pair on the block — at 4.78:1 when the wash is 0.82 and 4.47:1 at 0.80. So
     0.85 across the text zone is the limit, and everything right of it opens up
     to let the trucks read. Re-measure if the photo or --green-on-dark change. */
  background:
    linear-gradient(100deg,
      rgba(8, 14, 11, 0.93) 0%,
      rgba(8, 14, 11, 0.86) 38%,
      rgba(8, 14, 11, 0.62) 68%,
      rgba(8, 14, 11, 0.44) 100%),
    linear-gradient(to top,
      rgba(8, 14, 11, 0.88) 0%,
      rgba(8, 14, 11, 0) 46%);
}
/* Single column below 1100, so the copy runs over the whole frame rather than
   over its left third — the angled wash would leave the last lines sitting on
   bare photo. Flatten it out instead of merely darkening it. */
@media (max-width: 1099px) {
  .hero-scrim {
    /* Lighter through the middle than at either end: the headline sits in the
       top band and the links in the bottom one, and the mid is mostly the sub
       — which has far more headroom than the green links do. */
    background:
      linear-gradient(180deg,
        rgba(8, 14, 11, 0.85) 0%,
        rgba(8, 14, 11, 0.78) 55%,
        rgba(8, 14, 11, 0.90) 100%);
  }
}

.hero-dark .hero-grid { grid-template-columns: 1fr; }
.hero-dark .hero-content { max-width: 56ch; }

/* ── Type and controls on the dark ground ────────────────────────────────── */
.hero-dark h1 { color: var(--paper); }
.hero-dark .hero-sub { color: #dfe6e2; }
.hero-dark .hero-cta-label,
.hero-dark .hero-existing-label { color: var(--bone-dim); }
.hero-dark .hero-existing { border-top-color: var(--coal-line); }
.hero-dark .hero-existing-links a { color: var(--green-on-dark); }
.hero-dark .hero-existing-links a:hover { text-decoration-color: var(--green-on-dark); }

/* -- The flyer link, promoted ---------------------------------------------
   First in the row and the only one wearing a pill. Everything else in this
   line is permanent — the catalog, your rep — and this is the one item that
   changes every week, so it is the one an existing customer is actually
   opening the home page to find.

   It was a quiet tinted pill on purpose, sized not to compete with the address
   checker above. That was the wrong call for the audience: a prospect reads the
   checker, but a returning customer is here for this and nothing else, and at
   14px in a 15% tint it read as a third link rather than the errand. So it is
   now a filled pill at button scale — solid --green-on-dark with --green-forest
   type, 5.6:1, comfortably past AA at this size.

   Solid GREEN rather than the bone of .btn-on-dark, deliberately: the two sit
   in the same eyeline, and giving the flyer the primary button's exact skin
   would leave a first-time visitor with two identical-looking things to press
   and no way to tell which one answers their question. Different colour, same
   weight. Same tag icon as the floating tab so the two register as one thing
   seen twice, not two offers. */
.hero-dark .hero-existing-flyer a {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 12px 24px 12px 19px;
  border-radius: 999px;
  background: var(--green-on-dark);
  border: 1px solid var(--green-on-dark);
  color: var(--green-forest);
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  /* The icon, the label and the date are one object; letting the label break
     mid-phrase ("This week's / flyer") turned the pill into a ragged two-line
     block. The date is dropped by the media query below before it can wrap. */
  white-space: nowrap;
  /* Lifts it off a photographic hero the tinted version didn't need to clear. */
  box-shadow: 0 2px 14px color-mix(in srgb, var(--green-on-dark) 30%, transparent);
  transition: background var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard),
              box-shadow var(--dur-fast) var(--ease-standard);
}
.hero-dark .hero-existing-flyer a:hover,
.hero-dark .hero-existing-flyer a:focus-visible {
  background: color-mix(in srgb, var(--green-on-dark) 82%, var(--paper));
  border-color: color-mix(in srgb, var(--green-on-dark) 82%, var(--paper));
  box-shadow: 0 3px 20px color-mix(in srgb, var(--green-on-dark) 45%, transparent);
}
/* Same trick as .wwc-logos above: the global `li > a:not(.btn):not(.card-link)`
   underline rule is (0,2,2) and outranks the (0,2,1) block above, so a plain
   `text-decoration: none` there never took — the pill has carried a stray
   underline since it was built. Matching that selector's specificity switches
   it off. Invisible at the old 14px tint; not invisible on a filled button. */
.hero-dark li.hero-existing-flyer > a:not(.btn):not(.card-link) {
  text-decoration: none;
}
.hero-flyer-icon { flex: none; width: 20px; height: 20px; }

/* The week the prices are good for. Quieter than the label it follows — it is
   confirmation, not the thing being clicked. Empty until weekly-flyer.js fills
   it, and :empty keeps the separator from rendering as a stray bullet when the
   host sends no Last-Modified header and no range can be worked out. */
.hero-flyer-dates {
  font-weight: 600;
  font-size: 0.9rem;
  /* Was a light green, which was right when the pill was a dark tint and is
     invisible now that the pill is filled green. Softened toward the fill
     rather than toward white so it still reads as the quieter half — but only
     as far as the contrast allows: at 14.4px this is normal-size text and needs
     4.5:1 against the fill. 72% measured 3.40:1 and 85% still only 4.36:1;
     92% lands at 4.9:1. That leaves little room to separate it from the label
     by colour, so the size drop does the work instead — 14.4px against the
     label's 17px, with the ::before separator dimmed on its own. */
  color: color-mix(in srgb, var(--green-forest) 92%, var(--green-on-dark));
}
.hero-flyer-dates:empty { display: none; }

/* Past this width the label and the date together are wider than the column and
   the pill wraps into a ragged block with the icon stranded in the middle. The
   label is the point; the week is confirmation, and the floating tab still
   carries it.

   The threshold was 359px when the pill was 14px type in a tint. At button
   scale the pill measures 374px wide with the date and 212px without, against a
   335px column on a 375px phone — so it now has to drop by 430px, which covers
   every common handset in portrait. Above that the row has the room. */
@media (max-width: 429px) {
  .hero-flyer-dates { display: none; }
}
.hero-flyer-dates::before { content: "·"; margin-right: 6px; opacity: 0.65; }

/* The "family owned since 1991" line that replaced the seal on the photo —
   the seal had a photo corner to sit in, and a full-bleed hero has none. */
.hero-since {
  margin: var(--space-md) 0 0;
  font-size: 0.88rem;
  color: var(--bone-dim);
}

.hero-dark .proof-rail { border-top-color: var(--coal-line); }
.hero-dark .proof-item b { color: var(--green-on-dark); }
.hero-dark .proof-item span { color: var(--bone-dim); }

/* Focus rings: ink-on-coal is a ring you cannot see. Same inversion the
   footer and CTA band already use. */
.hero-dark :focus-visible {
  outline-color: var(--bone);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--coal) 70%, transparent);
}

/* ── The vendor strip, lifted out of the hero ────────────────────────────── */
/* These are third-party brand marks — Tyson red, Coca-Cola red, Pepsi blue —
   several of which carry a light ground in the artwork itself. They cannot go
   on coal without either recolouring someone else's logo or floating white
   boxes across the band, so the strip now sits in its own bone section
   directly under the hero. */
.vendor-section {
  background: var(--bone);
  /* Even top and bottom. The bottom was --space-2xl against a --space-xl top, and
     since 2xl is a clamp that reaches 88px it read as up to 40px of extra floor
     under the "many more brands" line — a band that looked bottom-heavy rather
     than one the logos sat centred in. */
  padding: var(--space-xl) 0;
}
.vendor-section .vendor-strip { margin-top: 0; }

/* ── Reduced motion ──────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  body.has-dark-hero .site-header { transition: none; }
}

/* ── Print ───────────────────────────────────────────────────────────────── */
@media print {
  .hero-dark { background: #fff !important; color: #000 !important; padding-top: 0; }
  .hero-bg, .hero-scrim { display: none !important; }
  .hero-dark h1, .hero-dark .hero-sub, .hero-dark .proof-item b,
  .hero-dark .proof-item span, .hero-since,
  /* Prints green on white otherwise. .hero-eyebrow is display:none below
     1100px and .hero-since is hidden above it, so only one of the pair ever
     reaches the paper — both are listed so whichever it is comes out black. */
  .hero-dark .hero-eyebrow { color: #000 !important; }
}


/* ════════════════════════════════════════════════════════════════════════════
   DARK CHROME — site-wide coal header + forest masthead
   ────────────────────────────────────────────────────────────────────────────
   The header is coal on EVERY page, not just the homepage. That was the open
   question when the dark hero shipped: a dark bar on the homepage and a light
   one everywhere else moves the single most repeated element on the site
   between two appearances, which is exactly the inconsistency navigation is
   supposed to avoid. One header, one colour, everywhere.

   It also deletes work. The homepage used to hand the header back to its light
   state once the hero scrolled past, which needed an IntersectionObserver, a
   no-JS fallback and an opt-out for the open mobile menu. None of that exists
   now: the header never changes, so there is no state to manage.

   Interior pages then open on a forest-green masthead rather than sand. Coal
   bar, forest band, bone page — three steps down in one direction, so the top
   of an interior page has the same weight as the homepage hero without needing
   a photograph for every page.
   ════════════════════════════════════════════════════════════════════════════ */

/* ── Header ──────────────────────────────────────────────────────────────── */
.site-header {
  background: var(--coal);
  border-bottom: 1px solid transparent;
}
/* .is-stuck MUST restate the background. The resting rule up the file is
   `.site-header` (0,1,0) but the stuck one is `.site-header.is-stuck` (0,2,0),
   and it sets a bone fill — so without this the bar flipped back to light the
   moment the page scrolled, on every page. Specificity, not source order.

   The elevation shadow goes with it: it was tuned to stop a white bar
   dissolving into white content, which is not a problem a near-black bar has.
   A hairline is enough to mark the edge. */
.site-header.is-stuck {
  background: var(--coal);
  box-shadow: none;
  border-bottom-color: var(--coal-line);
}
/* The desktop frosted-glass blur painted behind an opaque fill, so it cost a
   compositor layer on every scroll frame and showed nothing. Dropped. */
@media (min-width: 1180px) {
  .site-header { -webkit-backdrop-filter: none; backdrop-filter: none; }
}

/* These two live in the bar itself at every width, so they are always on coal. */
.site-header .header-call,
.site-header .nav-toggle { color: var(--bone); }
.site-header .header-call:hover { color: var(--green-on-dark); }

/* Everything else in the nav is bone ONLY from 1180px up, because that is the
   only width at which .site-nav is the horizontal bar sitting on the coal.
   Below it the same markup is a full-screen WHITE off-canvas panel, so bone
   links there are invisible — which is the same mistake as painting the
   dropdown panels bone, one breakpoint down. */
@media (min-width: 1180px) {
  /* `> ul > li >` matters: the dropdown panels are white surfaces hanging below
     the dark bar. Only the top-level row sits on the dark. */
  .site-header .site-nav > ul > li > a,
  .site-header .site-nav > ul > li > .nav-dropdown-toggle {
    color: var(--bone);
  }
  .site-header .site-nav > ul > li > a:hover,
  .site-header .site-nav > ul > li > .nav-dropdown-toggle:hover {
    color: var(--green-on-dark);
  }
  .site-header .site-nav > ul > li > a::after { background: var(--green-on-dark); }
  .site-header .site-nav > ul > li > a[aria-current="page"] { color: var(--paper); }

  /* Green at rest, white on hover — the same swap the hero and the CTA bands got.
     This was bone hovering to paper, which is two names for the same white: the
     most prominent control in the header had no visible hover state at all.

     --green-on-dark, matching the hero's Check button rather than the band's
     lighter accent, because this sits on coal. The flat --green pill that used to
     be here measured 2.7:1 against coal — under the 3:1 a control's own edge
     needs, which is what put a bone button on the bar in the first place. This
     green is 7.2:1 on coal with the forest label at 5.6:1 on it, so it can be
     green again without dissolving into the bar.

     Both halves of the split take it together: the label is a link straight to
     the contact page and the caret is the button that opens the menu, and they
     have to read as one pill. In the mobile panel neither rule applies — this is
     inside the 1180px block, and there the pill stays --green-dark on white. */
  .site-header .site-nav a.nav-cta-label,
  .site-header .nav-cta .nav-dropdown-toggle.nav-cta-caret {
    background: var(--green-on-dark);
    color: var(--green-forest);
  }
  .site-header .site-nav a.nav-cta-label:hover,
  .site-header .site-nav a.nav-cta-label:focus-visible,
  .site-header .nav-cta .nav-dropdown-toggle.nav-cta-caret:hover,
  .site-header .nav-cta .nav-dropdown-toggle.nav-cta-caret:focus-visible {
    background: var(--bone);
    color: var(--green-forest);
  }
  /* The divider between the two halves was 28% white, which was invisible on the
     bone pill and would have been nearly so on either of these fills. A forest
     tint reads on the green and on the white it hovers to, which is the point of
     it: the caret is a separate control from the label beside it. */
  .site-header .nav-cta .nav-dropdown-toggle.nav-cta-caret {
    border-left-color: color-mix(in srgb, var(--green-forest) 32%, transparent);
  }
}

/* Ink-on-coal is a ring you cannot see — but only the controls actually sitting
   on the coal get the inverted one. Listed explicitly rather than scoped to the
   header as a whole, because the header also contains two white surfaces (the
   dropdown panels, and the mobile off-canvas panel) where a bone ring would
   disappear exactly like bone text does. Those keep the default dark ring. */
.site-header .brand:focus-visible,
.site-header .header-call:focus-visible,
.site-header .nav-toggle:focus-visible {
  outline-color: var(--bone);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--coal) 70%, transparent);
}
@media (min-width: 1180px) {
  .site-header .site-nav > ul > li > a:focus-visible,
  .site-header .site-nav > ul > li > .nav-dropdown-toggle:focus-visible,
  .site-header .site-nav a.nav-cta-label:focus-visible,
  .site-header .nav-cta .nav-dropdown-toggle.nav-cta-caret:focus-visible {
    outline-color: var(--bone);
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--coal) 70%, transparent);
  }
}

/* The mobile panel stays white and starts directly under the bar. A coal
   header above a white sheet is a clean edge and needs no special case — the
   previous build forced the header light while the menu was open, which is
   the one moment the header changed colour and is no longer needed. */

/* ── Interior masthead ───────────────────────────────────────────────────── */
/* .doc-hero is in here too, but it had to be cut down first: on Credit
   Application it wrapped the entire page — 1767px including an embedded PDF
   viewer with its own toolbar — so "make the masthead dark" would have meant
   putting a white document slab on green. The cards now live in .doc-body
   below it and the band is just crumbs, headline and intro. */
/* --green-band and --green-on-band are declared with the rest of the palette
   at the top of the file; they now govern the footer and CTA too, not just
   these mastheads. */
.page-hero,
.doc-hero {
  background: var(--green-band);
  border-bottom: 0;
  color: var(--bone);
}
.doc-body { background: var(--bone); padding: var(--space-2xl) 0 var(--space-3xl); }
.page-hero h1,
.doc-hero h1 { color: var(--paper); }
.page-hero .page-hero-sub,
.doc-hero p { color: color-mix(in srgb, var(--bone) 80%, var(--green-band)); }
/* An older block up the file still centres this masthead (`text-align:center`
   plus `margin: 0 auto` on the paragraph) from before the redesign left-aligned
   everything else. Invisible while it sat on sand; against a forest band it
   read as crumbs centred, headline left, intro centred. Left, like the other
   nine mastheads. */
.doc-hero { text-align: left; }
.doc-hero p { margin-inline: 0; }

/* Every crumb tier moved up the scale. The old mixes were set against forest;
   on a band this much lighter the same percentages collapse — the current-page
   label landed at 4.54:1 and the "/" separator at 2.78:1. Rescaled so all three
   clear AA while keeping the hierarchy legible: link 88% > current 75% >
   separator 65%. */
.page-hero .crumbs,
.doc-hero .crumbs { color: color-mix(in srgb, var(--bone) 75%, var(--green-band)); }
.page-hero .crumbs a,
.doc-hero .crumbs a { color: color-mix(in srgb, var(--bone) 88%, var(--green-band)); }
.page-hero .crumbs a:hover,
.doc-hero .crumbs a:hover { color: var(--green-on-band); }
.page-hero .crumbs li + li::before,
.doc-hero .crumbs li + li::before { color: color-mix(in srgb, var(--bone) 65%, var(--green-band)); }
.page-hero .crumbs [aria-current="page"],
.doc-hero .crumbs [aria-current="page"] { color: color-mix(in srgb, var(--bone) 75%, var(--green-band)); }

.page-hero a:not(.btn):not(.crumbs a) { color: var(--bone); }
.page-hero .team-finder-link { color: var(--green-on-band); }

.page-hero :focus-visible,
.doc-hero :focus-visible {
  outline-color: var(--bone);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--green-band) 70%, transparent);
}

/* 404 reuses the homepage's "Already a customer?" block inside its masthead. */
.page-hero .hero-existing { border-top-color: color-mix(in srgb, var(--bone) 24%, transparent); }
.page-hero .hero-existing-label { color: color-mix(in srgb, var(--bone) 80%, var(--green-band)); }
.page-hero .hero-existing-links a { color: var(--green-on-band); }

/* ── Print ───────────────────────────────────────────────────────────────── */
@media print {
  .site-header { background: #fff !important; border-bottom: 1px solid #ccc !important; }
  .page-hero, .doc-hero { background: #fff !important; color: #000 !important; }
  .page-hero h1, .page-hero .page-hero-sub, .page-hero .crumbs,
  .page-hero .crumbs a, .page-hero .crumbs [aria-current="page"],
  .doc-hero h1, .doc-hero p, .doc-hero .crumbs,
  .doc-hero .crumbs a, .doc-hero .crumbs [aria-current="page"] { color: #000 !important; }
}

/* ═══ Mobile action bar (punchlist #7b) ═══════════════════════════════════
   The home page runs to ~11,800px on a phone. The header scrolls away in the
   first screen and nothing replaceable follows it, so for thirteen screens
   there is no way to call or to ask the one question every prospect has.

   Two actions, no more — a third would turn a decision into a menu. "Call us"
   because that is the business's actual conversion, and "Check your address"
   which jumps back to the hero field rather than loading another page.

   Phone only: above 720px the header is still in reach and a fixed bar is just
   chrome eating the viewport. */
:root { --mobile-bar-h: 58px; }

.mobile-bar { display: none; }

@media (max-width: 720px) {
  .mobile-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 93;          /* over .flyer-tab (92), which is offset below */
    display: grid;
    grid-template-columns: 1fr 1.4fr;
    align-items: stretch;
    min-height: var(--mobile-bar-h);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    background: var(--coal);
    border-top: 1px solid var(--coal-line);
  }

  .mobile-bar-item {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 0 12px;
    min-height: var(--mobile-bar-h);
    font-weight: 600;
    font-size: 0.92rem;
    line-height: 1.2;
    text-align: center;
    text-decoration: none;
    color: var(--bone);
  }
  .mobile-bar-item svg { flex: none; width: 18px; height: 18px; }
  .mobile-bar-item + .mobile-bar-item { border-left: 1px solid var(--coal-line); }
  .mobile-bar-item-primary { background: var(--green); color: var(--paper); }
  .mobile-bar-item:focus-visible { outline: 2px solid var(--green-on-dark); outline-offset: -3px; }

  /* Nothing should end up permanently underneath the bar: the footer's last
        line and the flyer button both sit at the bottom of the page.
     
        Scoped to .has-mobile-bar rather than applied at this width generally — the
        bar is on the home page only, and without the guard every other page would
        float its flyer button 58px up to clear a bar that is not there. */
  .has-mobile-bar .site-footer { padding-bottom: calc(var(--mobile-bar-h) + env(safe-area-inset-bottom, 0px) + var(--space-md)); }
  .has-mobile-bar .flyer-tab { bottom: calc(var(--mobile-bar-h) + 16px + env(safe-area-inset-bottom, 0px)); }
}

/* The bar is navigation chrome, not content. */
@media print { .mobile-bar { display: none; } }


/* The tab still appears and disappears under reduced motion — that is state,
   not decoration — but it does not travel to get there. */
@media (prefers-reduced-motion: reduce) {
  .flyer-tab[data-reveal-on-scroll]:not(.is-revealed) { transform: none; }
}


/* ════════════════════════════════════════════════════════════════════════════
   CRO PASS — Sep 12 2026
   ────────────────────────────────────────────────────────────────────────────
   Five changes off a conversion audit of the homepage. The finding behind each
   is in the markup comment at its call site; the measurements are here.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── 1a. The office number on desktop: NOT in the header, and why ────────────
   The obvious fix was to drop the `.header-call { display: none }` at 1180px
   and let the pill ride the bar at every width. It does not fit, and the
   arithmetic is worth leaving here so nobody spends the afternoon re-deriving
   it. Measured in the live bar, not estimated — the comment at the 1180px block
   still quotes a 627px nav and it is 749px now:

     content width available   .container is max-width 1180 with 40px padding,
                               so the row has min(1100, vw - 160)px — and it is
                               CAPPED. Raising the breakpoint buys nothing: the
                               squeeze at 1920 is the squeeze at 1180.
     brand 277 + nav 749       = 1026, leaving 59px of slack
     the pill                  = 184, or 154 with the word "Call" dropped

   154 into 59 does not go, and it does not go at 3px nav padding either (1120
   against 1100). Shrinking the wordmark or the nav labels to buy 95px would
   cost more than the number is worth in that slot.

   So the number lands in two places the layout can actually afford instead:
   .hero-aside below (above the fold, where the decision is made) and the call
   line closing .cta-band. Between them the "You get a person, not a queue"
   claim in the why-us list carries it inline, which covers the middle of the
   page where the old layout had nothing. */

/* ── 1b. The hero contact block ──────────────────────────────────────────────
   Desktop only. Below 1100 the grid is one column and this is display:none —
   the header pill and the bottom action bar already carry the number there,
   and the phone fold has under 40px of slack to spend. */
.hero-aside { display: none; }

@media (min-width: 1100px) {
  /* The dark hero reset .hero-grid to a single column because it had nothing to
     put in the second one. It does now. minmax(0,…) on the copy column so the
     headline can still shrink; a floor on the aside so the number never wraps. */
  .hero-dark .hero-grid {
    grid-template-columns: minmax(0, 1.4fr) minmax(260px, 0.6fr);
    gap: clamp(32px, 5vw, 72px);
    align-items: start;
  }
  .hero-aside {
    display: block;
    padding: var(--space-md);
    border-left: 2px solid var(--green-on-dark);
    border-radius: 0 var(--radius) var(--radius) 0;
    /* Optically aligned to the headline's cap height rather than its box. */
    margin-top: 10px;

    /* This has to be a filled panel, not the bare hairline it started as. The
       hero scrim is an ANGLED wash — 0.93 alpha at the left edge falling to 0.62
       at 68% and 0.44 at 100% — and the opening-up on the right is deliberate, so
       the trucks read. This block sits in exactly that stretch, which means it is
       the one piece of hero copy not standing on the 0.85 floor the text zone is
       measured against.

       Over the brightest pixel behind the hero (a white truck panel, rgb 233 237
       240) a 0.44 scrim composites to about rgb 134 139 139, where --bone-dim
       body text lands near 1.5:1. Unreadable, and only on some scroll positions
       and some screen widths, which is the worst kind of unreadable.

       Photo transmittance through two stacked layers is (1 - a_scrim)(1 - a_panel).
       Matching the text zone's 0.15 from the scrim's worst case needs
       (1 - 0.44)(1 - a) ≤ 0.15, so a ≥ 0.73. 0.80 with a little margin, which puts
       this block on the same floor as the headline beside it.

       Flat rgba, never backdrop-filter: a blur over a full-bleed photo repaints
       the whole area every scroll frame on a mid-range Android. */
    background: rgba(8, 14, 11, 0.80);
  }
  .hero-aside-label {
    margin: 0 0 var(--space-2xs);
    font-size: var(--step--1);
    font-weight: 600;
    letter-spacing: var(--tracking-eyebrow);
    text-transform: uppercase;
    color: var(--bone-dim);
  }
  .hero-aside-phone {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    font-family: var(--font-display);
    /* Tabular figures so the number reads as a number and not as words. */
    font-variant-numeric: tabular-nums;
    font-size: clamp(1.5rem, 1.2rem + 0.9vw, 1.9rem);
    font-weight: 600;
    line-height: 1.1;
    color: var(--bone);
    text-decoration: none;
  }
  .hero-aside-phone svg { width: 22px; height: 22px; flex: none; color: var(--green-on-dark); }
  .hero-aside-phone:hover,
  .hero-aside-phone:focus-visible { color: var(--green-on-dark); text-decoration: none; }
  .hero-aside-note {
    margin: var(--space-sm) 0 0;
    font-size: var(--step--1);
    line-height: 1.5;
    color: var(--bone-dim);
  }
}

/* Hours, rendered by the block in js/main.js. Outside the media query: render()
   rewrites className wholesale, so if these rules were scoped to ≥1100px the
   element would still be display:block from the class alone below it. */
.hero-aside-hours {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: var(--space-xs) 0 0;
  font-size: var(--step--1);
  color: var(--bone-dim);
}
.hero-aside-hours b { color: var(--bone); font-weight: 700; flex-shrink: 0; }
/* --green is 1.5-2.7:1 on coal — see the dark-surface notes. The hero's own
   accent is the only green that can carry a lit status dot here. */
.hero-aside-hours.is-open .hours-dot { background: var(--green-on-dark); }
.hero-aside-hours.is-open b { color: var(--green-on-dark); }
@media (max-width: 1099px) { .hero-aside-hours { display: none; } }


/* ── 3. The address check names what it pays out ─────────────────────────────
   A second line inside the label. display:block so it wraps as its own line
   rather than running on from the question, and lighter than the question so
   the control still reads as one thing rather than two. */
.hero-check-promise {
  display: block;
  margin-top: 2px;
  font-size: var(--step--1);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--bone-dim);
}


/* ── 4 + 5. The closing band gets a phone and a witness ──────────────────────
   The call sits under the buttons as a line: a third equal button would have
   flattened the primary action, and the band already runs two. */
.cta-band-call {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2xs) var(--space-sm);
  margin: var(--space-md) 0 0;
  font-size: var(--step-0);
  color: color-mix(in srgb, var(--bone) 80%, var(--green-band));
}
.cta-band-call svg { width: 20px; height: 20px; flex: none; color: var(--green-on-band); }
.cta-band-call a {
  color: var(--bone);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-decoration-color: color-mix(in srgb, var(--bone) 45%, transparent);
  text-underline-offset: 3px;
}
.cta-band-call a:hover,
.cta-band-call a:focus-visible { color: var(--green-on-band); }

.cta-band-hours {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--step--1);
  color: color-mix(in srgb, var(--bone) 70%, var(--green-band));
}
.cta-band-hours b { color: var(--bone); font-weight: 700; flex-shrink: 0; }
/* Same problem the footer has and the same answer: --green measures 1.45:1 on
   the band, so the dot uses the band's own accent at 4.62:1. */
.cta-band-hours.is-open .hours-dot { background: var(--green-on-band); }
.cta-band-hours.is-open b { color: var(--green-on-band); }

.cta-band-quote {
  max-width: 46ch;
  margin: var(--space-lg) 0 0;
  padding-left: var(--space-md);
  border-left: 2px solid color-mix(in srgb, var(--bone) 28%, transparent);
}
.cta-band-quote p {
  margin: 0;
  /* Overrides .cta-band p, which is a --step-1 lead paragraph. A quote that
     matched the lead would out-shout the buttons between them. */
  font-size: var(--step-0);
  font-style: italic;
  line-height: 1.55;
  max-width: none;
  color: color-mix(in srgb, var(--bone) 88%, var(--green-band));
}
.cta-band-quote cite {
  display: block;
  margin-top: var(--space-xs);
  font-size: var(--step--1);
  font-style: normal;
  color: color-mix(in srgb, var(--bone) 66%, var(--green-band));
}


/* ── 5b. Three lines, not four, on a phone ───────────────────────────────────
   The headline was 196px of the 608px usable fold on a 375x667 screen — the
   reason the check button sat within 14px of the action bar once the promise
   line above was added.

   It is NOT the 15ch cap doing it, which is the obvious suspect and the wrong
   one: 15ch is 391px and the column is 350px, so the cap never binds on a
   phone and widening it changes nothing. The break is the type size against
   the column. Measured in that 350px column, holding everything else:

     41px (the current --step-5 at 375px)   4 lines   187px
     40px                                   4 lines   182px
     39px                                   3 lines   133px   ← the cliff
     38px                                   3 lines   130px

   So the whole problem is worth two pixels. 39 is the edge, which is not a
   place to sit — a fallback face half a percent wider lands back on four — so
   this takes 38 and keeps a pixel of margin. 57px back, a 7% smaller headline
   on phones, and nothing above 479px is touched.

   Expressed as a vw ceiling rather than a fixed px so it holds across the whole
   range instead of at one width. The coefficient was swept at each stop, because
   the container padding is not proportional — 25px total at 375 and 40px at 414,
   so the column is a smaller share of a bigger phone and a ratio tuned at 375
   alone breaks above it:

     10.2vw   320 ✓   375 ✓   414 ✗ four lines   479 ✓
      9.6vw   320 ✓   375 ✓   414 ✓             479 ✓

   9.6 it is — the next stop down from the 414px cliff at 10.0, with a stop of
   margin rather than sitting on it. min() with the token means this can only
   ever reduce the scale, never raise it.

   Scoped to .hero-dark rather than .hero, unlike the other .hero h1 rules above:
   this coefficient is tuned to one specific string of 49 characters, not to the
   component, and a future page that borrows .hero with a different headline
   should get the untouched scale. */
@media (max-width: 479px) {
  .hero-dark h1 { font-size: min(var(--step-5), 9.6vw); }
}


/* ── 6. The desktop fold: the check field was under it ───────────────────────
   Measured at 1366x768, the commonest laptop screen there is. Maximised Chrome
   on Windows leaves roughly 640px of viewport once tabs, toolbar and taskbar are
   taken out. The hero put #hero-check-address — the only conversion control on
   the page — at 662, so the field, the Check button and all three proof figures
   sat below the fold. A visitor on that screen got the headline, the sub, and a
   clipped "Do we deliver to you?".

   The culprit was NOT the 15ch cap on .hero h1 near the top of the hero block,
   which is the obvious suspect and the wrong one. 1ch of Bitter at the 64px
   ceiling of --step-5 is 40.77px, so 15ch is 611.5px against a 601px content
   column (.hero-dark .hero-content is 56ch) — it never binds. It was a second,
   tighter cap of 13ch in the 1180px block much further down, 530px, and it only
   applied from 1180 up. Between 1100 and 1179 the hero was already correct,
   which is why the bug looked width-dependent in a way the base rule could not
   explain.

     13ch (530px)   Foodservice / supply from / people who / know your name
                    h1 306px   input at 662   hero 1132px
     19ch (774px)   Foodservice supply / from people who / know your name
                    h1 233px   input at 592   hero 1019px

   19ch is past the 601px column, so the column governs and no cap binds at any
   desktop width. Written as a number rather than max-width:none so the intent survives
   someone changing that 56ch later.

   This has to be a CSS fix rather than a wider column: splitLines() in
   js/main.js freezes the line breaks it measures at load, so widening the h1
   after the split does nothing at all. The cap has to be right before the
   script runs.

   Scoped ≥1100px. The phone fold is a separate and tighter problem, solved in
   5b, and neither cap ever bound there — 15ch is 611.5px against a 350px
   column, which is the measurement 5b's comment is making. */
@media (min-width: 1100px) {
  .hero-dark h1 { max-width: 19ch; }
}

/* ── 6b. Geography arrives before the headline, not after it ─────────────────
   Open item #3 from the Sep 12 CRO pass: geography is what disqualifies a
   visitor in the first three seconds, and it only turned up in the sub-line.
   The <title> tag did this better than the <h1>.

   .hero-eyebrow was written for exactly this and then never used by any page —
   "it states a fact the headline does not: where the company is and how long it
   has been there". This is that use. The string is not new copy: it is
   .hero-since, lifted from the foot of the hero (y=1094, below every fold, seen
   by nobody) to above the headline. So the two swap places at 1100px rather
   than both running — which is also why .hero-since is not simply deleted, it
   still does its original job on a phone.

   Desktop only, and the reason is measured. At 375px the string wraps to two
   lines and costs 61px; the phone fold has 9px of slack between .hero-secondary
   and the action bar (input 477-531, secondary 551-599, bar top 608). Showing
   it there pushes "or browse the catalog" under the bar. The shortened sub-line
   buys 29px back, which is not the 61 it costs.

   Contrast, sampled the same way as the scrim alphas above — the brightest pixel
   behind the copy (a white truck panel, rgb 233 237 240) composited under the
   wash, at the eyebrow's own position in the 0.93-0.86 band: --green-on-dark
   reads 6.58:1 falling to 5.44:1. Small text needs 4.5, so there is room. The
   0.82/4.78:1 pair in the scrim comment reproduces exactly, which is what says
   the model is right.

   One known cost: below 1100px this is display:none but still carries
   data-reveal-hero, so it takes a 90ms slot in the hero choreography without
   drawing anything. Left alone deliberately — the fix is a visibility filter in
   main.js's reveal list, which is shared with every other page on the site, and
   90ms of silence nobody can see is not worth that blast radius. */
.hero-dark .hero-eyebrow { display: none; }

@media (min-width: 1100px) {
  .hero-dark .hero-eyebrow {
    display: block;
    color: var(--green-on-dark);
  }
  /* Its content is in the eyebrow now; running both would print 1991 twice. */
  .hero-since { display: none; }
}
