/* =============================================================================
   I Like To Move It Movers — design system

   Palette derived from Ray's approved market logos, August 4 2026. The two
   markets are separate local operations and each carries its own logo, so the
   brand is themed per market rather than shared:

     Albany, NY (default)  red #CC1116 + black #131313 on off-white
     Charlotte, NC         navy #00193F + blue #1F5C9E on off-white

   Albany is the default because it is the live market — the crews, the trucks
   and the phone all answer there today. Charlotte is Fall 2026 and scopes
   itself under [data-market="charlotte"], which the generator stamps on <html>.

   Contrast decisions that are deliberate and should not be "tidied":
     - Both accents clear 4.5:1 on cream (red 5.31, blue 6.31) so the accent is
       safe for text on a light ground. The previous palette needed a separate
       darkened token for this because safety orange sat at 2.3:1; that hack is
       gone. --accent-deep now only exists for small print and dense link runs,
       where the extra headroom (red 7.28, blue 8.49) is worth having.
     - White on both accents clears 4.5:1 (red 5.74, blue 6.81), so accent
       buttons carry WHITE text. The old orange buttons needed navy text.
     - The light carolina blue from the Charlotte logo is 2.14:1 on cream and is
       therefore decorative only. It is the on-dark accent (7.51:1 against the
       Charlotte navy), never text on a light ground.

   Colours are also published as RGB triplets because roughly twenty rules build
   translucent fills with rgba(). Those referenced the literal navy and orange
   before, which meant a themed override could not reach them.
   ========================================================================== */

/* -------------------------------------------------------------- tokens ---- */
:root {
  /* Albany, the default market brand. */
  --dark: #131313;
  --dark-deep: #050505;
  --dark-soft: #232323;
  --dark-line: #3a3a3a;
  --dark-rgb: 19, 19, 19;

  --accent: #cc1116;
  --accent-lift: #e32b30;
  --accent-deep: #a50e13; /* extra headroom for small text on light grounds */
  --accent-rgb: 204, 17, 22;
  /* The accent is a mid-dark red, so it CANNOT be text on the dark header,
     hero or footer — #cc1116 on #131313 is only 3.24:1. This lightened red is
     6.09:1 there. It is for text on dark grounds only; on cream it drops to
     2.83:1, so never use it the other way round. */
  --accent-on-dark: #ff5a5f;

  /* Review stars stay gold on both themes. Gold is the convention users read as
     a rating; theming it would make the stars look like decoration. */
  --star: #f5a623;
  --line-hover: #d9d6d1;

  /* Form errors. On the Albany theme the brand accent IS red, so an invalid
     field styled in error-red would be indistinguishable from a focused one.
     Two things separate them: this error red is materially darker than the
     brand red (#8c1d18 vs #cc1116), and invalid fields also take a 2px border
     where focus uses a 4px translucent ring. Colour alone never carries the
     state — the field keeps aria-invalid and a visible message. */
  --error: #8c1d18;
  --error-ink: #8c1d18;

  --cream: #f7f6f4;
  --cream-deep: #edebe7;
  --cream-rgb: 247, 246, 244;
  --white: #ffffff;
  --ink: #131313;
  --ink-muted: #575757;
  --ink-on-dark: #f7f6f4;
  --ink-muted-on-dark: #c2c2c2;

  --font-display: "Montserrat", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-body: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

  --step--1: clamp(0.82rem, 0.79rem + 0.14vw, 0.9rem);
  --step-0: clamp(1rem, 0.96rem + 0.2vw, 1.11rem);
  --step-1: clamp(1.19rem, 1.12rem + 0.35vw, 1.39rem);
  --step-2: clamp(1.41rem, 1.29rem + 0.6vw, 1.73rem);
  --step-3: clamp(1.68rem, 1.48rem + 0.98vw, 2.17rem);
  --step-4: clamp(2rem, 1.7rem + 1.5vw, 2.71rem);
  --step-5: clamp(2.37rem, 1.92rem + 2.25vw, 3.39rem);
  --step-6: clamp(2.6rem, 2rem + 3vw, 4rem);

  --space-2xs: clamp(0.31rem, 0.3rem + 0.07vw, 0.35rem);
  --space-xs: clamp(0.56rem, 0.53rem + 0.14vw, 0.63rem);
  --space-sm: clamp(0.88rem, 0.84rem + 0.2vw, 1rem);
  --space-md: clamp(1.31rem, 1.24rem + 0.34vw, 1.5rem);
  --space-lg: clamp(1.75rem, 1.66rem + 0.45vw, 2rem);
  --space-xl: clamp(2.63rem, 2.49rem + 0.68vw, 3rem);
  --space-2xl: clamp(3.5rem, 3.32rem + 0.91vw, 4rem);
  --space-3xl: clamp(4.5rem, 3.98rem + 2.61vw, 6rem);

  --radius-sm: 8px;
  --radius: 14px;
  --radius-lg: 20px;
  --radius-xl: 28px;
  --radius-pill: 999px;

  --shadow-1: 0 1px 2px rgba(var(--dark-rgb), 0.07), 0 2px 6px rgba(var(--dark-rgb), 0.06);
  --shadow-2: 0 4px 10px rgba(var(--dark-rgb), 0.08), 0 10px 26px rgba(var(--dark-rgb), 0.08);
  --shadow-3: 0 10px 20px rgba(var(--dark-rgb), 0.1), 0 22px 48px rgba(var(--dark-rgb), 0.12);
  --shadow-accent: 0 8px 22px rgba(var(--accent-rgb), 0.32);

  --dur-fast: 140ms;
  --dur: 240ms;
  --dur-slow: 420ms;
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.645, 0.045, 0.355, 1);
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);

  --wrap: 1180px;
  --wrap-narrow: 780px;

  /* Two header heights, because the logo has two sizes. Ray, 2026-08-09: the
     header carries the original market badge now, and a badge only reads once
     it is big enough to show the mascot, the skyline and the city line — 92px,
     measured, not guessed. That plus 12px of air top and bottom is a 116px bar,
     and with the utility strip above it the page would open behind 153px of
     sticky chrome, which is a third of a laptop fold.

     So the header shrinks the moment it sticks: the utility strip collapses,
     the logo drops to 52px and the bar lands at 76px. At rest the visitor is
     looking at the brand, which is what the logo is for; once they have
     scrolled they are reading the page, and the header should get out of the
     way. scroll-padding-top reads the STUCK height, since any in-page jump
     happens with the header already stuck. */
  --header-h: 116px;
  --header-h-stuck: 76px;
  --brand-h: 92px;
  --brand-h-stuck: 52px;
  /* The utility strip above the header bar. It is not part of --header-h
     because --header-h drives the sticky offset and the strip collapses on
     scroll. The full-height hero needs the resting number instead: at the top
     of the page the strip is still open, so the hero starts --header-h plus
     this far down and has to subtract both to reach the fold. */
  --utility-h: 47px;

  /* Header surface, August 8 2026. Ray asked for "a lighter theme, the header is
     too dark", so the header — and ONLY the header — sits on white. The --dark*
     family is untouched: the hero, drawer, footer, dark cards and the photo
     scrims at the bottom of this file all still need a genuinely dark ground for
     white text to clear 4.5:1 over a bright photograph. Lightening those tokens
     globally would quietly break every one of them, so the header gets its own
     six tokens instead and every header rule reads from these.

     Two of them intentionally alias the light-ground tokens rather than holding
     their own hex: --header-fg is the page ink and --header-accent is the
     small-text accent, both already tuned for exactly this job. Because they
     alias, the Charlotte override below only has to restate what actually
     differs. --header-line and --header-tint are built from --dark-rgb, so they
     re-resolve per market at the point of use, the same trick --shadow-* uses.

     Measured against the white ground (Albany / Charlotte):
       --header-fg      #131313 / #00193f   18.58:1 / 17.34:1
       --header-fg-muted #575757 / #2e4d7e   7.23:1 /  8.47:1
       --header-accent  #a50e13 / #17497e    7.86:1 /  9.17:1
     and against the --header-bg-util strip (#edebe7, itself 1.19:1 off white):
       --header-fg      18.58 -> 15.62:1   /  17.34 -> 14.58:1
       --header-fg-muted                      6.07:1 /  7.11:1
       --header-accent                        6.60:1 /  7.70:1
     Every one clears 4.5:1, the muted tones included, so nothing on this header
     depends on the "large text" exemption. */
  --header-bg: var(--white);
  --header-bg-util: var(--cream-deep);
  --header-fg: var(--ink);
  --header-fg-muted: var(--ink-muted);
  --header-accent: var(--accent-deep);
  --header-line: rgba(var(--dark-rgb), 0.14);
  --header-tint: rgba(var(--dark-rgb), 0.06);
}

/* Albany market theme, restated. :root already holds these, so this block does
   nothing on an Albany page — it exists for the one case where it does matter:
   the market chooser on a CHARLOTTE page, where <html> is themed navy and the
   Albany card would otherwise inherit it and render a navy Albany. A card that
   says "Albany, NY" in Charlotte's colours is the kind of thing a client spots
   in two seconds. Anything scoped to a market must carry its own attribute and
   find its own tokens here, not assume it is on a page of that market. */
[data-market="albany"] {
  --dark: #131313;
  --dark-deep: #050505;
  --dark-soft: #232323;
  --dark-line: #3a3a3a;
  --dark-rgb: 19, 19, 19;

  --accent: #cc1116;
  --accent-lift: #e32b30;
  --accent-deep: #a50e13;
  --accent-rgb: 204, 17, 22;
  --accent-on-dark: #ff5a5f;

  --error: #8c1d18;
  --error-ink: #8c1d18;

  --ink: #131313;
  --ink-muted: #575757;
  --ink-on-dark: #f7f6f4;
  --ink-muted-on-dark: #c2c2c2;

  --header-fg: var(--ink);
  --header-fg-muted: var(--ink-muted);
  --header-accent: var(--accent-deep);
}

/* Charlotte market theme. Stamped on <html> by generate.mjs for the Charlotte
   hub and every Charlotte service page. Only the brand colours move — type,
   spacing, radii and motion are shared, so the two markets read as one company
   in two cities rather than as two different companies. */
[data-market="charlotte"] {
  --dark: #00193f;
  --dark-deep: #000d24;
  --dark-soft: #0d2b57;
  --dark-line: #1d3f6d;
  --dark-rgb: 0, 25, 63;

  --accent: #1f5c9e;
  --accent-lift: #2b73c0;
  --accent-deep: #17497e;
  --accent-rgb: 31, 92, 158;
  /* Charlotte's on-dark accent is the light carolina blue straight off its
     logo — 7.51:1 on the Charlotte navy. */
  --accent-on-dark: #6fb0ec;

  /* Charlotte's accent is blue, so error red is unambiguous here and can sit at
     the more legible weight. */
  --error: #b3261e;

  --ink: #00193f;
  /* Ray, 2026-08-07: Charlotte text must read navy, never grey or black. The
     old slate #4a5a74 was the one non-navy text tone on these pages; this is a
     desaturated navy that keeps the muted/full hierarchy (7.84:1 on cream vs
     15.4:1 for full ink). */
  --ink-muted: #2e4d7e;
  --ink-muted-on-dark: #a9cbea;

  /* The light header. The ground stays white in both markets — a navy-tinted
     header would just be the dark header again — so only the foregrounds move,
     and they move by following the Charlotte ink and accent above. These are
     restated rather than left to inherit from :root on purpose: if anyone ever
     hard-codes an Albany hex into the :root header tokens, Charlotte still
     resolves to its own navy instead of silently inheriting red-on-white.
     Ratios on white: fg 17.34:1, fg-muted 8.47:1, accent 9.17:1. */
  --header-fg: var(--ink);
  --header-fg-muted: var(--ink-muted);
  --header-accent: var(--accent-deep);
}

/* --------------------------------------------------------------- reset ---- */
*,
*::before,
*::after {
  box-sizing: border-box;
}

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h-stuck) + 16px);
}

body {
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.65;
  color: var(--ink);
  background: var(--cream);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

img,
svg,
picture,
video {
  display: block;
  max-width: 100%;
  height: auto;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

a {
  color: var(--accent-deep);
  text-underline-offset: 3px;
  text-decoration-thickness: 1.5px;
}

a:hover {
  color: var(--dark);
}

ul,
ol {
  padding-left: 1.15em;
}

:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

:focus:not(:focus-visible) {
  outline: none;
}

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

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  background: var(--accent);
  color: var(--dark);
  font-weight: 800;
  padding: 12px 20px;
  border-radius: 0 0 var(--radius) 0;
}

.skip-link:focus {
  left: 0;
}

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

/* ---------------------------------------------------------- typography ---- */
h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  font-weight: 800;
  line-height: 1.08;
  letter-spacing: -0.022em;
  text-wrap: balance;
}

h1 {
  font-size: var(--step-6);
  font-weight: 900;
}

h2 {
  font-size: var(--step-4);
}

h3 {
  font-size: var(--step-2);
  letter-spacing: -0.015em;
}

h4 {
  font-size: var(--step-1);
  letter-spacing: -0.01em;
}

p {
  text-wrap: pretty;
}

.eyebrow {
  font-family: var(--font-display);
  font-size: var(--step--1);
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-deep);
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: var(--space-sm);
}

/* The dashed rule that used to sit in front of every eyebrow is gone (Ray,
   2026-08-09). It was a 26px striped bar drawn with ::before, and at eyebrow
   size the stripes read as a strikethrough struck across the first word rather
   than as a rule leading into it. The eyebrow keeps its `display: flex`, which
   now only governs wrapping. */

/* The base .eyebrow takes --accent-deep, which is tuned for cream and white
   grounds and is far too dark to sit on one of the dark surfaces. On the hero it
   measured 1.59:1 — effectively invisible, and it had been that way since the
   photo hero shipped. Nothing caught it: axe declines to judge contrast when the
   text sits over a background image, so the browser gate passed it every run.
   Every dark context gets the on-dark accent here instead. */
.hero .eyebrow,
.page-hero .eyebrow,
.section-dark .eyebrow,
.panel-navy .eyebrow {
  color: var(--accent-on-dark);
}

.lede {
  font-size: var(--step-1);
  color: var(--ink-muted);
  max-width: 62ch;
}

.section-head {
  max-width: 66ch;
  margin-bottom: var(--space-xl);
}

.section-head p {
  color: var(--ink-muted);
  font-size: var(--step-1);
  margin-top: var(--space-sm);
}

/* ------------------------------------------------------------- layout ---- */
.container {
  width: min(100% - 2.5rem, var(--wrap));
  margin-inline: auto;
}

.container-narrow {
  width: min(100% - 2.5rem, var(--wrap-narrow));
  margin-inline: auto;
}

.section {
  padding-block: var(--space-3xl);
  position: relative;
}

.section-tight {
  padding-block: var(--space-2xl);
}

.section-cream {
  background: var(--cream);
}

.section-white {
  background: var(--white);
}

.section-sand {
  background: var(--cream-deep);
}

.section-dark {
  background: var(--dark);
  color: var(--ink-on-dark);
}

.section-dark h1,
.section-dark h2,
.section-dark h3,
.section-dark h4 {
  color: var(--white);
}

.section-dark p,
.section-dark li {
  color: var(--ink-muted-on-dark);
}

.section-dark .eyebrow {
  color: var(--accent-on-dark);
}

.grid {
  display: grid;
  gap: var(--space-md);
}

.grid-2 {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
}

.grid-3 {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 290px), 1fr));
}

.grid-4 {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
}

.split {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-xl);
  align-items: center;
}

@media (min-width: 900px) {
  .split {
    grid-template-columns: 1.05fr 0.95fr;
    gap: var(--space-2xl);
  }

  .split-media-first > :first-child {
    order: 2;
  }
}


/* ------------------------------------------------------------ buttons ---- */
.btn {
  /* White on the accent, not dark. Red gives 5.74:1 against white and only
     3.24:1 against the near-black; blue gives 6.81:1. The old orange was the
     other way round and needed dark text — do not carry that pairing over. */
  --btn-bg: var(--accent);
  --btn-fg: var(--white);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55em;
  min-height: 50px;
  padding: 0.7em 1.5em;
  border: 2px solid transparent;
  border-radius: var(--radius-pill);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--step-0);
  letter-spacing: -0.005em;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition:
    transform var(--dur) var(--ease-out),
    box-shadow var(--dur) var(--ease-out),
    background-color var(--dur) var(--ease-out),
    border-color var(--dur) var(--ease-out),
    color var(--dur) var(--ease-out);
}

.btn:hover {
  color: var(--btn-fg);
  transform: translateY(-2px);
  box-shadow: var(--shadow-accent);
}

.btn:active {
  transform: translateY(0) scale(0.98);
  transition-duration: var(--dur-fast);
}

.btn-primary:hover {
  background: var(--accent-lift);
}

.btn-navy {
  --btn-bg: var(--dark);
  --btn-fg: var(--white);
}

.btn-navy:hover {
  background: var(--dark-soft);
  box-shadow: var(--shadow-2);
}

/* Also the header phone button since the header went light. Nothing needed
   changing: --dark and --header-fg are the same value in both markets (#131313
   Albany, #00193f Charlotte), so the ink and the border land at 18.58:1 and
   17.34:1 on the white bar, and the hover inverts to white-on-dark at the same
   ratios. .btn-ghost-light below is still the hero's button — it is white on a
   translucent border and would vanish up here. */
.btn-ghost {
  --btn-bg: transparent;
  --btn-fg: var(--dark);
  border-color: var(--dark);
}

.btn-ghost:hover {
  background: var(--dark);
  color: var(--white);
  box-shadow: var(--shadow-2);
}

.btn-ghost-light {
  --btn-bg: transparent;
  --btn-fg: var(--white);
  border-color: rgba(var(--cream-rgb), 0.45);
  backdrop-filter: blur(6px);
}

.btn-ghost-light:hover {
  background: rgba(var(--cream-rgb), 0.12);
  border-color: var(--cream);
  color: var(--white);
  box-shadow: none;
}

.btn-sm {
  min-height: 42px;
  padding: 0.45em 1.1em;
  font-size: var(--step--1);
}

.btn-block {
  width: 100%;
}

.text-link {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--step--1);
  color: var(--accent-deep);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  letter-spacing: 0.01em;
}

.text-link::after {
  content: "→";
  transition: transform var(--dur) var(--ease-out);
}

.text-link:hover {
  color: var(--dark);
}

.text-link:hover::after {
  transform: translateX(4px);
}

.section-dark .text-link {
  color: var(--accent-on-dark);
}

.section-dark .text-link:hover {
  color: var(--white);
}

/* ------------------------------------------------------------- header ----
   Light surface, built entirely from the --header-* tokens at the top of this
   file. Nothing in here reads --dark or --ink-on-dark any more; if you need a
   new colour on the header, add a --header-* token rather than reaching for a
   dark-context one, because the dark tokens are shared with the hero, drawer,
   footer and photo scrims. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--header-bg);
  color: var(--header-fg);
  /* The hero below is dark, so this hairline does nothing there — it is for the
     interior pages whose first section is cream or white, where the header
     would otherwise dissolve into the page before it is scrolled. */
  border-bottom: 1px solid var(--header-line);
  transition: box-shadow var(--dur) var(--ease-out);
}

/* 0.28 was tuned to be visible falling out of a near-black bar. Under a white
   one the same alpha reads as a grey smudge, so the stuck shadow drops to 0.12
   and tightens its blur. */
.site-header.is-stuck {
  box-shadow: 0 6px 20px rgba(var(--dark-rgb), 0.12);
}

/* Desktop only. On a 390px screen the address, hours, both market links and the
   email wrapped to four lines of small grey text above the logo — pushing the
   headline and the primary CTA off the first screen. All of it is in the footer,
   and the phone is on the sticky action bar. */
/* Collapsed rather than hidden when the header sticks — see --header-h. It is an
   address, opening hours and an email; none of it is worth 37px of a scrolled
   viewport, and all of it is in the footer. max-height works here because the
   strip is a single line of known height, so there is no auto-height to guess. */
.header-utility {
  display: none;
  overflow: hidden;
  max-height: 60px;
  transition:
    max-height var(--dur) var(--ease-out),
    opacity var(--dur) var(--ease-out);
  /* A cream tint rather than white, so the strip separates from the header bar
     under it without needing a second rule. It is only 1.19:1 off white, which
     is why the ink on it is still checked against the strip and not the bar. */
  background: var(--header-bg-util);
  border-bottom: 1px solid var(--header-line);
  font-size: var(--step--1);
  color: var(--header-fg-muted);
}

@media (min-width: 900px) {
  .header-utility {
    display: block;
  }
}

.header-utility .container {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-md);
  align-items: center;
  justify-content: space-between;
  padding-block: 8px;
}

/* 6.07:1 Albany, 7.11:1 Charlotte on the strip — the email link is small print,
   so it gets the muted ink at rest and the deep accent (6.60 / 7.70:1) on hover
   rather than the brand accent, which is tuned for larger runs. */
.header-utility a {
  color: var(--header-fg-muted);
  text-decoration: none;
}

.header-utility a:hover {
  color: var(--header-accent);
}

.header-utility .util-markets {
  display: flex;
  gap: var(--space-sm);
  align-items: center;
  flex: none;
}

/* --- what yields when the strip runs out of room ---------------------------
   The strip gained a third item and stopped being a comfortable single line.
   Measured at 1280 with the container at its 1180px cap: address and hours 393,
   the estimates line plus the email 434, the selector 341, two gaps 48 — 1216
   into 1180. It wrapped, and `max-height: 60px` with `overflow: hidden` above
   turned that wrap into a selector sliced in half. A wrap here is not a
   graceful degradation, it is a missing control.

   Wrapping is therefore off, and the strip drops content in priority order
   instead. The order is not arbitrary — it is what the rest of this file
   already says about each item:
     1. "Free estimates, no obligation" goes first. It is marketing filler, and
        the trust strip twenty pixels below the hero carries "Free estimates" as
        a card of its own.
     2. The address and hours go next. The comment on .header-utility already
        notes they are all in the footer, which is why the whole strip is
        droppable below 900px in the first place.
     3. The email link and the selector are what survive. The selector survives
        everything, because it is the only control on the strip and the only
        thing here a visitor can act on.

   The widths below are measured, so they are only as good as the body font the
   measuring was done in. The ellipsis on the address span is the net under
   that: on a platform whose system font runs wider than the one these numbers
   came from, the address truncates and the selector still lands whole.
   Re-measure rather than nudging a threshold by eye. */
@media (min-width: 900px) {
  .header-utility .container {
    flex-wrap: nowrap;
  }

  .header-utility .container > span:first-child {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* No max-width on this one, and that is the point. .container caps at 1180px,
     so 1216 does not fit at 1280 and it does not fit at 2560 either — a wider
     window buys nothing. The estimates line and the selector can never share
     this strip, at any size. */
  .header-utility .util-markets > span {
    display: none;
  }
}

/* 989 into a 989 container is zero slack, so the label goes at the same 1060
   the nav list and the header buttons already break at. The nav still names
   itself to assistive tech through aria-label="Choose your location". */
@media (min-width: 900px) and (max-width: 1059px) {
  .market-nav-label {
    display: none;
  }
}

/* 901 into an 880 container is over, and the address is the last thing left
   that is repeated elsewhere. */
@media (min-width: 900px) and (max-width: 959px) {
  .header-utility .container > span:first-child {
    display: none;
  }
}

/* --- the market selector ---------------------------------------------------
   Ray, 2026-08-09: "have an easy navigation system at the top, or maybe in the
   header, where people can select between Albany and Charlotte, colour-coded."

   The colour coding is free and that is the whole design. Every rule below is
   written once, in --accent / --accent-deep / --accent-rgb, and the Charlotte
   pill carries data-market="charlotte" — the bare attribute selector near the
   top of this file, the same one the homepage market cards use. The pill
   re-resolves the brand tokens against itself and comes out navy with no second
   set of rules. There is deliberately no [data-market="charlotte"]
   .market-nav-pill anywhere and there must never be one: such a rule is proof
   that a hex got hard-coded instead of tokenised.

   The two-class selectors are not decoration either. `.header-utility a` above
   is (0,1,1) and paints every link on the strip muted grey; a bare
   `.market-nav-pill` is (0,1,0) and loses to it. `.market-nav .market-nav-pill`
   is (0,2,0), which wins on class count without tying the pill to the strip it
   happens to sit in today.

   Sampled off the rendered page rather than computed from the tokens, Albany /
   Charlotte, each against the ground it actually sits on:
     the "Your area" label on the #edebe7 strip     6.07:1 / 7.12:1
     resting pill ink, --accent-deep on the white
       pill — note white, not the strip             7.86:1 / 9.17:1
     the dot, --accent on the white pill; a
       graphic, so it owes 3:1                      5.74:1 / 6.81:1
     selected pill, white on --accent               5.74:1 / 6.81:1
     the white dot on that fill                     5.74:1 / 6.81:1
     selected pill hovered, white on --accent-deep  7.86:1 / 9.17:1
   Both markets appear in both columns because both pills are on every page —
   the figures above are for the pill, not for the page it is on. Nothing here
   leans on the large-text exemption; the pills are 0.76rem. */
.market-nav {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: none;
}

.market-nav-label {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 0.64rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--header-fg-muted);
  white-space: nowrap;
}

.market-nav ul {
  display: flex;
  align-items: center;
  gap: 6px;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* A white chip on the cream strip, outlined in the market's own accent. White
   rather than transparent because the strip is 1.19:1 off white — an outlined
   pill that shares its ground with the bar reads as a hairline box, and one
   that sits on its own white field reads as a control. */
.market-nav .market-nav-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 30px;
  padding: 4px 13px;
  border-radius: var(--radius-pill);
  border: 1.5px solid rgba(var(--accent-rgb), 0.45);
  background: var(--white);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 0.76rem;
  line-height: 1.2;
  letter-spacing: 0.01em;
  color: var(--accent-deep);
  text-decoration: none;
  white-space: nowrap;
  transition:
    background-color var(--dur) var(--ease-out),
    border-color var(--dur) var(--ease-out),
    color var(--dur) var(--ease-out),
    box-shadow var(--dur) var(--ease-out),
    translate var(--dur) var(--ease-out);
}

/* The colour chip. --accent, not currentColor: the resting ink is --accent-deep,
   which is tuned for reading small type and is a full step darker than the
   brand colour a visitor is being asked to recognise. The ring is the same
   accent at low alpha, so the dot reads as a swatch with a halo rather than as
   a full stop. */
.market-nav-pill .dot {
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 2.5px rgba(var(--accent-rgb), 0.2);
  transition:
    background-color var(--dur) var(--ease-out),
    box-shadow var(--dur) var(--ease-out),
    scale var(--dur) var(--ease-spring);
}

/* Filled, not merely outlined. aria-current="true" marks the market of the page
   you are on — note "true" and not "page", because on /albany-ny/local-moving/
   the Albany pill is the current market and not the current page — and a
   selected state that only thickened a border would be a shade difference on a
   1.19:1 ground. White on the accent is the same pairing every button on this
   site uses. The dot goes white with it: the whole pill is the market colour
   now, so a red dot on red would only be a hole in it. */
.market-nav .market-nav-pill[aria-current] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--white);
  box-shadow: 0 2px 6px rgba(var(--accent-rgb), 0.28);
}

/* No halo on the selected pill, and that is measured rather than tidied. The
   ring is what gives the resting dot presence against a white pill; over the
   accent fill the same ring is a 45% lightened accent sitting between a white
   dot and its ground, and white against THAT measured 2.96:1 Albany and 2.68:1
   Charlotte — under the 3:1 a non-text graphic owes its adjacent colour. The
   dot does not need a field here because the whole pill is one. White straight
   onto the accent is 5.74:1 / 6.81:1. */
.market-nav .market-nav-pill[aria-current] .dot {
  background: var(--white);
  box-shadow: none;
}

.market-nav .market-nav-pill:hover,
.market-nav .market-nav-pill:focus-visible {
  border-color: var(--accent);
  background: rgba(var(--accent-rgb), 0.09);
  color: var(--accent-deep);
  translate: 0 -1px;
}

.market-nav .market-nav-pill:hover .dot,
.market-nav .market-nav-pill:focus-visible .dot {
  scale: 1.2;
  box-shadow: 0 0 0 3.5px rgba(var(--accent-rgb), 0.24);
}

/* The selected pill darkens instead of lightening — it is already filled, so
   the only move left is inward. --accent-lift was tried first and white on it
   measures 4.51:1, which is a rounding error away from failing; --accent-deep
   is 7.86:1 Albany and 9.17:1 Charlotte. */
.market-nav .market-nav-pill[aria-current]:hover,
.market-nav .market-nav-pill[aria-current]:focus-visible {
  background: var(--accent-deep);
  border-color: var(--accent-deep);
  color: var(--white);
}

/* Same reason as the resting selected dot: no light ring over a filled pill.
   The dot still answers the hover through `scale` inherited from the rule
   above, so the acknowledgement is there without a halo that would put white
   against a near-white. */
.market-nav .market-nav-pill[aria-current]:hover .dot,
.market-nav .market-nav-pill[aria-current]:focus-visible .dot {
  background: var(--white);
  box-shadow: none;
}

/* Same trap as .market-card-cta: the global focus ring is 3px of --accent, and
   on a pill already filled with --accent that is an invisible ring. The header
   ink is 18.58:1 Albany / 17.34:1 Charlotte against the fill it hugs. */
.market-nav .market-nav-pill[aria-current]:focus-visible {
  outline-color: var(--header-fg);
}


/* 92px logo + 12px top + 12px bottom is 116px, so --header-h still sets the bar
   height rather than the padding doing it — which is what keeps
   scroll-padding-top honest. */
.header-bar {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  min-height: var(--header-h);
  padding-block: 12px;
  transition: min-height var(--dur) var(--ease-out);
}

/* --- the shrink, in one place ---
   Three properties move together and they are all cheap: a min-height, a
   max-height and the logo's own height. No layout thrash on scroll either —
   app.js flips .is-stuck once, off an IntersectionObserver on a 1px sentinel,
   not off a scroll handler. */
.site-header.is-stuck .header-bar {
  min-height: var(--header-h-stuck);
}

.site-header.is-stuck .header-utility {
  max-height: 0;
  opacity: 0;
}

.site-header.is-stuck .brand-full img {
  height: var(--brand-h-stuck);
}

/* --- the selector on a phone ----------------------------------------------
   The utility strip is display: none below 900px, and the reason is written
   above it: an address, opening hours and an email wrapped to four lines of
   small grey text and pushed the headline off the first screen. All of that is
   in the footer. None of it applies to a market selector, which is not in the
   footer, is not repeated anywhere else, and is the one control on the strip
   the client actually asked for.

   So the strip comes back below 900px carrying ONLY the selector. The two
   information spans are the direct children of the container, and they are the
   part that was too heavy for a phone; the <nav> is the part that is not. The
   drawer was the other candidate and it renders nothing for this — its market
   entries are a <details> of every service in each city, which is a menu, not a
   chooser — and a selector a visitor has to open a menu to find is not "easy
   navigation at the top".

   It also does not collapse when the header sticks, which is the one place this
   deliberately parts company with the desktop. Up there the collapse is free:
   what disappears is duplicated in the footer. Down here the collapse would
   take the selector with it, and a market switcher you can only reach by
   scrolling back to the top of the page is not a switcher. The cost is honest
   and measured — the strip is 45px, so the stuck header goes from 68px to 116px
   on a phone — and it is paid back by the thing being reachable from anywhere
   on a 51-page two-market site. scroll-padding-top is re-stated below to match,
   since the token it is built from only knows about the bar.

   This block has to sit AFTER `.site-header.is-stuck .header-utility` above,
   not next to the rest of the selector's rules. Both selectors are (0,3,0), so
   the collapse and the override are decided on source order alone; written a
   hundred lines earlier it looked correct, passed review, and the strip still
   vanished on scroll on every phone. */
@media (max-width: 899px) {
  .header-utility {
    display: block;
  }

  .header-utility .container > span {
    display: none;
  }

  .header-utility .container {
    justify-content: center;
    padding-block: 6px;
  }

  .site-header.is-stuck .header-utility {
    max-height: 60px;
    opacity: 1;
  }

  .market-nav {
    flex-wrap: wrap;
    justify-content: center;
    row-gap: 4px;
  }

  /* 32px, not the desktop 30px: this is now a thumb target rather than a mouse
     one. Both clear the 24px floor the layout gate enforces; 32px with a 6px
     strip padding is what keeps the whole row inside the 60px max-height the
     collapse transition animates against. */
  .market-nav .market-nav-pill {
    min-height: 32px;
    font-size: 0.78rem;
  }

  html {
    scroll-padding-top: calc(var(--header-h-stuck) + 44px + 16px);
  }
}

/* 320px is the width where "Your area" plus two city pills stops fitting on one
   line, and the row wraps to two — 4px over the 60px the strip can hold, which
   the overflow would eat. The nav carries aria-label="Choose your location", so
   dropping the visible label costs a sighted visitor a hint and costs a screen
   reader nothing; two city names behind coloured dots still read as a choice. */
@media (max-width: 380px) {
  .market-nav-label {
    display: none;
  }
}

/* .brand is emitted twice: in the header and in the drawer head. Not in the
   footer — that column is .footer-brand and it emits .footer-logo, never
   .brand. The drawer panel is still dark, so the base rule KEEPS the on-dark
   colouring and the header re-states its own below. Do not "simplify" this into
   one light colour — white-on-white in the drawer head is invisible and nothing
   in the QA gate would catch it. */
.brand {
  display: flex;
  align-items: center;
  gap: 14px;
  text-decoration: none;
  color: var(--white);
  flex: none;
}

/* The chip. Still used in the drawer head, whose panel is --dark: the full badge
   cannot go there, because Albany's skyline is drawn in black and Charlotte's
   lettering in navy, and both disappear against it. generate.mjs carries
   matching width/height attributes so the box is reserved before decode. */
.brand img {
  width: 68px;
  height: 68px;
  border-radius: 16px;
  transition: transform var(--dur) var(--ease-spring);
}

.brand:hover img {
  transform: rotate(-6deg) scale(1.06);
}

/* --- the header's own brand: the original approved market logo ---
   Ray, 2026-08-09. The badge is the whole lockup, so there is no wordmark
   beside it and no gap to close. Height-locked, width auto: Albany is a
   900x915 circle and Charlotte a 900x1000 free-form mark, and lining them up
   on height is the only way the two markets sit at the same optical weight.

   No border-radius and no rotate-on-hover. Both were written for a square chip;
   a rounded corner clips Charlotte's mascot where he breaks the bounding box,
   and tilting a full logo lockup reads as a glitch rather than as life. */
.brand-full {
  gap: 0;
}

.brand-full picture {
  display: block;
  line-height: 0;
}

.brand-full img {
  width: auto;
  height: var(--brand-h);
  border-radius: 0;
  transition:
    height var(--dur) var(--ease-out),
    transform var(--dur) var(--ease-spring);
}

.brand-full:hover img {
  transform: scale(1.03);
}

.brand-word {
  font-family: var(--font-display);
  line-height: 1;
  display: grid;
  gap: 4px;
}

.brand-word b {
  font-weight: 900;
  font-size: 1.14rem;
  letter-spacing: 0.005em;
  text-transform: uppercase;
}

.brand-word span {
  font-weight: 700;
  font-size: 0.68rem;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--accent-on-dark);
}

/* --- header-only brand colouring ---
   #ff5a5f is the on-dark accent: 6.09:1 on the near-black it was drawn for, but
   only 2.83:1 on white, which is why "MOVERS" cannot simply inherit it here.
   --header-accent is the deep accent instead — 7.86:1 Albany, 9.17:1 Charlotte
   on the white bar. The wordmark itself takes full ink at 18.58 / 17.34:1. */
.site-header .brand {
  color: var(--header-fg);
}

.site-header .brand-word span {
  color: var(--header-accent);
}

.primary-nav {
  margin-left: auto;
}

.nav-list {
  display: none;
  list-style: none;
  margin: 0;
  padding: 0;
  gap: 2px;
}

@media (min-width: 1060px) {
  .nav-list {
    display: flex;
  }
}

.nav-item {
  position: relative;
}

.nav-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 13px;
  border-radius: var(--radius-sm);
  position: relative;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.92rem;
  color: var(--header-fg);
  text-decoration: none;
  background: none;
  border: 0;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color var(--dur) var(--ease-out), color var(--dur) var(--ease-out);
}

/* The hover used to be --dark-soft, a raised near-black. On a white bar the
   equivalent move is a barely-there tint of the same brand dark: 6% over white
   lands near #f1f1f1, which still leaves the ink at 16.4:1. */
.nav-link:hover,
.nav-item:focus-within > .nav-link {
  background: var(--header-tint);
  color: var(--header-fg);
}

/* Current page: the deep accent, not the on-dark one — same 2.83:1 trap as the
   wordmark. 7.86:1 Albany, 9.17:1 Charlotte. */
.nav-link[aria-current="page"] {
  color: var(--header-accent);
}

/* A rule that draws itself in under the label. The tint alone was a very quiet
   hover for a nav that is now the only desktop navigation on the page, and the
   current-page item was carrying its whole state in a colour change, which is
   the one cue a visitor with a colour vision deficiency does not get. The rule
   is out of flow, so an inline-flex link does not gain a third flex item, and it
   scales rather than growing a width — no layout, no reflow, and it inherits
   whatever the market has already resolved --header-accent to. */
.nav-link::after {
  content: "";
  position: absolute;
  left: 13px;
  right: 13px;
  bottom: 6px;
  height: 2px;
  border-radius: 2px;
  background: var(--header-accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur) var(--ease-out);
}

.nav-link:hover::after,
.nav-item:focus-within > .nav-link::after,
.nav-link[aria-current="page"]::after {
  transform: scaleX(1);
}

.nav-link .caret {
  width: 9px;
  height: 9px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--dur) var(--ease-out);
  opacity: 0.75;
}

.nav-item:hover .caret,
.nav-item:focus-within .caret {
  transform: rotate(45deg) translate(0, 0);
}

.mega {
  position: absolute;
  top: calc(100% + 6px);
  left: 50%;
  translate: -50% 0;
  min-width: 560px;
  background: var(--white);
  color: var(--ink);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-3);
  padding: var(--space-md);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transition:
    opacity var(--dur) var(--ease-out),
    transform var(--dur) var(--ease-out),
    visibility var(--dur);
}

.nav-item:hover .mega,
.nav-item:focus-within .mega {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

.mega::before {
  content: "";
  position: absolute;
  inset: -8px 0 auto;
  height: 14px;
}

.mega-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-md);
}

.mega-col h4 {
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent-deep);
  margin-bottom: 10px;
  padding-bottom: 8px;
  border-bottom: 2px solid var(--cream-deep);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.mega-col a {
  display: block;
  padding: 7px 10px;
  margin-left: -10px;
  border-radius: var(--radius-sm);
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
  transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}

.mega-col a:hover {
  background: var(--cream);
  color: var(--accent-deep);
}

/* Locations menu. Each market heads its own column and is itself a link to that
   market's hub page, so "Albany, NY" behaves like a location entry rather than a
   label sitting above one. */
/* The status chip sits beside the city name, so this menu needs more room than the
   service menu before "Albany, NY" starts wrapping onto two lines. */
.mega-locations {
  min-width: 680px;
}

.mega-col-location {
  padding-right: var(--space-sm);
}

.mega-head {
  display: flex !important;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 4px 10px 8px !important;
  margin-bottom: 4px;
  border-bottom: 2px solid var(--cream-deep);
  border-radius: 0 !important;
}

.mega-head:hover {
  background: transparent !important;
}

.mega-head-city {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: 1rem;
  color: var(--dark);
  white-space: nowrap;
}

.mega-head:hover .mega-head-city {
  color: var(--accent-deep);
}

.mega-note {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-muted);
  padding: 0 0 6px;
  margin: 0;
}

.mega-foot {
  display: block;
  margin-top: var(--space-sm);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--cream-deep);
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--accent-deep);
  text-align: center;
  text-decoration: none;
}

.mega-foot:hover {
  text-decoration: underline;
}

.tag-live,
.tag-soon {
  font-family: var(--font-display);
  font-size: 0.58rem;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: var(--radius-pill);
  white-space: nowrap;
}

/* White on the accent, for the same reason the buttons carry white: the accent
   is a mid-dark red/blue and near-black text on it is only 3.24:1. */
.tag-live {
  background: var(--accent);
  color: var(--white);
}

/* Live markets get a breathing dot — the "we're open" cue. The soon tag stays
   still: pulsing a market that has not launched would promise the wrong thing. */
.tag-live::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 6px;
  border-radius: 50%;
  background: currentColor;
  vertical-align: 1px;
  animation: tag-pulse 2.4s var(--ease-in-out) infinite;
}

@keyframes tag-pulse {
  0%,
  100% {
    opacity: 1;
    transform: scale(1);
  }
  50% {
    opacity: 0.4;
    transform: scale(0.75);
  }
}

/* navy-soft rather than navy: this badge sits on the white mega-menu AND on the
   navy page heroes, and a flat navy chip disappears on the second one. */
.tag-soon {
  background: var(--dark-soft);
  color: var(--cream);
  border: 1px solid var(--dark-line);
}

.page-hero .tag-soon {
  background: rgba(var(--cream-rgb), 0.12);
  border-color: rgba(var(--cream-rgb), 0.32);
}

.header-actions {
  display: none;
  gap: 10px;
  flex: none;
}

@media (min-width: 1060px) {
  .header-actions {
    display: flex;
  }
}

/* 1060 is where the nav list and the actions both switch on, and between there
   and about 1200 they do not both fit: at 1060 the bar wants 1128px inside a
   1020px container and puts ~108px of horizontal scroll on the whole document.
   qa:layout sweeps 375/768/1280 and steps straight over the band, which is why
   this survived.

   The phone button is what gives way rather than the nav or the quote CTA. The
   utility strip is visible from 900px up, and it already carries the number, so
   nothing is actually lost in this range — whereas dropping the nav back to a
   hamburger at 1100px, or dropping "Free Quote", both cost something real. */
@media (min-width: 1060px) and (max-width: 1199px) {
  .header-actions .btn-ghost {
    display: none;
  }
}

.menu-toggle {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-height: 46px;
  padding: 0 16px;
  border-radius: var(--radius-pill);
  /* --dark-line is a lifted near-black, invisible on the white bar. The muted
     ink is the lightest tone here that still clears 3:1 for a control boundary
     (it is 7.23:1 Albany / 8.47:1 Charlotte, so there is plenty of headroom)
     and it matches the utility strip's ink, which keeps the two rows related. */
  border: 2px solid var(--header-fg-muted);
  background: transparent;
  color: var(--header-fg);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 0.85rem;
  cursor: pointer;
}

@media (min-width: 1060px) {
  .menu-toggle {
    display: none;
  }
}

.menu-toggle .bars {
  width: 18px;
  height: 12px;
  position: relative;
}

/* Re-checked against the white bar rather than the near-black one: the brand
   accent is 5.74:1 Albany and 6.81:1 Charlotte on white, well past the 3:1 a
   graphical control part needs. It stays --accent. */
.menu-toggle .bars i {
  position: absolute;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--accent);
  border-radius: 2px;
  transition: transform var(--dur) var(--ease-out), opacity var(--dur) var(--ease-out);
}

/* Below 900px the utility strip is gone, so the whole sticky header is just the
   bar. 92px of logo there is most of a phone's first screen before the headline
   starts, so the resting size comes down and the shrink stays proportional. */
@media (max-width: 899px) {
  :root {
    --header-h: 92px;
    --header-h-stuck: 68px;
    --brand-h: 68px;
    --brand-h-stuck: 46px;
  }
}

/* This block used to shrink the brand again below 400px, because the old lockup
   was a 54px chip PLUS a two-line wordmark and that, next to a labelled Menu
   button, tipped a 320px screen into horizontal scroll. The wordmark is gone —
   the logo is the lockup now — so at 68px the bar carries 67px of logo and a
   52px unlabelled button inside 296px of container with room to spare. Only the
   button's label still has to go. Measured at 320, 375 and 390. */
@media (max-width: 400px) {
  .container,
  .container-narrow {
    width: min(100% - 1.5rem, var(--wrap));
  }

  /* Drawer head only — .brand-full sizes off --brand-h and must not be caught
     by this, or the logo is forced back into a 54px square. */
  .drawer .brand img {
    width: 54px;
    height: 54px;
    border-radius: 13px;
  }

  .brand-word b {
    font-size: 0.9rem;
  }

  .brand-word span {
    font-size: 0.56rem;
    letter-spacing: 0.18em;
  }

  .menu-toggle {
    padding: 0 14px;
    gap: 0;
  }

  .menu-label {
    display: none;
  }
}

.menu-toggle .bars i:nth-child(1) { top: 0; }
.menu-toggle .bars i:nth-child(2) { top: 5px; }
.menu-toggle .bars i:nth-child(3) { top: 10px; }

.menu-toggle[aria-expanded="true"] .bars i:nth-child(1) { transform: translateY(5px) rotate(45deg); }
.menu-toggle[aria-expanded="true"] .bars i:nth-child(2) { opacity: 0; }
.menu-toggle[aria-expanded="true"] .bars i:nth-child(3) { transform: translateY(-5px) rotate(-45deg); }

/* ------------------------------------------------------------- drawer ---- */
.drawer {
  position: fixed;
  inset: 0;
  z-index: 120;
  visibility: hidden;
}

.drawer[data-open="true"] {
  visibility: visible;
}

.drawer-scrim {
  position: absolute;
  inset: 0;
  background: rgba(var(--dark-rgb), 0.55);
  opacity: 0;
  transition: opacity var(--dur) var(--ease-out);
  border: 0;
  width: 100%;
  cursor: pointer;
}

.drawer[data-open="true"] .drawer-scrim {
  opacity: 1;
}

.drawer-panel {
  position: absolute;
  inset: 0 0 0 auto;
  width: min(420px, 100%);
  background: var(--dark);
  color: var(--ink-on-dark);
  padding: var(--space-md) var(--space-md) 120px;
  overflow-y: auto;
  transform: translateX(100%);
  transition: transform var(--dur-slow) var(--ease-out);
}

.drawer[data-open="true"] .drawer-panel {
  transform: translateX(0);
}

.drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-bottom: var(--space-md);
  border-bottom: 1px solid var(--dark-line);
  margin-bottom: var(--space-sm);
}

.drawer-close {
  min-height: 44px;
  min-width: 44px;
  border-radius: var(--radius-pill);
  border: 2px solid var(--dark-line);
  background: transparent;
  color: var(--white);
  font-family: var(--font-display);
  font-weight: 800;
  cursor: pointer;
  padding-inline: 16px;
}

.drawer-close:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.drawer-panel a {
  color: var(--ink-on-dark);
  text-decoration: none;
}

.drawer-link {
  display: block;
  padding: 13px 4px;
  border-bottom: 1px solid var(--dark-line);
  font-family: var(--font-display);
  font-weight: 700;
}

.drawer-link:hover {
  color: var(--accent-on-dark);
}

.drawer details {
  border-bottom: 1px solid var(--dark-line);
}

.drawer summary {
  padding: 13px 4px;
  font-family: var(--font-display);
  font-weight: 700;
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.drawer summary::-webkit-details-marker {
  display: none;
}

.drawer summary::after {
  content: "+";
  font-size: 1.35rem;
  line-height: 1;
  color: var(--accent-on-dark);
  transition: transform var(--dur) var(--ease-out);
}

.drawer details[open] summary::after {
  transform: rotate(45deg);
}

.drawer details a {
  display: block;
  padding: 9px 4px 9px 18px;
  font-size: 0.92rem;
  color: var(--ink-muted-on-dark);
}

.drawer details a:hover {
  color: var(--accent-on-dark);
}

.drawer-cta {
  display: grid;
  gap: 10px;
  margin-top: var(--space-lg);
}

/* --------------------------------------------------------------- hero ---- */
.hero {
  position: relative;
  background: var(--dark);
  color: var(--ink-on-dark);
  overflow: hidden;
  isolation: isolate;
}

.hero::before {
  content: "";
  position: absolute;
  z-index: -2;
  inset: auto -10% -35% 45%;
  aspect-ratio: 1;
  background: radial-gradient(circle at center, rgba(var(--accent-rgb), 0.3), transparent 62%);
  filter: blur(10px);
}

.hero::after {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  background-image: repeating-linear-gradient(
    -45deg,
    rgba(var(--cream-rgb), 0.028) 0 12px,
    transparent 12px 34px
  );
  pointer-events: none;
}

.hero-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-xl);
  align-items: center;
  padding-block: var(--space-2xl) 0;
}

/* The market-hub photo hero is a single text column — without the homepage's
   badge column filling the right side, zero bottom padding parks the CTA
   buttons flush on the hero's edge (client feedback 2026-08-07). The homepage
   keeps its tight bottom so the CTA stays above the fold on short laptops. */
.hero-photo-hub .hero-grid {
  padding-block: var(--space-2xl);
}

/* Kept tight on purpose: on a 720px-tall laptop the primary CTA has to sit
   above the fold. Anything taller here pushes "Get a Free Quote" out of view.

   Ray, 2026-08-09: the hero is "a little compact on desktop". The height it was
   missing cannot come from padding — padding is a constant, so any amount that
   helps a 1080px screen is the same amount subtracted from the fold on a 720px
   one, which is the trap the paragraph above is warning about.

   A min-height in viewport units instead. It is a floor, not a size: the hero
   content measures 601px at 1440 wide, so at 72dvh the floor is 518px on a
   720-tall screen and does nothing at all there, 648px at 900 and 778px at 1080
   where there is room to spend. The clamp caps it at 780px so a 27" display
   does not open a canyon, and floors it at 520px so a short-but-wide window
   (a split screen, a projector) never crushes it below the current height.

   The extra space centres rather than piling up under the copy: the grid
   already carries align-items: center and the single implicit row stretches, so
   the block grows away from the CTA in both directions at half rate. Measured
   at 1440 wide with "Get a Free Quote" against the fold — 657px of 720, 681 of
   900, 746 of 1080. dvh is stated twice on purpose; the vh line is what a
   browser without dynamic viewport units falls back to, and on desktop the two
   resolve to the same number anyway. */
@media (min-width: 940px) {
  .hero-grid {
    grid-template-columns: 1.08fr 0.92fr;
    padding-block: var(--space-xl) 0;
    gap: var(--space-lg);
    min-height: clamp(520px, 72vh, 780px);
    min-height: clamp(520px, 72dvh, 780px);
  }
}

.hero h1 {
  color: var(--white);
}

.hero h1 .accent {
  color: var(--accent-on-dark);
  display: inline-block;
}

.hero-sub {
  font-size: var(--step-1);
  color: var(--ink-muted-on-dark);
  max-width: 52ch;
  margin-top: var(--space-md);
}

/* Over a photograph the muted token has no headroom left. --ink-muted-on-dark is
   calibrated against a flat --dark panel; across a skyline it measured 4.51:1 on
   Albany and 3.99:1 on Charlotte, one of them already failing. A photo hero
   takes the full-strength on-dark ink instead — the size and weight difference
   from the h1 is what makes it read as secondary, not the greying. */
.hero-photo .hero-sub {
  color: var(--ink-on-dark);
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-lg);
}

/* Status chip on the two market landing pages. It sits between the breadcrumb
   and the eyebrow, so it needs its own bottom margin — the breadcrumb's is
   tuned for a heading following it directly. */
.hero-tag {
  margin-bottom: var(--space-sm);
}

/* The two markets sit here, under the CTAs, at supporting-text scale. They used to
   be the H1; the business is a moving company first and a two-market company
   second, and the hero should read in that order. */
.hero-locations {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-top: var(--space-lg);
  padding-top: var(--space-md);
  border-top: 1px solid rgba(var(--cream-rgb), 0.14);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-muted-on-dark);
}

.hero-locations a {
  color: var(--white);
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 2px solid rgba(var(--accent-rgb), 0.55);
  transition: border-color var(--dur) var(--ease-out), color var(--dur) var(--ease-out);
}

.hero-locations a:hover {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

.hero-media {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: flex-end;
  min-height: 320px;
}

.hero-badge {
  position: absolute;
  background: var(--white);
  color: var(--dark);
  border-radius: var(--radius);
  padding: 12px 16px;
  box-shadow: var(--shadow-3);
  display: grid;
  gap: 2px;
  animation: badge-float 5s var(--ease-in-out) infinite;
}

.hero-badge strong {
  font-family: var(--font-display);
  font-size: 1.28rem;
  font-weight: 900;
  line-height: 1;
}

.hero-badge span {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.hero-badge-rating {
  left: 0;
  bottom: 22%;
}

.hero-badge-markets {
  right: 0;
  top: 12%;
  animation-delay: -2.5s;
}

@media (max-width: 600px) {
  .hero-badge-markets {
    top: 2%;
  }
}

@keyframes badge-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-9px); }
}

/* market switcher ---------------------------------------------------------- */
/* Lives on the cream section, so the track is light and the unselected label is
   --ink-muted (7.2:1 on white). The selected pill is navy-on-orange (5.6:1) —
   orange never carries white text anywhere on this site. */

/* The switcher is a filter on the service menu, not the section's headline, so it
   sits behind a small "Showing services for" label rather than standing alone. */
.market-switch-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-bottom: var(--space-lg);
}

.market-switch-label {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.market-switch-row .market-switch {
  margin-bottom: 0;
}

.market-switch {
  display: inline-flex;
  padding: 5px;
  gap: 4px;
  border-radius: var(--radius-pill);
  background: var(--white);
  border: 1px solid var(--cream-deep);
  box-shadow: var(--shadow-1);
  margin-bottom: var(--space-lg);
}

.market-switch button {
  border: 0;
  background: transparent;
  color: var(--ink-muted);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 0.86rem;
  padding: 9px 20px;
  min-height: 42px;
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease-out), color var(--dur) var(--ease-out);
}

.market-switch button:hover {
  background: var(--cream);
  color: var(--dark);
}

.market-switch button[aria-selected="true"] {
  background: var(--accent);
  color: var(--white);
}

.market-switch button[aria-selected="true"]:hover {
  background: var(--accent-lift);
}

.market-panel[hidden] {
  display: none;
}

/* market chooser ------------------------------------------------------------
   Two cards under the homepage hero, one per market, each a door into that
   market's hub. The live site puts a city photograph behind each of these; ours
   ships none. The only city imagery the client owns is mislabelled stock — their
   "Albany" is Shanghai and their "Charlotte" is Lower Manhattan — and this build
   does not ship generated photography either. So the ground is the market's own
   brand gradient and the city is carried by the badge, whose skyline art is
   genuine illustration of the real place.

   Every rule below is written once, in tokens. The Charlotte <article> carries
   data-market="charlotte", and that selector near the top of this file is a bare
   attribute selector rather than an html-scoped one, so the card re-resolves
   --dark*, --accent* and --ink* against itself and comes out navy and carolina
   blue with no second set of rules. There is deliberately no
   .market-card[data-market="charlotte"] colour override here and there should
   never be one: such a rule is proof that something got hard-coded instead of
   tokenised. The corollary is that the Albany card is the untagged one — it
   takes whatever theme the page is under, which is right on the homepage, the
   only page that emits this section and an Albany-default page. */
.market-cards {
  display: grid;
  gap: var(--space-lg);
}

/* 720px, the same breakpoint .location-blocks uses to put the two markets side
   by side further down the homepage. The two blocks are the same idea at
   different weights, so they should break at the same width rather than at two
   near-identical ones. */
@media (min-width: 720px) {
  .market-cards {
    grid-template-columns: 1fr 1fr;
  }
}

/* Grid items stretch, so both cards are already the height of the taller one —
   Charlotte's bullets are shorter than Albany's. The column then pushes the CTA
   down with margin-top: auto so both buttons sit on the same line.

   Radius, padding and shadow match .quote-form, the other full-size dark card
   on the site, so the two read as one family.

   The ground: a linear base from --dark down to --dark-deep for the body of the
   card, an accent wash bleeding in from the top behind the badge, and a weaker
   one lifting the bottom-right corner. The two washes do not meet — the top one
   fades out by ~44% of the card and the bottom one starts at ~71% — so the
   brightest pixel anywhere on the card is the top one at full strength, which is
   what the ratios below are measured against.

     brightest ground (0.28 accent over --dark)   Albany #471214 / Charlotte #092c5a
       --white          #ffffff             15.39:1 / 13.82:1
       --ink-on-dark    #f7f6f4             14.25:1 / 12.80:1
       --accent-on-dark #ff5a5f / #6fb0ec     5.04:1 /  5.99:1
     and at the far end of the card, on --dark-deep (#050505 / #000d24), the same
     three land at 20.38 / 19.39, 18.87 / 17.95 and 6.68 / 8.40:1. Every value on
     the card therefore sits between those two rows, and the floor is the accent
     status line at 5.04:1 Albany.

   This is also why --accent itself appears nowhere as text down here. It is the
   mid-dark brand red/navy and it is 3.24:1 on its own dark ground — the trap
   documented at the top of this file. --accent-on-dark is the one that reads. */
.market-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  padding: clamp(1.5rem, 3vw, 2.25rem);
  border-radius: var(--radius-xl);
  border: 1px solid var(--dark-line);
  color: var(--ink-on-dark);
  text-align: center;
  background:
    radial-gradient(120% 72% at 50% -4%, rgba(var(--accent-rgb), 0.28), rgba(var(--accent-rgb), 0) 66%),
    radial-gradient(90% 55% at 100% 104%, rgba(var(--accent-rgb), 0.12), rgba(var(--accent-rgb), 0) 60%),
    linear-gradient(163deg, var(--dark) 0%, var(--dark-deep) 100%);
  /* The inset hairline is the top edge catching light. Built from --cream-rgb so
     it stays a warm white in both markets. */
  box-shadow: var(--shadow-2), inset 0 1px 0 rgba(var(--cream-rgb), 0.07);
  /* Establishes the stacking context the skyline sits behind. */
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

/* The real skyline of the real city, under the gradient rather than instead of
   it. The gradient above is still doing the contrast work — the photo is texture
   and place, not the ground the text is read against — so it is dialled well
   back and pushed to the top of the card where the sky is, leaving the lower
   half, where the bullets and the button live, essentially untouched.

   The client's own site puts a photo of Shanghai on the Albany card and Lower
   Manhattan on the Charlotte card. These two were verified building by building
   before they were committed; see assets/city-photos/SOURCE.txt. If either is
   ever swapped, verify the replacement the same way. A caption is not evidence.

   Measured with the photo in place: the brightest pixel under the city name is
   #4a1a1c Albany / #0d2f5e Charlotte, so white holds 14.2:1 and 12.9:1 and the
   --accent-on-dark status line holds 4.9:1 and 5.8:1. Still clear of 4.5:1. */
.market-card-photo {
  position: absolute;
  inset: 0 0 auto 0;
  z-index: -1;
  display: block;
  /* 52% and this fade curve are not taste, they are the contrast budget. At 62%
     and 0.5 the photo was still lifting the ground under the status line, and
     --accent-on-dark measured 4.37:1 there on Albany — under the 4.5:1 floor.
     The photo is now spent by the time the status line starts. Re-measure if
     these numbers are ever touched; the status line is the tightest text on the
     card because it is the only one not in white. */
  height: 52%;
  opacity: 0.42;
  -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.42) 48%, rgba(0, 0, 0, 0) 88%);
  mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.42) 48%, rgba(0, 0, 0, 0) 88%);
}

.market-card-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 58%;
}

/* Without mask support the photo would run to a hard horizontal edge across the
   middle of the card. Drop it back far enough that the edge is not readable. */
@supports not ((mask-image: linear-gradient(black, black)) or (-webkit-mask-image: linear-gradient(black, black))) {
  .market-card-photo {
    opacity: 0.26;
  }
}

/* The reveal system owns `transform` AND the whole `transition` shorthand on any
   [data-reveal] element, and `.reveal-ready [data-reveal]` outranks a bare
   `.market-card`. Two consequences, both absorbed here rather than by editing
   the reveal rules: the hover lift uses `translate`, an independent property
   nothing else in this file touches, and the transition is restated one step up
   in specificity carrying the reveal's own opacity/transform entries so the
   entrance still fades and slides exactly as it did. The delays stay on those
   two entries only — the hover must not wait out a --reveal-delay. */
.market-choose .market-cards .market-card {
  transition:
    opacity var(--dur-slow) var(--ease-out) var(--reveal-delay, 0ms),
    transform var(--dur-slow) var(--ease-out) var(--reveal-delay, 0ms),
    translate var(--dur) var(--ease-out),
    box-shadow var(--dur) var(--ease-out),
    border-color var(--dur) var(--ease-out);
}

/* Restrained on purpose. The card is not a link — only the button inside it is —
   so this is the same 4px-and-a-shadow acknowledgement the other cards give,
   with no cursor change and nothing that promises the whole panel is clickable.
   :focus-within carries it for a keyboard, where the only focus stop in the card
   is the CTA. */
.market-card:hover,
.market-card:focus-within {
  translate: 0 -4px;
  box-shadow: var(--shadow-3), inset 0 1px 0 rgba(var(--cream-rgb), 0.1);
  border-color: rgba(var(--accent-rgb), 0.55);
}

/* No border-radius and no border. The artwork already carries its own white
   circular field inside a brand-coloured ring, on a transparent square — a CSS
   circle here would draw a second ring just outside the drawn one. The drop
   shadow is pure black rather than rgba(var(--dark-rgb), …) for the same reason
   .section-dark .mascot-inline is: a navy shadow on the Charlotte navy ground
   would not be visible at all. */
.market-card-badge {
  display: block;
}

.market-card-badge img {
  width: clamp(130px, 12vw, 150px);
  margin-inline: auto;
  filter: drop-shadow(0 14px 22px rgba(0, 0, 0, 0.38));
}

/* --- the badge floats as the card goes past --------------------------------
   The only scroll-DRIVEN animation on the site: not "play this once when the
   element appears", which is what the IntersectionObserver reveal does, but
   "tie this to where the element is in the viewport". The badge drifts about
   22px against the card as the card crosses the screen, which reads as depth —
   the stamp sitting slightly in front of the panel rather than printed on it.

   Chosen for what it cannot break. It is a floating decorative mark on a
   transparent ground, so there is no crop to spoil (the skyline photograph
   below it has a documented framing AND a documented contrast budget, which is
   exactly why the parallax is not on that), no text riding on it, and no
   layout: transform on an absolutely-sized image inside a card that already
   clips.

   The fallback is the absence of the rule. @supports gates it, `both` fill is
   irrelevant to a browser that never starts the animation, and the resting
   state is the base rule above — no transform at all. A browser without
   animation-timeline gets a badge that sits still, which is what shipped
   yesterday. Nothing here is load-bearing, which is the only kind of thing that
   should be riding an API this new.

   animation-duration is deliberately absent: a view() timeline ignores it and
   Chrome warns if one is set. That is also why the reduced-motion block has to
   kill this by name — the global `animation-duration: 0.01ms` down there does
   nothing at all to a scroll-driven animation. */
@supports (animation-timeline: view()) {
  .market-card-badge img {
    animation: badge-parallax linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 70%;
  }

  @keyframes badge-parallax {
    from {
      transform: translate3d(0, 14px, 0);
    }
    to {
      transform: translate3d(0, -8px, 0);
    }
  }
}

/* Uppercase wants tracking opened back up: the shared h1–h4 rule tightens to
   -0.022em, which is right for mixed case and too tight for caps. */
.market-card-city {
  font-family: var(--font-display);
  font-size: var(--step-3);
  font-weight: 900;
  letter-spacing: 0.015em;
  text-transform: uppercase;
  color: var(--white);
  margin-top: var(--space-xs);
}

.market-card-status {
  font-family: var(--font-display);
  font-size: var(--step--1);
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-on-dark);
}

/* Left-aligned inside a centred card, and capped so a long bullet does not run
   the full width of a 540px desktop column and lose the reader on the way back
   to the next line. */
.market-card-points {
  list-style: none;
  padding: 0;
  margin: var(--space-xs) 0 var(--space-sm);
  display: grid;
  gap: 12px;
  text-align: left;
  max-width: 34ch;
  margin-inline: auto;
}

.market-card-points li {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  line-height: 1.45;
  color: var(--ink-on-dark);
}

/* flex: none so a wrapping bullet cannot squeeze the disc into an ellipse, and
   the 1px nudge optically centres a 22px disc on a ~23px first line rather than
   letting it ride high. White on --accent is 5.74:1 Albany / 6.81:1 Charlotte,
   the same pairing the buttons use. */
.market-card-points .tick {
  flex: none;
  width: 22px;
  height: 22px;
  margin-top: 1px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--white);
  display: grid;
  place-items: center;
}

/* .btn.btn-primary.btn-block already gives the fill, the white label, the pill,
   the 50px min-height and the full width, so the only thing genuinely missing is
   the push to the bottom of the column that keeps both cards' buttons on one
   line. */
.market-card-cta {
  margin-top: auto;
}

/* The global focus ring is 3px of --accent, which is exactly the button's own
   fill — an invisible ring. White reads against both: 5.74:1 / 6.81:1 on the
   button it hugs, 15.39:1 / 13.82:1 on the card ground behind it. */
.market-card-cta:focus-visible {
  outline-color: var(--white);
  outline-offset: 4px;
}

/* -------------------------------------------------------- trust strip ----
   Ray, 2026-08-09: "the free estimates and the five-star reviews are not even
   coloured, it just looks bland." He was right, and the reason was structural
   rather than decorative. The strip sits in the one place on the homepage that
   has no ground of its own — a photo hero above it, the dark market cards below
   — and it was a white band holding three cream squares. It read as the gap
   between two sections rather than as content.

   Three things fix it, and they are deliberately three rather than six:
     1. The band gets its own surface. A warm cream ramp carrying a weak accent
        blush off the top edge, so it hands off from the dark hero instead of
        cutting a white slot between two rich sections.
     2. Each claim gets a card. Cards OR separators, not both — a rule per item
        plus a border per item fences everything twice and reads as a table.
     3. The icon holder becomes a real object: 54px, circular, tinted from the
        brand accent rather than flat cream, with the glyph in --accent-deep.

   Measured off the rendered page (Albany / Charlotte), each against the worst
   pixel of the ground it actually sits on rather than against the token:
     strong, --ink on the white card            18.58:1 / 17.34:1
     sub, --ink-muted on the white card          7.23:1 /  8.47:1
     glyph, --accent-deep on the deepest tint
       inside the disc, #f4cbcc / #cedbe9        5.34:1 /  6.52:1
     the same glyph on the hover disc            4.58:1 /  5.66:1
   The glyph only owes 3:1 as a non-text graphic; it clears 4.5:1 anyway, at
   rest and on hover, in both markets. */
.trust-strip {
  background:
    linear-gradient(180deg, rgba(var(--accent-rgb), 0.07) 0%, rgba(var(--accent-rgb), 0) 64%),
    linear-gradient(180deg, var(--cream-deep) 0%, var(--cream) 100%);
  border-bottom: 1px solid rgba(var(--dark-rgb), 0.1);
}

/* 360px is not a round number, it is the width at which the stars item still
   fits its five glyphs and its copy on one line. Below that the item stacks,
   and the point of the floor is that when it stacks its two neighbours stack
   with it — auto-fit at 260px produced a band around 1000px wide where the
   stars card had dropped to two rows and the other two had not, which reads as
   a mistake. At this floor the grid steps 3 -> 2 -> 1 with all three items in
   the same shape the whole way down.

   Re-measured 2026-08-09 with the chip removed. The stars row went from a 135px
   pill to 113px of bare glyphs, so the floor could in principle come down — it
   does not, because 360 is now the width at which the stars item needs 113 + 16
   of gap + the copy's 150px flex basis + 48 of padding = 327, and the 33px left
   over is what stops a one-word orphan line. The number survived the change for
   a different reason than it was picked for. */
.trust-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr));
  gap: var(--space-md);
  padding-block: var(--space-lg);
}

/* The stars item needs a wider column than the other two: five glyphs in a row
   is a much wider object than a single 54px disc, so at equal thirds its copy
   was narrower than its neighbours' and ran a line longer than theirs. This is
   also what puts the third column back — with a 360px floor auto-fit would not
   fit three until ~1170px, and .container caps at 1180.

   The 1.16 was tuned against the old 135px chip and it happens to be right for
   the 113px bare row too, which is worth stating because it looks like a number
   nobody re-checked. At 1280 the columns come out 416 / 358 / 358 and the copy
   inside them 237 / 238 / 238 — the extra column width is spent entirely on the
   wider icon slot, which is the whole point. If the star size changes, this
   number has to be re-derived, not nudged. */
@media (min-width: 1060px) {
  .trust-grid {
    grid-template-columns: 1.16fr 1fr 1fr;
  }
}

/* Three items in two columns leaves the third alone on the second row with an
   empty cell beside it, which reads as a hole in the band rather than as the
   end of a list. Let it take the row. Bounded on both sides on purpose: above
   1060 there are three columns and this would stretch a card across all of
   them, and below 760 there is only one column and it is already true. */
@media (min-width: 760px) and (max-width: 1059px) {
  .trust-item:last-child {
    grid-column: 1 / -1;
  }
}

.trust-item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  background: var(--white);
  border: 1px solid var(--cream-deep);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
}

/* --- third-party trust badges ---
   Google, MoverLead, and whatever else config turns on. They sit under the
   three trust cards rather than beside them: these are other people's marks,
   and giving one a card of its own would put a third-party logo at the same
   weight as the claims we actually stand behind.

   They keep their own colours. Greyscaling them was tried first and on the
   cream band the MoverLead seal turned into a grey smudge nobody would read as
   a verification mark, which defeats the point of carrying one. Held back a
   little at rest instead, and they come up to full on hover.

   Every badge is a link. The whole reason the strip exists is that a visitor
   can click through to the listing and see the reviews are real, so nothing in
   here is ever a bare <img>. */
.trust-badges {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs) var(--space-sm);
  padding-bottom: var(--space-lg);
  margin-top: calc(var(--space-md) * -1);
}

.trust-badges-label {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.66rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.trust-badge-list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs) var(--space-xs);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Each badge sits on a white chip. That is not decoration — third-party badge
   art arrives with a white ground baked in (Google's is an opaque PNG, Yelp's a
   JPEG, which cannot even carry alpha), so on the cream band the logos were
   already drawing white rectangles. A chip with the same radius and hairline as
   .trust-item makes those rectangles deliberate instead of accidental, and it
   is the one treatment that works for any badge a partner sends next.

   The art is 34px tall, which is the right optical weight but leaves a target
   around 60x34. Padding, not a bigger logo: the link clears 44px in both axes
   and the marks stay quiet. */
.trust-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 0;
  background: var(--white);
  border: 1px solid var(--cream-deep);
  border-radius: 10px;
  padding: 7px 12px;
  min-height: 44px;
  min-width: 44px;
  box-sizing: border-box;
  transition:
    border-color var(--dur) var(--ease-out),
    box-shadow var(--dur) var(--ease-out),
    translate var(--dur) var(--ease-out);
}

/* Same restrained lift the trust cards get, so a real link reads as a real
   link without shouting. */
.trust-badge:hover,
.trust-badge:focus-visible {
  translate: 0 -2px;
  border-color: rgba(var(--accent-rgb), 0.45);
  box-shadow: var(--shadow-2);
}

.trust-badge img {
  width: auto;
  height: 34px;
  opacity: 0.86;
  transition:
    opacity var(--dur) var(--ease-out),
    transform var(--dur) var(--ease-spring);
}

.trust-badge:hover img,
.trust-badge:focus-visible img {
  opacity: 1;
  transform: scale(1.05);
}

/* --- one optical height, three different heights ---
   A shared `height` is not a shared weight, and with three marks in the row
   instead of two that stopped being a rounding error. The three files are
   nothing like each other:

     Google     240x120, ratio 2.00 — a large coloured wordmark over a second
                line of type and five stars. Two text sizes, so the big one has
                to be judged, not the box.
     BBB        186x76, ratio 2.45 — a monogram beside THREE stacked lines of
                caps. Its type is the smallest of the three by a wide margin and
                it is flat navy line art on white, so it is also the lightest.
     MoverLead  360x188, ratio 1.91 — a solid blue rectangle with knocked-out
                white type. A filled block reads far heavier than line art at
                the same height, and it is the only one that draws its own edge.

   Matched by the height of the type a visitor actually reads rather than by the
   bounding box: BBB up so its three lines are legible at all, MoverLead down so
   its slab of blue does not dominate the row, Google between them. Rendered
   widths land at 60 / 93 / 57px, which is also why the row balances — the chip
   padding is constant, so near-equal widths mean near-equal chips. */
.trust-badge-google img {
  height: 30px;
}

.trust-badge-bbb img {
  height: 38px;
}

.trust-badge-moverlead img {
  height: 30px;
}

/* In the footer the strip is a left-aligned block in the brand column, on a
   dark ground where the badge colours read as colour rather than as noise. */
.trust-badges-footer {
  justify-content: flex-start;
  padding-bottom: 0;
  margin-top: 4px;
  margin-inline-start: -10px; /* cancel the first badge's tap padding */
}

.trust-badges-footer .trust-badge-list {
  justify-content: flex-start;
}

/* The chip stays white in the footer too. A dark chip would need every badge's
   art recoloured, which is not ours to do. */
.trust-badges-footer .trust-badge {
  border-color: transparent;
}

.trust-badges-footer .trust-badge:hover,
.trust-badges-footer .trust-badge:focus-visible {
  border-color: rgba(var(--cream-rgb), 0.35);
}

/* The footer runs the same three at about 88% of the strip's size. The per-badge
   heights have to be restated rather than left to cascade: `.trust-badges-footer
   .trust-badge img` is (0,3,1) and flattens `.trust-badge-bbb img` (0,1,1), so
   without these three lines the footer would go back to one shared height and
   lose exactly what the block above is for. */
.trust-badges-footer .trust-badge img {
  height: 30px;
  opacity: 0.92;
}

.trust-badges-footer .trust-badge-google img {
  height: 27px;
}

.trust-badges-footer .trust-badge-bbb img {
  height: 30px;
}

.trust-badges-footer .trust-badge-moverlead img {
  height: 28px;
}

/* The one width where the column floor above cannot help, because there is only
   one column left to give: at 320px the stars chip and its copy do not share a
   line, and letting just that item wrap would put one of three in a different
   shape again. Stack all three rather than one. */
@media (max-width: 359px) {
  .trust-item {
    flex-direction: column;
    align-items: flex-start;
  }
}

/* The 150px basis is the backstop behind the column floor, not the mechanism —
   between them nothing wraps from 360px up. It is what stops a future longer
   label, a wider icon or a user zoomed to 200% from squeezing the copy into a
   two-word-per-line column: the copy drops under the icon instead. min-width: 0
   because a flex item defaults to min-width: auto, which would let a long
   unbroken word push the card wider than its grid track. */
.trust-copy {
  flex: 1 1 150px;
  min-width: 0;
  display: grid;
  gap: 3px;
}

.trust-item .ico {
  flex: none;
  width: 54px;
  height: 54px;
  border-radius: 50%;
  /* Off-centre highlight so the disc reads as a lit object rather than a flat
     swatch. The 0.22 stop is the deepest the tint can go: it is the ground the
     glyph ratio above is measured against, and the hover state adds another
     0.10 underneath it, which is what puts the Albany floor at 4.58:1. Any
     stronger and the hover disc drops through 4.5:1. */
  background: radial-gradient(
    120% 120% at 32% 14%,
    rgba(var(--accent-rgb), 0.22),
    rgba(var(--accent-rgb), 0.1) 74%
  );
  box-shadow: inset 0 0 0 1px rgba(var(--accent-rgb), 0.3);
  display: grid;
  place-items: center;
  color: var(--accent-deep);
  transition: background-color var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out);
}

.trust-item .ico svg {
  display: block;
}

/* Ray, 2026-08-09: "fix the five-star reviews under the hero. I don't like the
   way it's formatted. Remove the little bubble surrounding the stars, just make
   it the star animation coming in."

   So the pill is gone — no ground, no ring, no shadow, no padding. What used to
   be written here was an argument for the chip: gold is 2.03:1 on white, so it
   was floated on a dark wash where it measured 6.78:1. That argument was about
   the chip. Without one, the five stars are a decorative graphic sitting beside
   a text label, not the label — "Five-star reviews" is 18.58:1 in --ink two
   pixels to the right of them, and "Read them on our Google Business Profile"
   under that. Nothing here is the only carrier of anything, which is what 1.4.3
   and 1.4.11 actually ask. Measured on the card the gold is 2.03:1 and that is
   fine; it would not be fine if the stars were the claim.

   Three things replace the chip's job:
     - Height is pinned to 54px, the disc the other two items carry, so the flex
       row's cross-axis centring lands all three labels on the same line and the
       three cards read as one set. Width comes from the glyphs.
     - The glyphs go up from 19px to 21px and the gap down from 4px to 2px. The
       chip used to give the row its edges; now the row has to be its own object,
       and five tight stars read as a rating where five loose ones read as five
       icons.
     - A one-pixel drop shadow, which is what stops #f5a623 dissolving into
       #ffffff at the top of each point. It is a definition edge, not a bubble —
       no fill, no border, nothing enclosing. */
.trust-item-stars .ico {
  width: auto;
  height: 54px;
  padding: 0;
  border-radius: 0;
  display: flex;
  align-items: center;
  gap: 2px;
  color: var(--star);
  background: none;
  box-shadow: none;
  filter: drop-shadow(0 1px 1px rgba(var(--dark-rgb), 0.22));
}

.trust-item-stars .ico svg {
  width: 21px;
  height: 21px;
}

.trust-item strong {
  display: block;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.02rem;
  line-height: 1.25;
}

/* Scoped to .trust-copy on purpose. The old rule was a bare descendant `span`,
   which also matched the .trust-copy wrapper itself and the .ico — harmless
   while the wrapper was a plain inline, and wrong the moment either grew rules
   of its own. */
.trust-copy span {
  font-size: 0.85rem;
  color: var(--ink-muted);
  line-height: 1.45;
}

/* Same trap as .market-card, same fix — see the long comment there. The reveal
   system owns `transform` and the whole `transition` shorthand on any
   [data-reveal] element and `.reveal-ready [data-reveal]` (0,2,0) outranks a
   bare `.trust-item` (0,1,0), so the hover lift uses the independent `translate`
   property and the transition is restated one specificity step up carrying the
   reveal's own opacity/transform entries. The delays stay on those two entries
   only, or a hover would sit out a --reveal-delay before it moved. */
.trust-strip .trust-grid .trust-item {
  transition:
    opacity var(--dur-slow) var(--ease-out) var(--reveal-delay, 0ms),
    transform var(--dur-slow) var(--ease-out) var(--reveal-delay, 0ms),
    translate var(--dur) var(--ease-out),
    box-shadow var(--dur) var(--ease-out),
    border-color var(--dur) var(--ease-out);
}

/* Nothing in the card is a link, so this is the same restrained acknowledgement
   .market-card gives and not a promise that the panel is clickable: no cursor
   change, a shorter lift than the real card links get, and the colour arrives
   in the disc rather than under the text where it would eat contrast. */
.trust-item:hover .ico {
  background-color: rgba(var(--accent-rgb), 0.1);
  box-shadow: inset 0 0 0 1px rgba(var(--accent-rgb), 0.42);
}

.trust-item:hover {
  translate: 0 -3px;
  box-shadow: var(--shadow-2);
  border-color: rgba(var(--accent-rgb), 0.45);
}

/* The stars have no disc to tint, and the generic rule above has the same
   specificity as this one — without these two lines the pale accent wash would
   be painted straight onto the bare stars row and the gold would land on pink.
   The card still lifts; the stars just do not gain a ground on the way. */
.trust-item-stars:hover .ico {
  background-color: transparent;
  box-shadow: none;
}

/* -------------------------------------------------- the star pop, boom x5 --
   Ray asked for the stars to arrive "boom, boom, boom, boom, boom", one at a
   time. CSS only: the reveal observer already stamps .is-revealed on each
   .trust-item as it enters the viewport, so the five beats hang off that and no
   new JS exists to break.

   The direction of the dependency is the whole point, and it is the same
   contract the reveal system states further down: content is visible by
   default. `.pop` carries NO hidden state of its own. The 0% frame is the only
   place opacity ever reaches zero, animation-fill-mode: backwards holds that
   frame during the stagger delay, and the whole rule is gated behind both
   .reveal-ready and .is-revealed — two classes that only exist once JS has
   confirmed it can drive an IntersectionObserver. With JS off, blocked or
   thrown, the selector never matches and all five stars are simply on screen.

   Timing: 100ms apart, which is far enough to hear five separate beats and
   close enough that the set still lands as one gesture. The first waits 120ms
   so the card's own 420ms fade is under way before the stars start, and the
   last settles at ~1.04s. The overshoot is --ease-spring doing its job on
   `scale` — the keyframes themselves are a plain 0.2 to 1, and the bezier's 1.4
   control point is what carries it past full size and back. */
@keyframes trust-star-pop {
  from {
    opacity: 0;
    scale: 0.2;
  }
  to {
    opacity: 1;
    scale: 1;
  }
}

.trust-item-stars .pop {
  display: inline-flex;
}

.reveal-ready .trust-item-stars.is-revealed .pop {
  animation: trust-star-pop 520ms var(--ease-spring) backwards;
}

.reveal-ready .trust-item-stars.is-revealed .pop:nth-child(1) {
  animation-delay: 120ms;
}

.reveal-ready .trust-item-stars.is-revealed .pop:nth-child(2) {
  animation-delay: 220ms;
}

.reveal-ready .trust-item-stars.is-revealed .pop:nth-child(3) {
  animation-delay: 320ms;
}

.reveal-ready .trust-item-stars.is-revealed .pop:nth-child(4) {
  animation-delay: 420ms;
}

.reveal-ready .trust-item-stars.is-revealed .pop:nth-child(5) {
  animation-delay: 520ms;
}

/* --------------------------------------------------------------- card ---- */
.card {
  background: var(--white);
  border-radius: var(--radius-lg);
  padding: var(--space-md);
  box-shadow: var(--shadow-1);
  border: 1px solid var(--cream-deep);
  display: flex;
  flex-direction: column;
  gap: 10px;
  transition:
    transform var(--dur) var(--ease-out),
    box-shadow var(--dur) var(--ease-out),
    border-color var(--dur) var(--ease-out);
}

.card p {
  color: var(--ink-muted);
  font-size: 0.95rem;
}

.card .text-link {
  margin-top: auto;
  padding-top: 6px;
}

/* Third instance of the same trap as .market-card and .trust-item, and this one
   was live rather than theoretical: the lift was `transform: translateY(-5px)`
   at (0,2,0), and `.reveal-ready [data-reveal].is-revealed { transform: none }`
   is (0,3,0), so once a service or area card had scrolled into view its hover
   lift stopped happening entirely. Same fix as the other two — `translate` is
   an independent property nothing else touches, and the transition is restated
   one specificity step up so the hover does not sit out a --reveal-delay before
   it moves. Every card in this build is inside a .grid, which is what makes
   that third class available. */
.grid .card.card-link {
  transition:
    opacity var(--dur-slow) var(--ease-out) var(--reveal-delay, 0ms),
    transform var(--dur-slow) var(--ease-out) var(--reveal-delay, 0ms),
    translate var(--dur) var(--ease-out),
    box-shadow var(--dur) var(--ease-out),
    border-color var(--dur) var(--ease-out);
}

.card-link:hover {
  translate: 0 -5px;
  box-shadow: var(--shadow-3);
  border-color: rgba(var(--accent-rgb), 0.5);
}

.card-service {
  position: relative;
  overflow: hidden;
}

/* Full-bleed illustration band. A 92px thumbnail was too small for the mascot to
   read as a character, and poses whose subject sits low in the frame (the watch
   pose especially) cropped to an empty square. The band bottom-aligns the art so
   every pose lands the same way regardless of where the character sits. */
.card-service .card-figure {
  width: calc(100% + 2 * var(--space-md));
  margin: calc(-1 * var(--space-md)) calc(-1 * var(--space-md)) 6px;
  aspect-ratio: 4 / 3;
  background: var(--cream);
  border-bottom: 4px solid var(--cream-deep);
  display: grid;
  place-items: end center;
  overflow: hidden;
  position: relative;
}

/* --- photo variant of the service-card figure ---
   The mascot rules below are bottom-anchored and height-limited so the cap and
   the feet survive. A photograph wants the opposite: fill the box, crop from the
   centre, no ground shadow. Everything the mascot needs is re-set here rather
   than made conditional, because the two treatments share nothing but the box.

   The source photos run 800x1139 portrait to 1200x837 landscape, so object-fit
   is doing real work — without it a portrait shot would letterbox inside a 4:3
   figure and a landscape one would overflow it.

   Every selector here is qualified `.card-service .card-figure.card-figure-photo`
   rather than the shorter `.card-figure-photo`. The mascot rules below are
   `.card-service .card-figure img`, which is 0-2-1; a bare `.card-figure-photo img`
   is 0-1-1 and loses, so the first version of this block was overridden in full
   and the photos rendered at 90% height, centred, with the mascot's ground
   shadow over them. Do not shorten these selectors. */
.card-service .card-figure.card-figure-photo {
  place-items: stretch;
  background: var(--dark);
  border-bottom-color: var(--accent);
}

.card-service .card-figure.card-figure-photo::after {
  /* A bottom-weighted scrim, not the mascot's radial ground shadow. It keeps the
     4px accent rule from vibrating against a bright patch of photograph. */
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(var(--dark-rgb), 0.34), transparent 46%);
  pointer-events: none;
}

.card-service .card-figure.card-figure-photo picture {
  display: contents;
}

.card-service .card-figure.card-figure-photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  transform: none;
  transform-origin: center;
  transition: transform var(--dur-slow) var(--ease-out);
}

.card-service:hover .card-figure.card-figure-photo img {
  transform: scale(1.05);
}

@media (prefers-reduced-motion: reduce) {
  .card-service:hover .card-figure.card-figure-photo img {
    transform: none;
  }
}

.card-service .card-figure::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at 50% 118%, rgba(var(--dark-rgb), 0.11), transparent 58%);
  pointer-events: none;
}

/* The image is positioned, not laid out. A percentage height on an in-flow child
   of an aspect-ratio box resolves to auto, which renders the mascot at its full
   1080px natural size and clips it to a meaningless sliver. Absolute positioning
   gives the percentage a definite containing block to resolve against. */
.card-service .card-figure picture {
  display: contents;
}

/* The mascot art is trimmed to zero padding — the cap is on the first row of the
   PNG and the ground shadow on the last. So the figure box cannot bleed it at all
   without cutting the character: at 116% height pulled down 7% it lost 29px off
   the top and 19px off the bottom of a 330px box, which sliced the cap and the
   feet off every card. Any headroom has to come from the box, not the file.

   Height is therefore under 100% with the mascot standing on the bottom edge, and
   the hover scale has to stay inside the remainder — 90% × 1.06 = 95.4%, so the
   head still clears the top on hover. Raising either number re-crops the art. */
.card-service .card-figure img {
  position: absolute;
  bottom: 0;
  left: 50%;
  width: auto;
  height: 90%;
  max-width: none;
  transform: translateX(-50%);
  transform-origin: bottom center;
  transition: transform var(--dur-slow) var(--ease-out);
}

.card-service:hover .card-figure img {
  transform: translateX(-50%) scale(1.06);
}

.card-service::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 4px;
  background: repeating-linear-gradient(-45deg, var(--dark) 0 8px, var(--accent) 8px 16px);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-slow) var(--ease-out);
}

.card-service:hover::after {
  transform: scaleX(1);
}

.card-area {
  gap: 8px;
}

.card-area h3 {
  font-size: var(--step-1);
}

.card-area .area-state {
  font-family: var(--font-display);
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent-deep);
}

.section-dark .card {
  background: var(--dark-soft);
  border-color: var(--dark-line);
}

.section-dark .card h3 {
  color: var(--white);
}

.section-dark .card p {
  color: var(--ink-muted-on-dark);
}

/* -------------------------------------------------------------- steps ---- */
.steps {
  display: grid;
  gap: var(--space-md);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  counter-reset: step;
}

.step {
  position: relative;
  padding-top: var(--space-md);
  border-top: 3px solid var(--cream-deep);
}

.step::before {
  counter-increment: step;
  content: "0" counter(step);
  font-family: var(--font-display);
  font-weight: 900;
  font-size: var(--step-3);
  line-height: 1;
  color: var(--accent);
  display: block;
  margin-bottom: 10px;
}

.step h3 {
  font-size: var(--step-1);
  margin-bottom: 6px;
}

.step p {
  color: var(--ink-muted);
  font-size: 0.95rem;
}

.section-dark .step {
  border-top-color: var(--dark-line);
}

.section-dark .step p {
  color: var(--ink-muted-on-dark);
}

/* ------------------------------------------------------------- lists ---- */
.check-list {
  list-style: none;
  padding: 0;
  display: grid;
  gap: 11px;
}

.check-list li {
  position: relative;
  padding-left: 34px;
  line-height: 1.55;
}

.check-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 2px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--accent);
}

.check-list li::after {
  content: "";
  position: absolute;
  left: 7.5px;
  top: 8px;
  width: 6px;
  height: 10px;
  border-right: 2.5px solid var(--dark);
  border-bottom: 2.5px solid var(--dark);
  transform: rotate(42deg);
}

.pill-list {
  list-style: none;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 9px;
}

/* Every pill-list item wraps an a.pill, so the box styling belongs on the anchor
   only — styling the li as well drew a second border around every pill. */
.pill-list li {
  display: inline-flex;
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 14px;
  border-radius: var(--radius-pill);
  background: var(--white);
  border: 1px solid var(--cream-deep);
  font-size: 0.86rem;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
  transition: border-color var(--dur) var(--ease-out), transform var(--dur) var(--ease-out);
}

a.pill:hover {
  border-color: var(--accent);
  transform: translateY(-2px);
  color: var(--ink);
}

.section-dark .pill {
  background: var(--dark-soft);
  border-color: var(--dark-line);
  color: var(--ink-on-dark);
}

/* Homepage locations block: the two markets as two grouped lists, so the towns
   stay attached to the location they belong to instead of running together into
   one seventeen-town wall. */
.location-blocks {
  display: grid;
  gap: var(--space-lg);
  margin-top: var(--space-lg);
}

@media (min-width: 720px) {
  .location-blocks {
    grid-template-columns: 1fr 1fr;
  }
}

.location-block-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: var(--space-sm);
  padding-bottom: 8px;
  border-bottom: 1px solid var(--cream-deep);
  font-family: var(--font-display);
  font-weight: 900;
  font-size: 1.05rem;
  color: var(--dark);
  text-decoration: none;
}

.location-block-head:hover {
  color: var(--accent-deep);
}

/* ------------------------------------------------------- job photos ------ */
/* Real client photography. Frames match the panel treatment so photos and
   panels read as one card family. */
.job-photo img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-lg);
  border: 1px solid var(--cream-deep);
  box-shadow: var(--shadow-2);
}

/* Homepage strip: four crew shots as framed prints — white mat, caption under
   the photo, every other frame dropped so the row reads as pinned prints
   rather than a thumbnail grid. Offset is margin, not transform, because the
   reveal animation owns transform. */
.photo-strip {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(1.25rem, 2.6vw, 2.5rem);
  align-items: start;
  padding-top: var(--space-sm);
}

.crew-shot {
  margin: 0;
  background: var(--white);
  border: 1px solid var(--cream-deep);
  border-radius: var(--radius-lg);
  padding: 10px 10px 0;
  box-shadow: var(--shadow-2);
  transition: box-shadow var(--dur) var(--ease-out);
}

.crew-shot:hover {
  box-shadow: var(--shadow-3);
}

.crew-shot:nth-child(even) {
  margin-top: var(--space-xl);
}

.crew-shot img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: calc(var(--radius-lg) - 6px);
}

/* Caption carries the same dash motif as the eyebrows, so the prints read as
   part of the design system rather than dropped-in images. */
.crew-shot figcaption {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 11px 4px 13px;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 0.8rem;
  letter-spacing: 0.01em;
  color: var(--ink-muted);
}

.crew-shot figcaption::before {
  content: "";
  flex: none;
  width: 15px;
  height: 4px;
  border-radius: 2px;
  background: repeating-linear-gradient(90deg, var(--dark) 0 5px, var(--accent) 5px 10px);
}

@media (max-width: 700px) {
  .photo-strip {
    grid-template-columns: repeat(2, 1fr);
    gap: var(--space-md);
  }

  .crew-shot:nth-child(even) {
    margin-top: var(--space-lg);
  }
}

/* ------------------------------------------------------ panel / callout -- */
.panel {
  background: var(--white);
  border: 1px solid var(--cream-deep);
  border-radius: var(--radius-lg);
  padding: var(--space-lg);
  box-shadow: var(--shadow-1);
}

.panel-navy {
  background: var(--dark);
  border-color: var(--dark-line);
  color: var(--ink-on-dark);
}

.panel-navy h2,
.panel-navy h3 {
  color: var(--white);
}

.panel-navy p,
.panel-navy li {
  color: var(--ink-muted-on-dark);
}

.callout {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  background: var(--white);
  border-left: 5px solid var(--accent);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: var(--space-md);
  box-shadow: var(--shadow-1);
}

.callout .ico {
  flex: none;
  color: var(--accent-deep);
  margin-top: 2px;
}

.callout p {
  font-size: 0.95rem;
  color: var(--ink-muted);
}

.callout strong {
  color: var(--ink);
}

.callout a {
  color: var(--accent-deep);
}

.callout-dark {
  background: var(--dark-soft);
  border-left-color: var(--accent);
}

.callout-dark p {
  color: var(--ink-muted-on-dark);
}

.callout-dark strong {
  color: var(--white);
}

.callout-dark .ico,
.callout-dark a {
  color: var(--accent-on-dark);
}

/* ------------------------------------------------------ google reviews --- */
.google-badge {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-top: var(--space-md);
  padding: 9px 18px 9px 9px;
  border-radius: var(--radius-pill);
  background: var(--cream);
  border: 1px solid var(--cream-deep);
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 0.85rem;
  color: var(--dark);
  text-decoration: none;
  position: relative;
  transition: border-color var(--dur) var(--ease-out), transform var(--dur) var(--ease-out);
}

.google-badge:hover {
  border-color: var(--accent);
  transform: translateY(-2px);
  color: var(--dark);
}

.google-badge .g {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--white);
  display: grid;
  place-items: center;
  box-shadow: var(--shadow-1);
}

.review-quote {
  background: var(--white);
  border-radius: var(--radius-lg);
  padding: var(--space-md);
  border: 1px solid var(--cream-deep);
  box-shadow: var(--shadow-1);
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.review-quote blockquote {
  margin: 0;
  font-size: 1.02rem;
  font-weight: 600;
  line-height: 1.5;
}

.review-quote .stars {
  display: flex;
  gap: 3px;
  color: var(--star);
}

/* These two targeted `footer` and never matched anything — the generator has
   always emitted <figcaption> inside the <figure>, so the attribution has been
   rendering at body size in full ink. Retargeted, not deleted. */
.review-quote figcaption {
  font-size: 0.84rem;
  color: var(--ink-muted);
  margin-top: auto;
}

.review-quote figcaption b {
  color: var(--ink);
  font-family: var(--font-display);
}

/* ------------------------------------------------------ review conveyor ---
   A crest on the left, a marquee of review cards on the right. The crest is the
   fixed anchor — it carries the wreath, so it must not shrink — and the
   conveyor absorbs whatever width is left. min-width: 0 is load-bearing: a flex
   item defaults to min-width: auto, which would let the max-content track push
   the rail wider than the container and put the whole page into horizontal
   scroll. */
.review-rail {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
}

.review-crest {
  flex: none;
  width: 260px;
  max-width: 100%;
}

/* Laurel wreath behind the crest — the real laurel asset from the house
   reference (speedymovingcompany.com), recoloured to the Albany brand red and
   rendered with the reference geometry: a 330px wreath centred on the 260x200
   box that holds the stars and the label, so it overhangs the box on both
   sides. PNG, not webp: a CSS background gets no <picture> fallback, and a
   browser that can't decode the file shows no wreath at all. 31 KB buys
   certainty. */
.review-crest .crest-core {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 260px;
  max-width: 100%;
  height: 200px;
  margin-inline: auto;
  background: url("/assets/icons/laurel.png") center / 330px no-repeat;
}

.crest-core .stars {
  display: flex;
  justify-content: center;
  gap: 5px;
  color: var(--star);
}

/* Capped narrower than the box so the label breaks inside the wreath rather
   than running out through the leaves. */
.crest-label {
  max-width: 170px;
  margin: 0;
  font-family: var(--font-display);
  font-weight: 900;
  font-size: 0.84rem;
  line-height: 1.25;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-align: center;
  color: var(--ink);
}

.review-conveyor {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  /* The cards carry --shadow-1. overflow clips at the padding box, so this is
     the room the shadow needs to survive the clip. */
  padding-block: var(--space-xs);
}

/* Soft edges, so cards fade in and out instead of being guillotined by the
   overflow. Gated behind @supports because a browser without mask support
   would apply nothing and show a hard cut — acceptable — but one that half
   supports the unprefixed form would hide the rail entirely. black/transparent
   here are alpha stops, not paint: the mask only reads the alpha channel. */
@supports (mask-image: linear-gradient(black, black)) or (-webkit-mask-image: linear-gradient(black, black)) {
  .review-conveyor {
    -webkit-mask-image: linear-gradient(to right, transparent 0, black 56px, black calc(100% - 56px), transparent 100%);
    mask-image: linear-gradient(to right, transparent 0, black 56px, black calc(100% - 56px), transparent 100%);
  }
}

/* The generator emits N cards, then the same N again inside a wrapper marked
   aria-hidden. display: contents dissolves that wrapper so its cards become
   direct flex items of the track — without it the whole clone set would
   collapse into one flex item and the loop point would be wrong. aria-hidden
   still applies to the subtree, so a screen reader hears each review once. */
.conveyor-track > [aria-hidden="true"] {
  display: contents;
}

/* No `gap` here, deliberately. The track is 2N cards wide and the loop point is
   -50%, which only lands seam-free if half the track is exactly N card-slots.
   With gap the track measures 2N cards + (2N-1) gaps, so half of it is half a
   gap short and the marquee visibly jumps once per cycle. Putting the gap on
   the card as margin-right makes every slot identical — 2N x (card + gap) — and
   -50% is then exact. */
.conveyor-track {
  display: flex;
  width: max-content;
  animation: conveyor 44s linear infinite;
}

.review-conveyor .review-quote {
  width: 300px;
  flex: none;
  margin-right: var(--space-md);
}

/* Hover for a mouse, focus-within for a keyboard: the caption holds no link
   today, but the moment one is added, tabbing into a card that is still sliding
   is unusable. Pausing on focus costs nothing and covers it. */
.review-conveyor:hover .conveyor-track,
.review-conveyor:focus-within .conveyor-track {
  animation-play-state: paused;
}

/* The region itself is focusable — it carries tabindex="0" so that under
   reduced motion, where it becomes a horizontal scroller, a keyboard can reach
   the last card (WCAG 2.1.1). A focus stop with no visible ring is its own
   failure, so give it the same 3px accent ring the global :focus-visible uses.
   The mask has to go while it is focused: it fades the outer 56px to
   transparent and a mask paints the outline too, so the left and right sides of
   the ring would fade out with the cards. Dropping it exposes the hard edges
   for the duration of the focus, which is the right trade — and the rule above
   has already stopped the track, so nothing is moving under it. */
.review-conveyor:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
  -webkit-mask-image: none;
  mask-image: none;
}

@keyframes conveyor {
  from {
    transform: translate3d(0, 0, 0);
  }
  to {
    transform: translate3d(-50%, 0, 0);
  }
}

/* Below this the crest and the conveyor stop being a row: 260px of fixed crest
   plus a 300px card leaves the conveyor too narrow to show a card and a half,
   which is what makes it read as motion rather than as a stuck card. */
@media (max-width: 820px) {
  .review-rail {
    flex-direction: column;
    align-items: center;
  }

  .review-crest {
    margin-inline: auto;
  }

  .review-conveyor {
    width: 100%;
  }
}

/* The wreath overhangs its box by 35px a side. On a 320px screen that reaches
   past the viewport edge and only body { overflow-x: hidden } is catching it —
   shrink the wreath instead of relying on that. */
@media (max-width: 400px) {
  .review-crest .crest-core {
    background-size: 270px;
  }
}

/* ---------------------------------------------------------------- faq ---- */
.faq-list {
  display: grid;
  gap: 10px;
}

.faq-item {
  background: var(--white);
  border: 1px solid var(--cream-deep);
  border-radius: var(--radius);
  overflow: hidden;
  transition: border-color var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out);
}

.faq-item:has(> button[aria-expanded="true"]) {
  border-color: rgba(var(--accent-rgb), 0.55);
  box-shadow: var(--shadow-2);
}

.faq-q {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: var(--space-md);
  background: none;
  border: 0;
  text-align: left;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.03rem;
  color: var(--ink);
  cursor: pointer;
  min-height: 60px;
}

.faq-q:hover {
  color: var(--accent-deep);
}

.faq-q .sign {
  flex: none;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--cream);
  display: grid;
  place-items: center;
  position: relative;
  transition: background-color var(--dur) var(--ease-out), transform var(--dur) var(--ease-out);
}

.faq-q .sign::before,
.faq-q .sign::after {
  content: "";
  position: absolute;
  background: var(--dark);
  border-radius: 2px;
  transition: transform var(--dur) var(--ease-out), opacity var(--dur) var(--ease-out);
}

.faq-q .sign::before {
  width: 12px;
  height: 2.5px;
}

.faq-q .sign::after {
  width: 2.5px;
  height: 12px;
}

.faq-q[aria-expanded="true"] .sign {
  background: var(--accent);
  transform: rotate(180deg);
}

.faq-q[aria-expanded="true"] .sign::after {
  transform: scaleY(0);
  opacity: 0;
}

.faq-a {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-slow) var(--ease-out);
}

.faq-a > div {
  overflow: hidden;
}

.faq-item.is-open .faq-a {
  grid-template-rows: 1fr;
}

.faq-a p {
  color: var(--ink-muted);
  padding: 0 var(--space-md) var(--space-md);
  font-size: 0.97rem;
}

/* --------------------------------------------------------------- form ---- */
.quote-form {
  display: grid;
  gap: var(--space-md);
}

.field-grid {
  display: grid;
  gap: var(--space-sm);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
}

.field {
  display: grid;
  gap: 6px;
}

.field > span {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.83rem;
  letter-spacing: 0.02em;
}

.field .hint {
  font-family: var(--font-body);
  font-weight: 400;
  color: var(--ink-muted);
  font-size: 0.78rem;
  letter-spacing: 0;
}

.field input,
.field select,
.field textarea {
  width: 100%;
  min-height: 50px;
  padding: 12px 14px;
  border-radius: var(--radius);
  border: 2px solid var(--cream-deep);
  background: var(--white);
  font-size: 1rem;
  transition: border-color var(--dur) var(--ease-out), box-shadow var(--dur) var(--ease-out);
}

.field textarea {
  min-height: 112px;
  resize: vertical;
}

.field input:hover,
.field select:hover,
.field textarea:hover {
  border-color: var(--line-hover);
}

.field input:focus,
.field select:focus,
.field textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.18);
  outline: none;
}

.field input[aria-invalid="true"],
.field select[aria-invalid="true"] {
  border-color: var(--error);
  border-width: 2px;
}

.field-error {
  font-size: 0.8rem;
  color: var(--error-ink);
  font-weight: 600;
}

.field-error:empty {
  display: none;
}

/* -------------------------------------------------- two-step quote card -- */
/* The teddybearmoving reference treatment: near-black card with the brand
   left edge, two thin step bars, one column of fields per step. Step 1 is the
   move, step 2 is contact. */
.quote-form {
  background: var(--dark);
  border-left: 5px solid var(--accent);
  border-radius: var(--radius-xl);
  padding: clamp(1.5rem, 3vw, 2.25rem);
  box-shadow: var(--shadow-3);
}

.qf-head h3 {
  color: var(--white);
  font-size: 1.45rem;
}

.qf-head p {
  margin-top: 4px;
  color: var(--ink-muted-on-dark);
  font-size: 0.9rem;
}

.qf-accent {
  color: var(--accent-on-dark);
}

.qf-bars {
  display: flex;
  gap: 8px;
}

.qf-bar {
  height: 4px;
  flex: 1;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.15);
  transition: background-color var(--dur) var(--ease-out);
}

.qf-bar.is-active {
  background: var(--accent-lift);
}

.qf-step {
  display: grid;
  gap: var(--space-md);
}

.qf-step[hidden] {
  display: none;
}

/* Re-showing a step restarts this animation (display: none resets it), so each
   step of the two-step form slides in rather than snapping. */
.qf-step:not([hidden]) {
  animation: qf-step-in var(--dur-slow) var(--ease-out);
}

@keyframes qf-step-in {
  from {
    opacity: 0;
    transform: translateX(16px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.qf-back {
  justify-self: start;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  background: none;
  border: 0;
  padding: 2px 0;
  cursor: pointer;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
  transition: color var(--dur) var(--ease-out);
}

.qf-back:hover,
.qf-back:focus-visible {
  color: var(--white);
}

.quote-form .field > span {
  color: rgba(255, 255, 255, 0.88);
}

.quote-form .req {
  color: var(--accent-on-dark);
}

.quote-form .field input,
.quote-form .field select {
  background: rgba(255, 255, 255, 0.05);
  border-color: rgba(255, 255, 255, 0.16);
  color: var(--white);
  /* Native date picker, select dropdown and calendar glyph render dark. */
  color-scheme: dark;
}

.quote-form .field input::placeholder {
  color: rgba(255, 255, 255, 0.4);
}

.quote-form .field input:hover,
.quote-form .field select:hover {
  border-color: rgba(255, 255, 255, 0.32);
}

.quote-form .field input:focus,
.quote-form .field select:focus {
  border-color: var(--accent-lift);
  background: rgba(255, 255, 255, 0.08);
}

.quote-form .field input[aria-invalid="true"],
.quote-form .field select[aria-invalid="true"] {
  border-color: var(--accent-on-dark);
}

.quote-form .field-error {
  color: #ff9598;
}

.qf-cta {
  width: 100%;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-size: 0.88rem;
  padding-block: 16px;
}

.quote-form .form-note {
  color: rgba(255, 255, 255, 0.55);
}

.quote-form .form-status {
  color: var(--ink-on-dark);
}

.quote-form .form-status[data-state="error"] {
  color: #ff9598;
}

.form-note {
  font-size: 0.82rem;
  color: var(--ink-muted);
}

.form-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  align-items: center;
}

.form-status {
  font-weight: 600;
  font-size: 0.92rem;
}

.form-status[data-state="error"] {
  color: var(--error-ink);
}

.quote-summary {
  background: var(--dark);
  color: var(--ink-on-dark);
  border-radius: var(--radius-lg);
  padding: var(--space-lg);
  display: grid;
  gap: var(--space-sm);
  /* Client feedback 2026-08-07: the summary sat flush under the form card and
     read as squished. Clear air above it, an accent keyline so it reads as its
     own card, and an entrance so it arrives rather than appears. */
  margin-top: var(--space-xl);
  border: 1px solid rgba(var(--accent-rgb), 0.4);
  border-top: 4px solid var(--accent);
}

.quote-summary:not([hidden]) {
  animation: summary-in var(--dur-slow) var(--ease-out);
}

@keyframes summary-in {
  from {
    opacity: 0;
    transform: translateY(18px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.quote-summary h3 {
  color: var(--white);
}

.quote-summary dl {
  display: grid;
  gap: 8px;
  margin: 0;
}

.quote-summary .row {
  display: flex;
  gap: var(--space-sm);
  justify-content: space-between;
  border-bottom: 1px dashed var(--dark-line);
  padding-bottom: 7px;
  font-size: 0.93rem;
}

.quote-summary dt {
  color: var(--ink-muted-on-dark);
}

.quote-summary dd {
  margin: 0;
  font-weight: 700;
  color: var(--white);
  text-align: right;
}

/* ------------------------------------------------------- page furniture -- */
.page-hero {
  background: var(--dark);
  color: var(--ink-on-dark);
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

.page-hero::after {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  background-image: repeating-linear-gradient(
    -45deg,
    rgba(var(--cream-rgb), 0.028) 0 12px,
    transparent 12px 34px
  );
}

.page-hero-inner {
  display: grid;
  gap: var(--space-lg);
  padding-block: var(--space-2xl);
  align-items: center;
}

.page-hero h1 {
  font-size: var(--step-5);
  color: var(--white);
}

.page-hero p {
  color: var(--ink-muted-on-dark);
  font-size: var(--step-1);
  max-width: 60ch;
  margin-top: var(--space-sm);
}

.breadcrumb {
  font-size: 0.8rem;
  color: var(--ink-muted-on-dark);
  margin-bottom: var(--space-sm);
}

.breadcrumb ol {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}

.breadcrumb li:not(:last-child)::after {
  content: "/";
  margin-left: 6px;
  opacity: 0.5;
}

.breadcrumb a {
  color: var(--ink-muted-on-dark);
  text-decoration: none;
}

.breadcrumb a:hover {
  color: var(--accent-on-dark);
}

.breadcrumb [aria-current="page"] {
  color: var(--white);
}

/* The hero breadcrumb is the one place these run on cream instead of over a
   dark hero. --ink-muted-on-dark is #c2c2c2, which is 1.4:1 there — axe caught
   it on both market hubs as soon as the scrim came off. */
.hero-photo .breadcrumb,
.hero-photo .breadcrumb a {
  color: var(--ink-muted);
}

.hero-photo .breadcrumb a:hover {
  color: var(--accent);
}

.hero-photo .breadcrumb [aria-current="page"] {
  color: var(--ink);
}

/* -------------------------------------------------------- launch band ---- */
/* Printed directly under the hero on every page of a market that has not opened
   yet — see launchBand() in generate.mjs. On all 21 of those pages it lands
   between a dark hero and a cream section, so it takes the deeper cream plus an
   accent rule along the top: separated from both neighbours, without becoming a
   third full-colour band above the fold.

   The heading takes --accent-deep rather than --accent because it sits directly
   on top of a paragraph that is already full-strength navy and has to separate
   from it. On --cream-deep that is 7.70:1 (Charlotte) and 6.60:1 (Albany, if a
   second market ever launches); the paragraph runs --ink-muted at 7.12 / 6.07:1
   and the ghost button's navy at 14.57:1. */
.launch-band {
  background: var(--cream-deep);
  border-top: 3px solid var(--accent);
  border-bottom: 1px solid var(--line-hover);
}

.launch-inner {
  display: grid;
  gap: var(--space-md);
  align-items: center;
  padding-block: var(--space-lg);
}

/* Grid items default to min-width:auto, which is min-content, and .btn is
   white-space:nowrap — so "Careers – Join Our Charlotte Team" set a 339px floor
   on the column and pushed the whole band 31px past a 320px viewport. Both
   children get min-width:0 and the buttons are allowed to wrap. Found by
   `npm run qa:browser`, which caught it on two of the 21 Charlotte pages. */
.launch-inner > * {
  min-width: 0;
}

@media (min-width: 820px) {
  .launch-inner {
    grid-template-columns: 1fr auto;
    gap: var(--space-xl);
  }
}

/* One step down from a section heading. This is a qualifier on the page, not a
   section of it, and at --step-4 it competes with the h1 in the hero above. */
.launch-copy h2 {
  font-size: var(--step-2);
  color: var(--accent-deep);
}

.launch-copy p {
  margin-top: 6px;
  max-width: 62ch;
  color: var(--ink-muted);
}

.launch-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

/* The careers label is 32 characters and does not fit one line on a small
   phone. It wraps to two here rather than anywhere else on the site, so the
   override is scoped to this band and .btn keeps its nowrap everywhere else. */
.launch-actions .btn {
  white-space: normal;
  text-align: center;
  max-width: 100%;
}

/* White ink, not --dark. This is the mirror of the trap documented at the top of
   the file: --accent is a mid-dark red and navy, so it cannot carry dark text
   any more than it can be dark text. Measured on the rendered band before the
   change, --dark on --accent came out 3.24:1 on Albany and 2.55:1 on Charlotte,
   with the 0.82-alpha paragraph no better — failing on every page that ends in
   this band, which is nearly all of them. White is 5.74:1 and 6.81:1.
   axe never flagged it: it scores --dark against --accent and both are declared
   on the same element, but the band's ::before stripe overlay is a background
   image, and axe declines to judge contrast wherever one is involved. */
.cta-band {
  background: var(--accent);
  color: var(--white);
  position: relative;
  overflow: hidden;
}

/* --- the one thing on the page that moves without being asked ---------------
   Everything else here is scroll-triggered, which means every animation on the
   site is a consequence of the visitor doing something. One continuous accent
   earns its place, and this is the right band for it: it is the last thing
   before the footer, it is the only full-bleed accent surface on the page, and
   the stripes are already drawn — they just sat still.

   They now drift, at about 6px a second, which is slow enough that you notice
   the band is alive rather than notice the animation. A moving company hauling
   a diagonal band slowly to the right is also the one piece of motion here that
   means something.

   62.23px is not a taste number. The stripes repeat every 44px measured
   perpendicular to themselves, and at -45deg the horizontal distance that maps
   the pattern back onto itself is 44 / cos 45deg = 62.225. Any other value
   makes the loop visibly jump once per cycle, the same failure the review
   conveyor's comment describes for its -50%.

   The 80px of horizontal overhang is what the drift travels through; without it
   the pattern would run out and expose an edge inside .cta-band's overflow. It
   is a single composited transform on a decorative pseudo-element, so it costs
   nothing and cannot shift layout. */
.cta-band::before {
  content: "";
  position: absolute;
  inset: 0 -80px;
  background-image: repeating-linear-gradient(
    -45deg,
    rgba(var(--dark-rgb), 0.06) 0 16px,
    transparent 16px 44px
  );
  animation: cta-drift 10s linear infinite;
}

@keyframes cta-drift {
  from {
    transform: translate3d(0, 0, 0);
  }
  to {
    transform: translate3d(62.23px, 0, 0);
  }
}

.cta-inner {
  position: relative;
  display: grid;
  gap: var(--space-md);
  align-items: center;
  padding-block: var(--space-xl);
}

@media (min-width: 820px) {
  .cta-inner {
    grid-template-columns: 1fr auto;
  }
}

/* These two restated --dark on their own, so lightening the band's inherited
   colour alone would have left the heading and paragraph exactly as they were.
   White for both; the paragraph keeps a slight step down in weight rather than
   the old alpha, because 0.82 alpha over the accent was what put it at 2.55:1
   on Charlotte in the first place. */
.cta-inner h2 {
  color: var(--white);
  font-size: var(--step-3);
}

.cta-inner p {
  color: rgba(var(--cream-rgb), 0.94);
  margin-top: 6px;
}

.cta-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

.cta-band .btn-navy:hover {
  box-shadow: 0 8px 22px rgba(var(--dark-rgb), 0.3);
}

/* The ghost button sits on the accent too, so it follows the ink white with the
   rest of the band rather than staying a dark outline on a mid-dark ground. */
.cta-band .btn-ghost {
  --btn-fg: var(--white);
  border-color: rgba(var(--cream-rgb), 0.7);
}

.cta-band .btn-ghost:hover {
  background: var(--white);
  color: var(--accent-deep);
  border-color: var(--white);
}

/* ------------------------------------------------------------- footer ---- */
.site-footer {
  background: var(--dark-deep);
  color: var(--ink-muted-on-dark);
  font-size: 0.92rem;
}

.footer-grid {
  display: grid;
  gap: var(--space-lg);
  padding-block: var(--space-2xl);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
}

.footer-brand {
  display: grid;
  gap: var(--space-sm);
  align-content: start;
}

.footer-col h3 {
  font-size: 0.74rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent-on-dark);
  margin-bottom: 12px;
}

.footer-col a,
.footer-brand a {
  color: var(--ink-muted-on-dark);
  text-decoration: none;
}

.footer-col a {
  display: block;
  padding: 5px 0;
}

.footer-col a:hover,
.footer-brand a:hover {
  color: var(--accent-on-dark);
}

.footer-contact {
  display: grid;
  gap: 5px;
}

.footer-contact a {
  font-weight: 700;
  color: var(--white);
}

.footer-hours {
  display: grid;
  gap: 4px;
  font-size: 0.85rem;
}

.footer-hours div {
  display: flex;
  justify-content: space-between;
  gap: var(--space-sm);
  max-width: 260px;
}

.footer-legal {
  border-top: 1px solid var(--dark-line);
  padding-block: var(--space-md);
  font-size: 0.82rem;
}

.footer-legal .container {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-md);
  justify-content: space-between;
  align-items: center;
}

.footer-legal a {
  color: var(--ink-muted-on-dark);
  text-decoration: none;
}

.footer-legal a:hover {
  color: var(--accent-on-dark);
}

/* This sits inside the dark footer, so it takes the on-dark muted token. Using
   the light-ground --ink-muted here renders it near-invisible — it is the FMCSA
   and review-verification disclosure and has to stay readable. */
.disclosure {
  max-width: 78ch;
  font-size: 0.78rem;
  line-height: 1.6;
  color: var(--ink-muted-on-dark);
  padding-bottom: var(--space-md);
}

/* --------------------------------------------------------- mobile bar ---- */
.action-bar {
  position: fixed;
  z-index: 90;
  inset: auto 0 0 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
  background: rgba(var(--dark-rgb), 0.94);
  backdrop-filter: blur(10px);
  border-top: 1px solid var(--dark-line);
  transform: translateY(110%);
  transition: transform var(--dur-slow) var(--ease-out);
}

.action-bar.is-visible {
  transform: translateY(0);
}

@media (min-width: 860px) {
  .action-bar {
    display: none;
  }
}

body:has(.action-bar) {
  padding-bottom: 0;
}

@media (max-width: 859px) {
  .site-footer {
    padding-bottom: 84px;
  }
}

/* A job photograph standing in a two-column split, where a mascot pose used to.
   Rounded and cropped to a consistent landscape so four different photographs
   across four sections read as one treatment rather than four odd shapes. */
.media-rounded {
  width: 100%;
  aspect-ratio: 4 / 3;
  max-height: 62vh;
  object-fit: cover;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2);
}

/* Portrait source. 4:3 on a standing group shot crops it at the shins and the
   foreheads; 4:5 keeps the whole group and still sits inside the split. */
.media-rounded.media-portrait {
  aspect-ratio: 4 / 5;
}

/* ------------------------------------------------------------ reveals ---- */
/* Content is visible by default. The hidden state only exists once JS has
   confirmed it can drive an IntersectionObserver and added .reveal-ready, so a
   script error, an old browser or a blocked file can never leave a section
   invisible. This is the safe direction for the dependency to point. */
.reveal-ready [data-reveal] {
  opacity: 0;
  transform: translateY(22px);
  transition:
    opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}

/* --- variants ---------------------------------------------------------------
   Ray, 2026-08-09: "it kinda is all the same type of animation where it's just
   a reveal animation." He is right — every section on the page rose 22px and
   faded, forty-odd times on the homepage alone, and a gesture repeated that
   often stops being a gesture and becomes a page that loads slowly.

   Four variants, chosen so each one says something about the thing it is on
   rather than just being different for the sake of it:

     a section head wipes in from the lead edge, the direction you read from;
     a card grows into place, because a card is an object arriving;
     the about photograph wipes up, because a photograph is revealed, not moved;
     the copy beside it comes DOWN as the photograph comes up.

   Everything below only ever restates the HIDDEN state. The reset stays the one
   shared `.is-revealed` rule underneath, which is why these are written above it
   — every variant here is (0,3,0) and so is that rule, so it wins on source
   order and there is exactly one place where a reveal ends. Add a variant below
   that line and the element never reveals.

   NOTHING HERE MOVES ANYTHING SIDEWAYS, and that is not a stylistic choice. The
   first cut of this block slid the section heads -26px and the about copy +24px,
   which looked right and put 12px of horizontal document overflow on a 320px
   screen and 4px at 768 for as long as the hidden state lasted. .container is
   `100% - 2.5rem`, so the gutter is 20px a side and 12px below 400px — any
   lateral reveal larger than the gutter pushes the document wider while it
   waits, and `body { overflow-x: hidden }` hides the scrollbar rather than the
   problem. The original translateY(22px) was almost certainly chosen for this
   reason. Directional variety is carried by clip-path instead, which cannot
   overflow anything because it only ever removes paint.

   `transform` is what most of them touch, because it is already in the
   transition list above and already carries --reveal-delay. The two that need
   clip-path restate the whole shorthand rather than adding a second transition
   declaration that would replace it — the same trap the .market-card comment
   describes. */

/* Cards arrive rather than rise. The scale is 0.965 and not 0.9: a grid of six
   service cards all growing from small reads as a slideshow. Scaling down also
   cannot overflow, which is the other half of why it is this and not a slide. */
.reveal-ready .card[data-reveal] {
  transform: translate3d(0, 12px, 0) scale(0.965);
}

/* Heads wipe in from the lead edge — left to right, the direction the line is
   read in. The fade rides along so the leading edge is soft rather than a hard
   guillotine across the middle of a word.

   A browser without clip-path support gets that fade on its own, which is a
   normal reveal. Nothing is left hidden by the absence of the feature. */
.reveal-ready .section-head[data-reveal] {
  transform: none;
  clip-path: inset(0 100% 0 0);
  transition:
    opacity 620ms var(--ease-out) var(--reveal-delay, 0ms),
    clip-path 620ms var(--ease-out) var(--reveal-delay, 0ms);
}

/* inset(0) rather than `none` on all of these: `none` is not an interpolable
   value, so the wipe would snap instead of running. */
.reveal-ready .section-head[data-reveal].is-revealed {
  clip-path: inset(0 0 0 0);
}

/* The about band, in two halves that move against each other. The photograph
   wipes up out of its own frame at full opacity — a wipe that also fades is
   just a fade behind a mask — and the copy comes down to meet it. Opposed
   directions are what makes the band read as one gesture rather than as two
   sections that happen to be side by side. */
.reveal-ready .about-media[data-reveal] {
  opacity: 1;
  transform: scale(1.03);
  clip-path: inset(45% 0 0 0 round var(--radius-lg));
  transition:
    opacity var(--dur-slow) var(--ease-out) var(--reveal-delay, 0ms),
    transform 720ms var(--ease-out) var(--reveal-delay, 0ms),
    clip-path 720ms var(--ease-out) var(--reveal-delay, 0ms);
}

.reveal-ready .about-media[data-reveal].is-revealed {
  clip-path: inset(0 0 0 0 round var(--radius-lg));
}

.reveal-ready .about-copy > [data-reveal] {
  transform: translate3d(0, -18px, 0);
}

.reveal-ready [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
}

/* --- the step rule ---------------------------------------------------------
   The three "how it works" steps each sit under a 3px cream rule. The accent
   draws itself across that rule as the step arrives, left to right, so the row
   reads as a sequence being laid down rather than three boxes fading up.

   The dependency points the safe way round, which for a decoration means the
   opposite of the reveal system: the line is drawn by default and only the
   not-yet-revealed state retracts it. With JS off, blocked or thrown, all three
   rules are simply drawn. */
.step::after {
  content: "";
  position: absolute;
  inset: -3px 0 auto 0;
  height: 3px;
  background: var(--accent);
  transform-origin: left;
  transition: transform 620ms var(--ease-out) var(--reveal-delay, 0ms);
}

.reveal-ready .step[data-reveal]:not(.is-revealed)::after {
  transform: scaleX(0);
}

.section-dark .step::after {
  background: var(--accent-on-dark);
}

/* --------------------------------------------------------- misc blocks --- */
.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr));
  gap: var(--space-md);
}

.stat {
  display: grid;
  gap: 4px;
}

.stat b {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: var(--step-3);
  line-height: 1;
  color: var(--accent);
}

.stat span {
  font-size: 0.85rem;
  color: var(--ink-muted);
}

.section-dark .stat span {
  color: var(--ink-muted-on-dark);
}

.map-frame {
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--cream-deep);
  background: var(--cream-deep);
  aspect-ratio: 4 / 3;
  box-shadow: var(--shadow-1);
}

.map-frame iframe {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

.prose > * + * {
  margin-top: var(--space-md);
}

.prose h2 {
  font-size: var(--step-3);
  margin-top: var(--space-xl);
}

.prose h3 {
  font-size: var(--step-1);
  margin-top: var(--space-lg);
}

.prose ul,
.prose ol {
  display: grid;
  gap: 8px;
}

.prose li {
  color: var(--ink-muted);
}

.post-list {
  display: grid;
  gap: var(--space-md);
}

.post-item {
  display: grid;
  gap: 8px;
  padding: var(--space-md);
  background: var(--white);
  border: 1px solid var(--cream-deep);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
}

.post-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  font-size: 0.78rem;
  color: var(--ink-muted);
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.role-card {
  display: grid;
  gap: var(--space-sm);
  padding: var(--space-lg);
  background: var(--white);
  border: 1px solid var(--cream-deep);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1);
}

.role-card .role-market {
  font-family: var(--font-display);
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent-deep);
}

/* Kept deliberately compact: at 300px the mascot is 400px tall and pushed the
   headline and the recovery links past the fold on a 720px laptop, which is the
   one thing a 404 must not do. */
.notfound {
  display: grid;
  place-items: center;
  text-align: center;
  gap: var(--space-sm);
  padding-block: var(--space-xl) var(--space-2xl);
}

/* The grid centres the <picture>, not the <img> inside it. */
.notfound picture {
  display: contents;
}

.notfound img {
  width: min(100%, 210px);
  margin-inline: auto;
}

/* -------------------------------------------------- reduced motion ------- */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .reveal-ready [data-reveal] {
    opacity: 1;
    transform: none;
  }

  /* The rule above resets opacity and transform, which was every property the
     reveal system used until the variants landed. It is not enough for the two
     wipes: clip-path is not opacity and it is not transform, so without these
     two lines a section head would sit clipped to zero width — genuinely
     invisible, and exactly the failure the note on the star pop below is about
     — and the about photograph would sit clipped to its bottom 55%. `none` is
     fine here because nothing is interpolating any more. Any future variant
     that hides with a property outside {opacity, transform} has to be unhidden
     here too. */
  .reveal-ready .section-head[data-reveal],
  .reveal-ready .about-media[data-reveal] {
    clip-path: none;
  }

  /* Decoration whose default state is drawn, so this only matters for the
     window between a user turning the setting on and the observer firing. */
  .reveal-ready .step[data-reveal]:not(.is-revealed)::after {
    transform: none;
  }

  /* Scroll-driven, so `animation-duration: 0.01ms` above does nothing to it —
     a view() timeline never reads a duration. It has to be killed by name or it
     keeps tracking the scroll position, which is the most literal possible
     version of "motion tied to scrolling" for someone who asked for less of it. */
  .market-card-badge img {
    animation: none;
  }

  /* Infinite, and the global iteration-count: 1 would leave it parked at the
     end of one 62px cycle rather than at the start — visually identical, since
     the whole point of that number is that the pattern maps onto itself, but
     there is no reason to run it at all. */
  .cta-band::before {
    animation: none;
  }

  .hero-badge {
    animation: none;
  }

  /* The blanket 0.01ms above is not enough for the star pop, for the same
     reason it is not enough for the marquee: it shortens the duration and
     leaves animation-delay and fill-mode alone. With fill-mode: backwards the
     five stars would still hold their 0% frame — invisible — for up to 520ms
     and then snap in one at a time, which is a worse version of the effect
     rather than no effect. Killing the animation drops each star straight back
     to its base state, which is fully visible.

     Belt and braces: app.js already refuses to add .reveal-ready at all under
     reduced motion, so this only matters for a user who turns the setting on
     after the page has loaded. That is exactly the case a media query catches
     and a load-time matchMedia read does not. */
  .reveal-ready .trust-item-stars.is-revealed .pop {
    animation: none;
  }

  .action-bar {
    transition: none;
  }

  /* The blanket 0.01ms + iteration-count: 1 above is wrong for a marquee: it
     would run the whole conveyor instantly and leave the track parked at -50%,
     i.e. scrolled halfway into the duplicate set with the first reviews off
     screen. Kill the animation outright instead, drop the clone so the reviews
     are not listed twice, unmask the edges and hand the row to the user as a
     plain horizontal scroller. */
  .conveyor-track {
    animation: none !important;
    transform: none !important;
  }

  .conveyor-track > [aria-hidden="true"] {
    display: none;
  }

  .review-conveyor {
    overflow-x: auto;
    -webkit-mask-image: none;
    mask-image: none;
    mask: none;
  }
}

/* ------------------------------------------------------------- print ----- */
@media print {
  .site-header,
  .drawer,
  .action-bar,
  .cta-band {
    display: none !important;
  }

  body {
    background: #fff;
  }
}

/* ------------------------------------------------------- footer logo ------
   The full approved market logo. Capped rather than shown at native width —
   at 900px it would dwarf the footer columns. The badge already contains the
   wordmark, so no type sits beside it. */
.footer-logo {
  display: block;
  max-width: 260px;
  margin-bottom: var(--space-md);
}

.footer-logo img {
  width: 100%;
  height: auto;
}

@media (max-width: 720px) {
  .footer-logo {
    max-width: 200px;
  }
}

/* ------------------------------------------------------- photo hero -------
   A real photograph of the client's own work behind the headline. The scrim is
   what makes this work: the photo is a bright, busy, sunlit job site, and white
   text over it is unreadable without one. It ramps from solid brand dark on the
   text side to nearly clear on the image side, so the photo still reads as a
   photo while the copy keeps its contrast. Both QA gates check the result.

   The scrim is built from --dark-rgb, so it themes with the market for free. */
.hero-photo {
  position: relative;
}

/* A grid cell now, not a backdrop. It was `position: absolute; inset: 0` when
   the copy sat on top of it.

   The <img> is taken out of flow on purpose. Left in, a 1600x1600 source with
   `height: 100%` inside an auto-height grid item falls back to its intrinsic
   ratio, so the picture measured as tall as the column was wide (741px at 1440)
   and that, not the copy, became the row height. The hero then overhung the
   fold by exactly the difference. Out of flow, the row is sized by the copy and
   the min-height alone, and the photograph fills whatever that comes to. */
.hero-photo-media {
  position: relative;
  display: block;
  overflow: hidden;
  background: var(--cream-deep);
}

.hero-photo-media img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  /* One-shot settle on load: the photograph eases off a slight zoom as the
     headline cascades in. A single composited transform, and the global
     reduced-motion rule collapses it to its end state. */
  animation: hero-photo-settle 2.8s var(--ease-out) both;
}

@keyframes hero-photo-settle {
  from {
    transform: scale(1.045);
  }
  to {
    transform: scale(1);
  }
}

/* Deleted 2026-08-09: the scrim.

   There were two gradients here, 0.95 black down the left on desktop and 0.94
   flat on mobile, and between them they were the only thing holding the hero
   copy above 4.5:1. Ray: "take off the black fade from the site on the image,
   it doesn't look good, I don't like it being so black."

   Nothing replaces them, because nothing needs to. The copy moved off the
   photograph and onto its own cream surface, so contrast is now a property of
   the surface rather than something a gradient has to rescue. Anything that
   reintroduces text over this photograph brings the scrim back with it. */

.hero-photo {
  background: var(--cream);
  color: var(--ink);
}

/* The two ambient layers on .hero, an accent radial and a diagonal hatch, were
   drawn for the dark mascot hero. On cream the radial reads as a pink stain. */
.hero-photo::before,
.hero-photo::after {
  content: none;
}

.hero-split {
  display: grid;
  grid-template-columns: 1fr;
}

.hero-copy {
  display: flex;
  align-items: center;
  padding-block: var(--space-2xl);
}

/* The copy column is flush to the viewport's left edge, but its text has to
   line up with .container on every section below it, or the whole page reads as
   slightly loose. .container is `min(100% - 2.5rem, var(--wrap))` centred, so
   its left gutter at wide viewports is (100vw - wrap)/2, and 1.25rem below the
   point where the wrap kicks in. max() reproduces both cases in one expression. */
.hero-copy-inner {
  width: 100%;
  padding-inline: max(1.25rem, (100vw - var(--wrap)) / 2) var(--space-xl);
}

/* The badges used to sit against the mascot artwork. With a photograph behind
   them they need their own surface to stay legible. */
.hero-photo .hero-media {
  min-height: 340px;
  align-items: center;
  gap: var(--space-md);
  flex-direction: column;
  justify-content: center;
}

.hero-photo .hero-badge {
  position: static;
  animation: none;
}

/* Ink on cream, where every one of these used to be cream on a scrim. */
.hero-photo h1,
.hero-photo .hero-locations a {
  color: var(--ink);
}

.hero-photo h1 .accent {
  color: var(--accent);
}

.hero-photo .eyebrow {
  color: var(--accent);
}

.hero-photo .hero-sub {
  color: var(--ink-muted);
}

.hero-photo .hero-locations {
  color: var(--ink-muted);
}

.hero-photo .hero-locations a:hover {
  color: var(--accent);
}

/* Stacked below 940px, copy first and the photograph as a band under it.
   Leading with the photo was the first instinct and it does not survive the
   arithmetic: on a 375x812 phone the header already eats 275px, so a band deep
   enough to be worth looking at pushes "Get a Free Quote" past 900px and off
   the first screen. The headline and both CTAs land above the fold this way,
   and the truck is one short scroll under them.

   The band takes a fixed aspect rather than a viewport height. A dvh here would
   resize it every time mobile Safari's toolbar collapses, reflowing whatever
   sits under it mid-scroll. */
.hero-photo .hero-photo-media {
  aspect-ratio: 5 / 4;
}

@media (min-width: 940px) {
  /* Full-height, which is the other half of Ray's note. Both bars come off the
     top: .site-header is sticky and therefore still in flow, and the utility
     strip sits above it and is open at rest. Subtracting only --header-h left
     the "Serving Albany, NY / Charlotte, NC" row 76px below the fold on a
     900-tall window, which is the same complaint the hero already had.

     dvh is stated after vh as the progressive upgrade; on desktop the two
     resolve to the same number anyway. */
  .hero-photo .hero-split {
    grid-template-columns: minmax(0, 0.96fr) minmax(0, 1.04fr);
    min-height: calc(100vh - var(--header-h) - var(--utility-h));
    min-height: calc(100dvh - var(--header-h) - var(--utility-h));
  }

  /* 0.96fr rather than the 0.86 this started at. At 1440 the narrower column
     gave the text 435px, and the two CTAs measure 216 + 208 plus the gap, so
     they wrapped onto two rows and the sub-paragraph ran to three lines. The
     photo slot is still slightly the wider of the two, and still square enough
     for the 1:1 crop it is fed. */

  /* Padding that yields when the window is short. A constant here is the trap
     the old hero-grid comment describes: any amount that helps a 1080px screen
     is the same amount taken off the fold at 720. At 900 tall this resolves to
     29px and the whole block clears the fold; at 1080 it opens back up to the
     full space-xl. */
  .hero-photo .hero-copy {
    padding-block: clamp(1.25rem, 3.2dvh, var(--space-xl));
  }

  .hero-photo .hero-photo-media {
    aspect-ratio: auto;
    max-height: none;
    height: 100%;
  }
}

/* Short laptop windows. The copy block measures 628px at 1280x720 against 556px
   of room, and the overflow lands on the "Serving Albany, NY / Charlotte, NC"
   row. The h1 is what costs the height: at 64px in a 509px column the headline
   takes four lines and 276px on its own.

   Trimming it gives back about 70px, which is the whole overflow. The ramp is
   tied to vh so it recovers as the window grows: 55px at 720, 60px at 820, and
   above that the media query stops applying and the normal 64px returns. An
   earlier attempt used a shallower ramp and landed on 46px, which fit fine and
   read as a subheading. Only the hero h1 moves, and only when the window is
   genuinely short, so nothing changes on a 1080 screen or on a phone. */
@media (min-width: 940px) and (max-height: 820px) {
  .hero-photo h1 {
    font-size: clamp(3.2rem, 5.6vh + 0.9rem, 3.9rem);
  }

  .hero-photo .hero-sub {
    margin-top: var(--space-sm);
  }
}

@media (max-width: 760px) {
  /* What used to live here was the mobile half of the scrim, a flat
     0.94-to-0.72 black wash top to bottom, plus an object-position nudge that
     pushed the truck down out of the headline's way. Both were consequences of
     printing the copy on the photograph. Stacked, neither is needed: the phone
     gets the picture undimmed and the copy on cream underneath it.

     The contrast numbers that block was defending are also gone, and good
     riddance. They were measured by sampling rendered pixels because axe will
     not judge text over a photograph, and the worst of them, the "Serving
     Albany, NY / Charlotte, NC" row, sat at 1.11:1 before that retune. On cream
     the same row is 7.4:1 by construction. */
  .hero-photo .hero-media {
    min-height: 0;
    flex-direction: row;
    justify-content: flex-start;
  }
}

/* ============================================================== about band ==

   Added 2026-08-09 against Ray's "looks a little bland" note. It carries the
   about copy his live site has and this build did not, and it is deliberately
   the one section on the page that is not a centred heading over a grid.

   Three things do the work: a photograph that bleeds to the panel edge, a
   two-column tick list instead of cards, and three oversized numerals. The
   numerals are the only place type is used as a graphic element here, which is
   what makes the band read as a stop rather than as more page.             */

.about-band {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: clamp(1.75rem, 1rem + 3vw, 3.25rem);
  align-items: center;
}

.about-media {
  position: relative;
  border-radius: var(--radius-lg);
  overflow: hidden;
  /* Portrait-ish so it holds its own beside four rows of ticks plus the stats.
     The source is 1000x1065, so cover crops a little off the sides, not the
     subject. */
  aspect-ratio: 4 / 5;
  background: var(--dark);
  box-shadow: 0 22px 50px -28px rgba(var(--dark-rgb), 0.55);
}

.about-media picture {
  display: contents;
}

.about-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Source is 1000x960 into a 4:5 box, so cover crops width only, 72px a side.
     Biased left because both movers are on that side of the frame. */
  object-position: 40% center;
}

/* The accent keyline reads as intent rather than as a stray border because it
   traces the same radius as the photo it sits inside. */
.about-media::after {
  content: "";
  position: absolute;
  inset: 10px;
  border: 2px solid rgba(255, 255, 255, 0.34);
  border-radius: calc(var(--radius-lg) - 6px);
  pointer-events: none;
}

.about-copy > * + * {
  margin-top: var(--space-sm);
}

.about-copy .lede {
  max-width: 54ch;
}

.cred-list {
  list-style: none;
  padding: 0;
  margin-top: var(--space-lg) !important;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--space-md) var(--space-lg);
}

.cred-list li {
  display: flex;
  gap: 12px;
  align-items: flex-start;
}

.cred-list li > span:last-child {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.cred-list strong {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--step-0);
  letter-spacing: -0.01em;
}

.cred-list span span {
  font-size: var(--step--1);
  color: var(--ink-muted);
  line-height: 1.45;
}

/* Filled disc rather than a bare tick. At this size a stroked check floating in
   text reads as a bullet glyph; the disc makes it a mark. */
.cred-ico {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  display: grid;
  place-items: center;
  margin-top: 2px;
}

.stat-row {
  margin-top: var(--space-lg) !important;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-lg);
  padding-top: var(--space-lg);
  border-top: 1px solid rgba(var(--dark-rgb), 0.12);
}

.stat-row div {
  display: flex;
  flex-direction: column;
}

.stat-row dt {
  font-family: var(--font-display);
  font-weight: 900;
  font-size: var(--step-5);
  line-height: 1;
  letter-spacing: -0.03em;
  /* --accent-deep, not --accent: this is large type but it sits on cream beside
     small muted labels, and the deep tone holds up in both places. */
  color: var(--accent-deep);
}

.stat-row dd {
  margin: 6px 0 0;
  font-size: var(--step--1);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

@media (max-width: 900px) {
  .about-band {
    grid-template-columns: 1fr;
  }

  /* Copy first on narrow screens. The photograph is support here, not the point,
     and 4:5 of it above the heading pushes the credentials off the screen.

     Square, not 16:10. The source is 1000x960, so a 1:1 box crops almost nothing
     while 16:10 took the top off both movers' heads. */
  .about-media {
    order: 2;
    aspect-ratio: 1 / 1;
  }

  .about-copy {
    order: 1;
  }
}

/* --- reputation band, dark ground ---
   The section moved from cream to dark. Two things needed re-pointing and one
   thing turned out not to: the conveyor's soft edges are a mask-image, which
   reads only the alpha channel, so they work on any ground without help. */
.section-reputation .review-quote {
  /* Kept white. The point of the move was to give these cards an edge, and on
     the dark band a white card is lit rather than merely present. The hairline
     goes from cream-deep, which vanishes here, to a light rim.

     `color` is the load-bearing line. .review-quote sets a background but never
     a colour, so the blockquote inside it inherited from .section-dark — light
     ink, on a white card. The quotes went invisible the moment this section
     moved, and the attribution underneath did not, because figcaption sets
     --ink-muted explicitly. Anything white-on-dark-section has to re-assert its
     own ink; inheriting is what breaks. */
  color: var(--ink);
  border-color: rgba(255, 255, 255, 0.16);
  box-shadow: 0 18px 42px -26px rgba(0, 0, 0, 0.85);
}

.section-reputation .crest-label {
  /* --ink is near-black. On the dark band the label inside the wreath was
     invisible. */
  color: var(--white);
}

/* --- wide section head ---
   .section-head is capped at 66ch. Over a full-width three-column grid that
   leaves roughly half the container empty beside the heading, and the homepage
   ran four of those in a row — a real part of why the middle of the page read
   as flat. This modifier drops the cap above 1000px and sets the heading and
   its supporting line side by side, bottoms aligned, so the head spans the same
   width as the grid underneath it.

   Opt-in rather than a change to .section-head, because several heads are
   deliberately centred and narrow (the reputation and FAQ blocks) and would be
   wrecked by this. */
@media (min-width: 1000px) {
  .section-head-wide {
    max-width: none;
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
    column-gap: clamp(2rem, 1rem + 3vw, 4rem);
    align-items: end;
  }

  .section-head-wide .eyebrow {
    grid-column: 1;
    grid-row: 1;
  }

  .section-head-wide h2 {
    grid-column: 1;
    grid-row: 2;
    margin-bottom: 0;
  }

  .section-head-wide > p {
    grid-column: 2;
    grid-row: 2;
    margin-top: 0;
    max-width: 48ch;
    /* Optical, not mathematical: the h2 is 900-weight display type with a much
       larger cap height, so a true baseline match sits the body copy too low. */
    padding-bottom: 0.35em;
  }
}
