:root {
  /* v3 — premium direction, researched against real top US mover sites
     (Allied, JK Moving, FlatRate NYC): confident sans display type, real
     photography, soft realistic shadows, restrained single accent — not the
     comic-mascot treatment of v2, and not apache-moving's palette of v1.
     Navy is a deepened, muted read of the mascot's blue — present in the
     brand without reading as a cartoon primary blue. */
  --navy: #0a2f5c;
  --navy-mid: #123c72;
  --navy-deep: #06213f;
  --navy-soft: #eef2f7;
  --red: #c81e2e;
  --red-light: #e0384a;
  --red-dark: #8f1119;
  /* The mascot's actual mask/shirt blue — brighter and more visibly "blue"
     than the muted --navy pair, used on buttons so the logo's blue shows up
     as a real color in the UI, not just a dark neutral. */
  --blue: #1e6fd9;
  --blue-light: #4a94ee;
  --blue-dark: #14509e;
  /* Warm metallic gold — the one enrichment accent added on top of the v3
     navy/red pair, used only for fine detail (rules, numerals, small glows)
     so the palette gains depth without gaining a third competing hue. */
  --gold: #b8892f;
  --gold-light: #e0bc6b;
  --ink: #14181f;
  --paper: #ffffff;
  /* Section-background tints — visibly colored, not near-white, so content
     blocks read as distinct rooms rather than one long white page. Each
     pairs with one of the three brand hues (gold/navy/red) instead of
     being a generic gray. */
  --cream: #f8f1e2;
  --mist: #e9eff5;
  --blush: #fbeceb;
  --navy-soft: #dfe9f5;
  --line: #e4e3e0;
  --text: #14181f;
  --text-dim: #4b5563;
  --text-mute: #8a8f98;

  /* Archivo at heavy weight — a tougher, more serious grotesk than the
     original Space Grotesk pass, better suited to a moving/trucking brand
     that wants to read as sturdy rather than tech-friendly. Still used for
     UI-level type: buttons, eyebrows, card headings, numerals. */
  --font-display: 'Archivo', 'Helvetica Neue', Arial, sans-serif;
  /* Fraunces at black weight for the big display moments (h1/h2) only - a
     high-contrast serif with real character, not a second generic grotesk.
     Reads as premium editorial rather than "another SaaS landing page,"
     while staying heavy/confident rather than delicate. */
  --font-serif: 'Fraunces', Georgia, 'Times New Roman', serif;
  --font: 'Inter', 'Helvetica Neue', Arial, sans-serif;

  --space-1: 6px; --space-2: 10px; --space-3: 16px; --space-4: 24px;
  /* --space-6/7/8 compressed from an earlier 52/76/108 pass - that much
     vertical padding, repeated across 8+ sections on the homepage, added a
     lot of scroll distance without adding content. */
  --space-5: 36px; --space-6: 40px; --space-7: 56px; --space-8: 72px;
  --r-lg: 12px; --r-md: 10px; --r-sm: 6px; --pill: 999px;

  --shadow-card: 0 1px 2px rgba(10,20,40,.04), 0 8px 24px rgba(10,20,40,.07);
  --shadow-card-hover: 0 4px 8px rgba(10,20,40,.05), 0 20px 40px rgba(10,20,40,.12);
  --shadow-btn: 0 10px 24px -8px rgba(200,30,46,.5);
  --shadow-lift: 0 24px 60px -18px rgba(10,20,40,.28);

  /* Gradients — the enrichment layer. Dark surfaces get a diagonal navy
     gradient instead of a flat fill; the primary CTA gets a subtle red
     gradient for depth; gold-line is a thin fading rule used under eyebrows. */
  --grad-navy: linear-gradient(135deg, var(--navy-deep) 0%, var(--navy) 55%, var(--navy-mid) 100%);
  --grad-red: linear-gradient(135deg, var(--red-light) 0%, var(--red) 55%, var(--red-dark) 100%);
  --grad-red-hover: linear-gradient(135deg, var(--red) 0%, var(--red-dark) 100%);
  --grad-gold-text: linear-gradient(120deg, var(--gold) 0%, var(--gold-light) 45%, var(--gold) 100%);
  --grad-gold-line: linear-gradient(90deg, var(--gold) 0%, rgba(184,137,47,0) 100%);
  --grad-navy-bar: linear-gradient(90deg, var(--navy), var(--navy-mid));
  --grad-red-bar: linear-gradient(90deg, var(--red-dark), var(--red-light));
  --grad-gold-bar: linear-gradient(90deg, var(--gold), var(--gold-light));
  --grad-blue: linear-gradient(135deg, var(--blue-light) 0%, var(--blue) 55%, var(--blue-dark) 100%);
  --grad-blue-hover: linear-gradient(135deg, var(--blue) 0%, var(--blue-dark) 100%);

  /* Section backgrounds as soft two-stop gradients instead of one flat
     tint - each section reads as lit from a corner rather than a single
     paint-bucket fill. */
  --grad-cream: linear-gradient(160deg, #f6e9c9 0%, #edd7a3 100%);
  --grad-mist: linear-gradient(160deg, #dbe7f5 0%, #c1d7ec 100%);
  --grad-blush: linear-gradient(160deg, #f8dcd9 0%, #efc2bd 100%);
  --grad-navy-soft: linear-gradient(160deg, #cfe0f2 0%, #b0cbe6 100%);
  /* Near-black, pulled from the logo's own "MOVE MAN" wordmark - a distinct
     third dark tone from navy, giving one section real gravitas/trust
     instead of another soft pastel wash. */
  --grad-ink: linear-gradient(160deg, #1c2027 0%, #0a0c10 100%);

  /* Warm paper/ledger palette (editorial direction sample) - was
     accidentally declared outside this block on an earlier pass, which
     silently made every var(--paper-warm) etc. reference resolve to
     nothing (an undefined custom property), turning anything using it
     transparent instead of the intended opaque cream. */
  --paper-warm: #f7f2e6;
  --paper-warm-deep: #efe3c9;
  --linen: #ddceac;
  --ink-warm: #1c1712;
}

* { box-sizing: border-box; margin: 0; padding: 0; }
/* Guards against macOS/trackpad rubber-band overscroll flashing the
   browser's own canvas past the real end of the page. Also tints that
   canvas navy instead of white, since every page now ends on the dark
   footer/CTA anyway. (An earlier attempt used min-height:100vh + flex on
   body to handle short pages, but that produced a ~140px phantom gap below
   the real content in some cases - removed. All four pages now have enough
   real content to exceed a typical viewport height on their own.) */
/* overflow-x:hidden is repeated on html (not just body) because on real
   mobile Safari a body-only rule doesn't reliably stop the page's own
   horizontal rubber-band swipe - it still reveals a strip of html's
   background past the edge even though nothing is meant to scroll there.
   overscroll-behavior-Y was dropped entirely (was here only for a cosmetic
   macOS rubber-band color flash) after it turned out to fully disable
   trackpad/wheel vertical scrolling in Chrome on macOS - confirmed live:
   arrow keys still scrolled, only the gesture path was dead, which pointed
   straight at this property. Losing a rare colored flash at the very
   top/bottom of the page is a trivial price for scrolling actually working. */
/* scroll-behavior: smooth deliberately left off here - global smooth-scroll
   on <html> is a known Chrome bug trigger: each mouse-wheel tick queues its
   own smooth-scroll animation, and rapid ticks stack/conflict, making the
   whole page feel unresponsive to further scroll input (confirmed live -
   removing just this one property was the difference between totally
   stuck and fully working scroll). The one place that wants an animated
   scroll (the room-picker jump to the calculator) already requests it
   explicitly via scrollIntoView({behavior:'smooth'}) in JS, which works
   independently of this CSS property. */
html { background: var(--navy-deep); overflow-x: hidden; }
body {
  font-family: var(--font); color: var(--text); background: var(--paper); overflow-x: hidden; -webkit-font-smoothing: antialiased;
}
/* overscroll-behavior-x is scoped to touch/coarse-pointer devices only (the
   mobile Safari horizontal-swipe bug above) rather than set globally - on a
   fine-pointer/trackpad desktop (Mac Chrome), a large, slightly-diagonal
   trackpad swipe near the top or bottom of a short page (e.g. contact.html,
   which has little content above the fold) got its whole gesture rejected
   and rubber-banded back to where it started, because the browser's
   axis-lock/overscroll-boundary handling saw the tiny horizontal component
   blocked by overscroll-behavior-x and bounced the entire swipe instead of
   just the horizontal sliver - confirmed by it only ever happening on big,
   fast swipes right at a scroll boundary, and only ever taking a second,
   separate swipe to actually move. overflow-x:hidden above already fully
   prevents the mobile bug on its own (there's nothing to rubber-band
   sideways into), so desktop loses nothing by not layering this on top. */
@media (pointer: coarse) {
  html, body { overscroll-behavior-x: none; }
}
a { text-decoration: none; color: inherit; }
img { max-width: 100%; display: block; }
button { font-family: inherit; cursor: pointer; }
h1, h2, h3 { font-family: var(--font-display); font-weight: 800; line-height: 1.06; color: var(--navy-deep); letter-spacing: -.015em; }
/* The two biggest display moments on any page get the serif - h3 (card/UI
   level headings) stays on the sturdier grotesk, so the serif reads as an
   occasional, deliberate event rather than the whole site's voice. */
h1, h2 { font-family: var(--font-serif); font-weight: 900; font-optical-sizing: auto; letter-spacing: -.01em; }

.container { max-width: 1200px; margin: 0 auto; padding: 0 32px; }
@media (max-width: 640px) { .container { padding: 0 20px; } }
.edge { width: 100%; box-sizing: border-box; padding: 0 clamp(20px, 4.5vw, 96px); }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  background: var(--grad-red); color: #fff; border: none; border-radius: var(--r-sm);
  padding: 15px 32px; font-family: var(--font-display); font-weight: 700; font-size: 16px;
  box-shadow: var(--shadow-btn); transition: transform .16s ease, box-shadow .16s ease, background .16s ease;
}
.btn:hover { transform: translateY(-2px); background: var(--grad-red-hover); box-shadow: 0 14px 28px -8px rgba(200,30,46,.6); }
/* Secondary CTA — filled with the bright mascot blue rather than a
   transparent/white outline, so it reads as its own color, not "no color". */
.btn-outline {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  background: var(--grad-blue); color: #fff; border: 1.5px solid rgba(255,255,255,.35);
  border-radius: var(--r-sm); padding: 14px 30px; font-family: var(--font-display); font-weight: 700; font-size: 16px;
  box-shadow: 0 10px 24px -8px rgba(30,111,217,.5); transition: transform .16s ease, box-shadow .16s ease, background .16s ease;
}
.btn-outline:hover { transform: translateY(-2px); background: var(--grad-blue-hover); box-shadow: 0 14px 28px -8px rgba(30,111,217,.6); }
/* Frosted-glass button — reserved for buttons sitting directly on a photo
   (the hero), where a translucent blur actually has something behind it to
   blur, instead of a flat gradient there too. */
.btn-glass {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  background: rgba(255,255,255,.16); backdrop-filter: blur(10px) saturate(1.3); -webkit-backdrop-filter: blur(10px) saturate(1.3);
  color: #fff; border: 1.5px solid rgba(255,255,255,.55);
  border-radius: var(--r-sm); padding: 14px 30px; font-family: var(--font-display); font-weight: 700; font-size: 16px;
  transition: background .16s ease, border-color .16s ease, transform .16s ease;
}
.btn-glass:hover { background: rgba(255,255,255,.26); border-color: #fff; transform: translateY(-2px); }
/* On light backgrounds, a warm beige fill instead of transparent/white. */
.btn-outline.on-light {
  background: linear-gradient(135deg, var(--gold-light) 0%, var(--gold) 100%); color: #fff;
  border-color: var(--gold); box-shadow: 0 10px 22px -10px rgba(184,137,47,.55); transition: transform .16s ease, box-shadow .16s ease;
}
.btn-outline.on-light:hover { transform: translateY(-2px); box-shadow: 0 14px 28px -10px rgba(184,137,47,.65); }

.card {
  --card-tint: rgba(10,47,92,.09);
  background: linear-gradient(180deg, var(--card-tint) 0%, var(--paper) 180px), var(--paper);
  border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow-card); padding: var(--space-5); position: relative; overflow: hidden;
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
.card:hover { transform: translateY(-3px); box-shadow: var(--shadow-card-hover); border-color: #d7d5d0; }
/* Top accent bar, cycled navy/red/gold across a card grid via nth-child so
   a row of cards reads as one deliberate rhythm rather than one flat color
   repeated N times - and the same hue washes a little way down into the
   card body (--card-tint, set alongside each bar) so the color isn't just
   a 4px sliver sitting on an otherwise all-white card. */
.card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 5px; background: var(--grad-navy-bar); }
.card-grid > .card:nth-child(3n+2) { --card-tint: rgba(200,30,46,.08); }
.card-grid > .card:nth-child(3n+2)::before { background: var(--grad-red-bar); }
.card-grid > .card:nth-child(3n+3) { --card-tint: rgba(184,137,47,.1); }
.card-grid > .card:nth-child(3n+3)::before { background: var(--grad-gold-bar); }
/* Glass card variant — for cards sitting on a colored section gradient
   (testimonials) rather than needing maximum text contrast (FAQ, services).
   Frosted and translucent instead of the solid-white + tint-wash treatment. */
.card--glass {
  background: rgba(255,255,255,.6); backdrop-filter: blur(14px) saturate(1.3); -webkit-backdrop-filter: blur(14px) saturate(1.3);
  border: 1px solid rgba(255,255,255,.55);
}
/* A soft tint of the same accent behind the card on hover - the "colored
   shadow" cue several premium SaaS sites use for depth. */
.card-grid > .card:nth-child(3n+1):hover { box-shadow: var(--shadow-card-hover), 0 22px 48px -26px rgba(10,47,92,.4); }
.card-grid > .card:nth-child(3n+2):hover { box-shadow: var(--shadow-card-hover), 0 22px 48px -26px rgba(200,30,46,.35); }
.card-grid > .card:nth-child(3n+3):hover { box-shadow: var(--shadow-card-hover), 0 22px 48px -26px rgba(184,137,47,.35); }

/* Service-card icons - custom line-art (not stock emoji), cycled
   navy/red/gold in lockstep with the card's own top-bar/tint rhythm above
   so each icon reads as that card's accent color, not a random hue. */
.service-icon {
  width: 52px; height: 52px; border-radius: var(--r-md); margin-bottom: var(--space-3);
  display: flex; align-items: center; justify-content: center;
  background: var(--card-tint, rgba(10,47,92,.09)); color: var(--navy-deep);
}
.service-icon svg { width: 28px; height: 28px; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.card-grid > .card:nth-child(3n+2) .service-icon { color: var(--red); }
.card-grid > .card:nth-child(3n+3) .service-icon { color: var(--gold); }

/* 3D tilt - js/main.js sets --rx/--ry per card on mousemove. Perspective
   lives on the grid so cards tilt toward a shared vanishing point rather
   than each rotating around its own center. */
.card-grid { perspective: 1200px; }
.card-grid > .card { transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) translateY(0); transition: transform .35s cubic-bezier(.16,1,.3,1), box-shadow .18s ease, border-color .18s ease; }
.card-grid > .card:hover { transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) translateY(-4px); transition: transform .08s linear, box-shadow .18s ease, border-color .18s ease; }
@media (prefers-reduced-motion: reduce), (hover: none) {
  .card-grid > .card { transform: none !important; }
}

.eyebrow {
  display: inline-block; font-size: 12.5px; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--red); margin-bottom: var(--space-2);
}
/* Thin fading gold rule under an eyebrow — the enrichment layer's smallest,
   most repeatable touch of richness. */
.eyebrow-rule { width: 64px; height: 3px; background: var(--grad-gold-line); border-radius: 2px; margin-bottom: var(--space-3); }

/* Metallic gradient text — spent on exactly one moment per page (a stat
   number or a single headline word). Animated: the gradient itself drifts
   left-to-right on a slow loop so the metallic read keeps catching light
   instead of sitting static. */
.gold-text {
  background: linear-gradient(110deg, var(--gold) 15%, var(--gold-light) 35%, #fff3d6 50%, var(--gold-light) 65%, var(--gold) 85%);
  background-size: 240% 100%;
  -webkit-background-clip: text; background-clip: text;
  color: transparent; -webkit-text-fill-color: transparent;
  animation: shimmer 6s ease-in-out infinite;
}
@keyframes shimmer { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } }
@media (prefers-reduced-motion: reduce) { .gold-text { animation: none; } }

/* Soft radial glow — a restrained mesh accent for light sections, sitting on
   a decorative sibling layer so it never clips a sticky/floating child. Given
   a slow ambient drift so the "volume" reads as alive, not a static print. */
.glow { position: relative; isolation: isolate; }
.glow::before {
  content: ''; position: absolute; z-index: -1; pointer-events: none;
  top: -120px; right: -80px; width: 480px; height: 480px; border-radius: 50%;
  background: radial-gradient(circle, rgba(10,47,92,.07), transparent 70%);
  animation: driftA 16s ease-in-out infinite;
}
.glow::after {
  content: ''; position: absolute; z-index: -1; pointer-events: none;
  bottom: -140px; left: -100px; width: 420px; height: 420px; border-radius: 50%;
  background: radial-gradient(circle, rgba(184,137,47,.06), transparent 70%);
  animation: driftB 19s ease-in-out infinite;
}
@keyframes driftA { 0%, 100% { transform: translate(0,0) scale(1); } 50% { transform: translate(-24px,22px) scale(1.08); } }
@keyframes driftB { 0%, 100% { transform: translate(0,0) scale(1); } 50% { transform: translate(20px,-18px) scale(1.06); } }
@media (prefers-reduced-motion: reduce) {
  .glow::before, .glow::after { animation: none; }
}

/* Second decorative language: a faint dot grid, for sections that need
   texture without a shape competing with their content. Lives on the same
   isolated z-index:-1 layer as .glow. */
.dots { position: relative; isolation: isolate; }
.dots::before {
  content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background-image: radial-gradient(var(--navy) 1px, transparent 1px);
  background-size: 22px 22px; opacity: .06;
}

/* Stat bar — the page's signature device: real facts (years in business,
   rating, license), not decoration, presented with the confident weight
   premium mover sites give their trust numbers. Floats over the hero photo's
   bottom edge on desktop. */
/* Glass panel: it floats across the seam between the hero photo and the
   section below, so a frosted, translucent treatment reads as material
   rather than a plain white card pasted on top. */
.stat-bar {
  display: grid; grid-template-columns: repeat(3, minmax(0,1fr));
  background: rgba(255,255,255,.68); backdrop-filter: blur(18px) saturate(1.4); -webkit-backdrop-filter: blur(18px) saturate(1.4);
  border: 1px solid rgba(255,255,255,.5);
  border-radius: var(--r-lg); box-shadow: var(--shadow-lift); position: relative; overflow: hidden;
}
/* Rounds its own top corners to match the card instead of relying purely
   on the card's overflow:hidden to clip it - iOS Safari doesn't reliably
   clip a child against a backdrop-filter parent's border-radius, so this
   plain top bar was poking past the rounded corner on real phones. */
.stat-bar::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: var(--grad-gold-bar); border-radius: var(--r-lg) var(--r-lg) 0 0; }
.stat-bar > div { padding: var(--space-4) var(--space-3); text-align: center; border-right: 1px solid var(--line); }
.stat-bar > div:last-child { border-right: none; }
.stat-bar .num { font-family: var(--font-display); font-size: clamp(26px,3vw,34px); font-weight: 700; color: var(--navy-deep); }
.stat-bar .label { font-size: 12.5px; color: var(--text-mute); text-transform: uppercase; letter-spacing: .04em; margin-top: 4px; }
@media (max-width: 480px) {
  .stat-bar > div { padding: 12px 6px; }
  .stat-bar .num { font-size: 19px; }
  .stat-bar .label { font-size: 9.5px; }
}

/* Floating glass capsule instead of a full-bleed bar: fixed (not sticky) so
   it never reserves layout space of its own - the hero photo behind it
   starts at the true top of the page instead of leaving a blank strip above
   it - and inset from the viewport edges so the page is visible on both
   sides at all times, blurred/tinted rather than solid-opaque where it
   actually sits over content. Every page's first section gets matching
   top clearance (~136px) so its own content doesn't sit under the panel.
   The corners are deliberately asymmetric (sharp on two, rounded on the
   other two, diagonally) rather than a uniform rounded-pill. */
/* width uses vw, not %, deliberately - on a fixed-position box, some
   mobile viewport/emulation setups resolve percentage widths against a
   stale containing-block size instead of the real viewport, silently
   widening the bar enough to push the hamburger toggle off-screen. vw
   always resolves against the true viewport width. */
.site-header {
  position: fixed; top: 14px; left: 0; right: 0; z-index: 100;
  width: min(1180px, calc(100vw - 28px)); margin: 0 auto;
  background: rgba(255,255,255,.72);
  backdrop-filter: blur(18px) saturate(1.6); -webkit-backdrop-filter: blur(18px) saturate(1.6);
  border: 1px solid rgba(255,255,255,.6);
  border-radius: 6px 28px 6px 28px;
  box-shadow: 0 12px 30px -16px rgba(6,33,63,.35);
  transition: box-shadow .3s ease, background .3s ease, border-radius .3s ease, top .3s ease;
}
.site-header.is-scrolled {
  top: 8px; background: rgba(255,255,255,.88);
  box-shadow: 0 18px 40px -16px rgba(6,33,63,.45);
  border-radius: 6px 20px 6px 20px;
}
.site-header-inner { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); padding: 5px 22px 5px 26px; }
.site-header nav ul { display: flex; gap: var(--space-5); list-style: none; }
.site-header nav a { font-weight: 700; font-size: 20px; color: var(--navy-deep); }
/* Kept slightly smaller than a plain .btn so the bar itself stays slim -
   the nav links carry the size/prominence in this header, not the logo
   or the button. */
.site-header .btn { padding: 13px 28px; font-size: 15px; }
.site-header nav a.active { color: var(--red); }
/* Two-way segmented pill (both languages always visible, current one
   highlighted) rather than a single "switch to the other language" link -
   sits in the main header row next to the call button on every screen
   size, not tucked into the mobile dropdown. Full "English"/"Español"
   labels shrink to "EN"/"ES" at the same breakpoint the call button's own
   text shortens at, so the two compact together instead of one pushing
   the other off the already-tight mobile bar. */
/* Wrapped together with the call button in .lang-call-group so the two
   sit a fixed, tight distance apart regardless of container width - as
   top-level siblings in .site-header-inner's own space-between/gap
   layout, any slack in the header gets redistributed as EXTRA space
   between them (that's what space-between does with left-over room),
   which no amount of negative margin on .lang-toggle alone could undo. */
/* Wraps lang-call-group + the hamburger together so .site-header-inner's
   space-between only ever splits space between the logo and this one
   block. With lang-call-group, nav, and nav-toggle as three independent
   siblings, space-between spread the leftover room into two separate
   gaps of its own choosing - on mobile (nav hidden) that routinely left
   a cramped gap next to the lang toggle and a much wider one before the
   hamburger, since the two gaps only ever end up equal by coincidence,
   not by design. */
.header-controls { display: flex; align-items: center; gap: 16px; }
.lang-call-group { display: flex; align-items: center; gap: 10px; }
.lang-toggle {
  display: flex; align-items: center; border: 2.5px solid rgba(6,33,63,.3); border-radius: var(--pill);
  overflow: hidden; flex-shrink: 0;
}
.lang-option {
  padding: 9px 14px; font-family: var(--font-display); font-weight: 800; font-size: 13px;
  color: var(--navy-deep); letter-spacing: .02em; transition: background .15s ease, color .15s ease;
}
.lang-option.active { background: var(--navy-deep); color: #fff; }
.lang-option:not(.active):hover { background: rgba(10,47,92,.07); }
.lang-option .lang-short { display: none; }
.nav-toggle { display: none; background: none; border: none; font-size: 22px; flex-shrink: 0; }
/* The header-call-btn's full "Call (773) 873-7374" label doesn't fit
   next to the logo and hamburger on a phone-width screen - .call-short
   swaps in below 480px instead of letting the row overflow past the
   header's edge and push the hamburger off-screen entirely. */
.call-short { display: none; }
@media (max-width: 860px) {
  .site-header nav { display: none; }
  .nav-toggle { display: block; }
  /* Solid white, not translucent - a blurred/semi-see-through panel let the
     hero photo and headline bleed through behind the links on a real
     phone, reading as cheap rather than premium. Each link is now a full-
     width row with its own padding/divider instead of a bare left-aligned
     word floating in a mostly-empty box, so the panel actually looks
     designed at the width it has to fill. */
  .site-header nav.open {
    display: flex; position: absolute; top: 100%; left: 0; right: 0; margin-top: 8px;
    background: #fff;
    flex-direction: column; padding: var(--space-2);
    border: 1px solid var(--line); border-radius: 6px 22px 6px 22px;
    box-shadow: 0 24px 48px -16px rgba(6,33,63,.4);
  }
  .site-header nav.open ul { flex-direction: column; gap: 2px; }
  .site-header nav.open ul li a {
    display: block; padding: 15px var(--space-3); font-size: 16.5px;
    border-radius: 10px; border-bottom: 1px solid var(--line);
  }
  .site-header nav.open ul li:last-child a { border-bottom: none; }
  .site-header nav.open ul li a:hover { background: rgba(10,47,92,.06); }
  .site-header nav.open ul li a.active { color: var(--red); background: rgba(200,30,46,.06); }
}
@media (max-width: 640px) {
  /* Explicit left/right insets with width:auto instead of a computed
     width + margin:auto - margin:auto centering hit the same stale-
     containing-block issue described above in some mobile emulation
     setups, silently shifting the whole bar sideways even once its width
     was already correct. Plain insets need no centering math at all. */
  .site-header { left: 10px; right: auto; width: calc(100vw - 20px); margin: 0; border-radius: 6px 20px 6px 20px; }
  .site-header-inner { padding: 9px 16px 9px 18px; }
}
@media (max-width: 480px) {
  .site-header-inner { gap: var(--space-2); }
  .site-header-inner > a:first-child img { height: 40px; }
  .call-full { display: none; }
  .call-short { display: inline; }
  .header-call-btn { padding: 11px 18px; font-size: 14px; }
  .lang-option .lang-full { display: none; }
  .lang-option .lang-short { display: inline; }
  .lang-option { padding: 9px 10px; font-size: 12px; }
  .lang-call-group { gap: 6px; }
  .header-controls { gap: 10px; }
}

.site-footer { background: var(--grad-navy); color: rgba(255,255,255,.75); padding: var(--space-7) 0 var(--space-5); }
.site-footer a { color: #fff; }
.site-footer strong { color: #fff; }
.site-footer .legal { font-size: 13px; color: rgba(255,255,255,.45); margin-top: var(--space-5); }

/* ===== Motion ===== *
   One orchestrated system, not scattered effects: sections/cards fade up
   into place as they're scrolled to (js/main.js toggles .in-view via
   IntersectionObserver), the hero plays a single staggered entrance on
   load, and stat numbers count up once when the stat bar comes into view.
   Everything here is inert until JS adds a class, so a no-JS or reduced-
   motion visit still gets the final, fully-visible layout. */
.reveal {
  opacity: 0; transform: translateY(34px) scale(.97); filter: blur(9px);
  transition: opacity .85s cubic-bezier(.16,1,.3,1), transform .85s cubic-bezier(.16,1,.3,1), filter .85s cubic-bezier(.16,1,.3,1);
}
.reveal.in-view { opacity: 1; transform: none; filter: blur(0); }
.card-grid.reveal-group > .card {
  opacity: 0; transform: translateY(34px) scale(.96); filter: blur(7px);
  transition: opacity .75s cubic-bezier(.16,1,.3,1), transform .75s cubic-bezier(.16,1,.3,1), filter .75s cubic-bezier(.16,1,.3,1);
}
.card-grid.reveal-group.in-view > .card { opacity: 1; transform: none; filter: blur(0); }
.card-grid.reveal-group > .card:nth-child(1) { transition-delay: .05s; }
.card-grid.reveal-group > .card:nth-child(2) { transition-delay: .12s; }
.card-grid.reveal-group > .card:nth-child(3) { transition-delay: .19s; }
.card-grid.reveal-group > .card:nth-child(4) { transition-delay: .26s; }
.card-grid.reveal-group > .card:nth-child(5) { transition-delay: .33s; }
.card-grid.reveal-group > .card:nth-child(6) { transition-delay: .4s; }

/* Hero entrance is sequenced to start once the curtain (below) has wiped
   away, not on page load - see .hero-curtain / initHeroCurtain in main.js.
   The headline uses .split-words instead of .hero-in - its own word-by-word
   stagger is timed from main.js to start alongside the first .hero-in.
   Delays key off sibling order (~) rather than nth-child position, so
   removing/adding an element before these two doesn't silently desync
   the stagger - the p fades in first, the button row right after it. */
.hero-in { opacity: 0; transform: translateY(22px); animation: heroIn .9s cubic-bezier(.16,1,.3,1) forwards; animation-delay: 2.05s; }
.hero-in ~ .hero-in { animation-delay: 2.2s; }
@keyframes heroIn { to { opacity: 1; transform: translateY(0); } }

@media (prefers-reduced-motion: reduce) {
  .reveal, .card-grid.reveal-group > .card, .hero-in { opacity: 1; transform: none; filter: none; animation: none; transition: none; }
  html { scroll-behavior: auto; }
}

/* ===== Floating quote button ===== *
   Fixed in the bottom-right corner on every page - large enough to notice
   at a glance, sized well short of a hero-level CTA so it doesn't crowd
   the corner of the screen. */
/* right uses the calc(Npx - (100vw - 100%)) correction - see the
   .site-header comment above for why: some mobile emulation setups resolve
   % against a stale containing-block size for fixed-position boxes while
   vw stays correct, so a plain "right:28px" silently drifts the button off
   past the true right edge on narrow screens. bottom stays a plain px
   value on purpose - the equivalent vh-based correction actively broke
   this on a real iPhone (Safari's 100vh includes the address-bar's
   collapsed/expanded height inconsistently, so (100vh - 100%) isn't the
   reliable no-op it is for vw, and it clipped the button below the fold). */
.fab-quote {
  position: fixed; right: calc(28px - (100vw - 100%)); bottom: 28px; z-index: 200;
  display: inline-flex; align-items: center; gap: 10px;
  background: var(--grad-red); color: #fff; border: none; border-radius: var(--pill);
  padding: 17px 30px; font-family: var(--font-display); font-weight: 800; font-size: 16px;
  box-shadow: var(--shadow-lift); transition: transform .15s ease, box-shadow .15s ease;
}
.fab-quote:hover { transform: scale(1.05); box-shadow: 0 28px 60px -14px rgba(200,30,46,.55); }
@media (max-width: 640px) {
  .fab-quote { right: calc(16px - (100vw - 100%)); bottom: 16px; padding: 14px 22px; font-size: 14.5px; }
}

/* One-time nudge shown only if the estimator teaser scrolls fully out of
   view without being clicked (js/main.js) - a small dismissible chip next
   to the always-there FAB, not another modal fighting for attention. */
.fab-nudge {
  position: fixed; right: calc(28px - (100vw - 100%)); bottom: 92px; z-index: 199;
  display: flex; align-items: center; gap: 10px;
  background: var(--paper); color: var(--navy-deep); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 12px 14px 12px 16px;
  font-family: var(--font-display); font-weight: 700; font-size: 13.5px;
  box-shadow: var(--shadow-lift); cursor: pointer;
  opacity: 0; transform: translateY(10px) scale(.96); pointer-events: none;
  transition: opacity .35s ease, transform .35s ease;
}
.fab-nudge.is-visible { opacity: 1; transform: none; pointer-events: auto; }
.fab-nudge-close { color: var(--text-mute); font-size: 12px; padding: 2px 4px; border-radius: 4px; }
.fab-nudge-close:hover { color: var(--red); background: rgba(200,30,46,.08); }
@media (max-width: 640px) {
  .fab-nudge { right: calc(16px - (100vw - 100%)); bottom: 78px; font-size: 13px; padding: 10px 12px 10px 14px; }
}
@media (prefers-reduced-motion: reduce) { .fab-nudge { transition: opacity .01s linear; } }

/* ===== Scroll story ===== *
   A lightweight, self-hosted take on a "roomwalk" scroll experience: the
   photo stays pinned and slowly zooms while the facts beside it advance one
   at a time as the visitor scrolls past. No third-party service, no per-
   generation credits - just scroll position driving a CSS custom property
   (js/main.js) and an IntersectionObserver toggling which fact is "active". */
.story-grid { display: grid; grid-template-columns: 1fr 1.05fr; gap: var(--space-6); align-items: start; }
.story-visual {
  position: sticky; top: 108px; height: 62vh; min-height: 420px; border-radius: var(--r-lg);
  overflow: hidden; box-shadow: var(--shadow-lift);
}
.story-visual img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  transform: scale(1.04); will-change: transform;
}
/* Mobile now loads a separate, pre-cropped image (truck-1-mobile.jpg, via
   the <picture> source) framed for a portrait screen already - no CSS
   cropping/zooming needed on top of it, so object-fit switches to contain
   (show the whole image, no cover-crop) instead of the desktop cover+zoom.
   !important beats the inline style="object-fit:cover" on the <img>. */
@media (max-width: 700px) {
  .hero-photo { object-fit: contain !important; object-position: center; }
}

.story-visual::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(200deg, rgba(6,33,63,.5) 0%, rgba(6,33,63,0) 55%);
}
@media (max-width: 900px) {
  /* Mobile now loads a separate, pre-cropped photo (team-1-mobile.jpg, via
     the <picture> source) framed for this column already - contain shows
     it whole instead of cover cropping into it further. The 200deg dark
     gradient was tuned for the desktop crop; kept toned down here since a
     flatter, more evenly-lit photo still benefits from a lighter overlay. */
  .story-visual img { object-fit: contain; object-position: center; }
  .story-visual::after { background: linear-gradient(200deg, rgba(6,33,63,.22) 0%, rgba(6,33,63,0) 55%); }
}
/* Glassmorphism: frosted, translucent panel floating over the photo - the
   one place on the site that leans fully into the "glass" material. */
.story-badge {
  position: absolute; left: 22px; bottom: 22px; z-index: 2;
  background: rgba(255,255,255,.72); backdrop-filter: blur(14px) saturate(1.4); -webkit-backdrop-filter: blur(14px) saturate(1.4);
  border: 1px solid rgba(255,255,255,.5); border-radius: var(--r-md);
  padding: 14px 20px; box-shadow: 0 8px 30px rgba(6,33,63,.18);
}
.story-steps { display: flex; flex-direction: column; }
.story-step { min-height: 40vh; display: flex; flex-direction: column; justify-content: center; opacity: .3; transform: translateY(14px); transition: opacity .45s ease, transform .45s ease; }
.story-step.active { opacity: 1; transform: translateY(0); }
.story-step:last-child { min-height: 28vh; }
@media (max-width: 900px) {
  .story-grid { grid-template-columns: 1fr; }
  .story-visual { position: relative; top: 0; height: 46vh; min-height: 280px; }
  .story-step { min-height: auto; padding: var(--space-4) 0; opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .story-step { opacity: 1; transform: none; transition: none; }
  .story-visual img { transform: none !important; }
}

/* ===== Horizontal scroll-jack: How It Works ===== *
   A tall scroll runway (.hiw-scroll) with a pinned viewport (.hiw-sticky)
   inside it. js/main.js turns vertical scroll progress into a horizontal
   translateX on .hiw-track, so the four steps slide past like a filmstrip
   instead of sitting in a static grid - the "not just a classic landing
   page" moment requested. Falls back to a plain horizontally-scrollable
   row on touch devices and for reduced-motion, where hijacking the scroll
   axis is disorienting rather than impressive. */
.hiw-scroll { position: relative; height: 320vh; background: var(--grad-cream); }
.hiw-sticky {
  position: sticky; top: 0; height: min(88vh, 760px); overflow: hidden;
  display: flex; flex-direction: column; justify-content: center;
  background: var(--grad-cream);
}
.hiw-track { display: flex; gap: var(--space-5); margin-top: var(--space-6); padding: 0 clamp(20px, 6vw, 120px); will-change: transform; }
.hiw-step {
  flex: 0 0 min(78vw, 360px); background: var(--paper); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow-card); padding: var(--space-6); position: relative; overflow: hidden;
}
.hiw-step::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 5px; background: var(--grad-navy-bar); }
.hiw-track > .hiw-step:nth-child(2)::before { background: var(--grad-red-bar); }
.hiw-track > .hiw-step:nth-child(3)::before { background: var(--grad-gold-bar); }
.hiw-track > .hiw-step:nth-child(4)::before { background: var(--grad-navy-bar); }

@media (max-width: 760px), (hover: none) {
  .hiw-scroll { height: auto; }
  .hiw-sticky { position: relative; height: auto; padding: var(--space-7) 0; }
  .hiw-track { overflow-x: auto; scroll-snap-type: x mandatory; padding-bottom: var(--space-3); transform: none !important; }
  .hiw-step { scroll-snap-align: start; }
}
@media (prefers-reduced-motion: reduce) {
  .hiw-scroll { height: auto; }
  .hiw-sticky { position: relative; height: auto; }
  .hiw-track { overflow-x: auto; transform: none !important; }
}

/* ===== v5 — bold interactive layer ===== *
   The site-wide response to "too static, too much plain text": a cursor-
   reactive spotlight, magnetic buttons, per-card cursor highlight, a light-
   sweep on primary buttons, and a scroll-tied ambient hue drift. All driven
   from js/main.js (initCursorGlow / initMagneticButtons / initCardSpotlight
   / initScrollHue) and all inert without JS or under reduced-motion, so the
   page is still fully usable and fully visible with none of it running. */

/* A soft brand-colored glow that follows the cursor across the whole page,
   sitting above content on a fixed layer. Screen-blended so it reads as
   light, not a colored film over the page. */
.cursor-glow {
  position: fixed; inset: 0; z-index: 45; pointer-events: none;
  background: radial-gradient(190px circle at var(--mx,50%) var(--my,50%), rgba(224,188,107,.28), rgba(200,30,46,.1) 55%, transparent 75%);
  opacity: 0; transition: opacity .5s ease;
  mix-blend-mode: plus-lighter;
}
.cursor-glow.is-active { opacity: 1; }
@media (hover: none), (prefers-reduced-motion: reduce) { .cursor-glow { display: none; } }

/* Per-card cursor highlight - layered on top of the existing 3D tilt so
   hovering a card feels like it's being lit from where the cursor is,
   rather than just tilting in place. --mx/--my set per-card in main.js. */
.card { isolation: isolate; }
.card::after {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  border-radius: inherit; opacity: 0; transition: opacity .3s ease;
  background: radial-gradient(240px circle at var(--mx,50%) var(--my,50%), rgba(255,255,255,.55), transparent 60%);
  mix-blend-mode: overlay;
}
.card--paper::after { background: radial-gradient(240px circle at var(--mx,50%) var(--my,50%), rgba(184,137,47,.4), transparent 60%); mix-blend-mode: normal; }
.card--glass::after { background: radial-gradient(240px circle at var(--mx,50%) var(--my,50%), rgba(255,255,255,.7), transparent 60%); }
.card:hover::after { opacity: 1; }
.card > * { position: relative; z-index: 2; }
@media (hover: none) { .card::after { display: none; } }

/* Magnetic pull + light-sweep on primary buttons - transform is driven by
   main.js (initMagneticButtons), the sweep is pure CSS on hover. */
/* .fab-quote is deliberately excluded from the `position: relative` here -
   it already declares `position: fixed` in its own rule above, and this
   selector (same specificity, later in the file) was silently clobbering
   that back to relative, which dropped the button into normal document
   flow at the very bottom of the page instead of pinning it to the corner. */
.btn, .btn-outline { position: relative; overflow: hidden; will-change: transform; }
.fab-quote { overflow: hidden; will-change: transform; }
.btn::before, .btn-outline::before, .fab-quote::before {
  content: ''; position: absolute; top: 0; left: -65%; width: 45%; height: 100%;
  background: linear-gradient(115deg, transparent, rgba(255,255,255,.4), transparent);
  transform: skewX(-18deg); transition: left .65s cubic-bezier(.16,1,.3,1); pointer-events: none;
}
.btn:hover::before, .btn-outline:hover::before, .fab-quote:hover::before { left: 135%; }
@media (hover: none) { .btn, .btn-outline, .fab-quote { transform: none !important; } }

/* Scroll-tied ambient hue drift - main.js writes --scroll-p (0..1, total
   page progress) onto <html>; the decorative glow blobs rotate hue against
   it very slightly, so the page's ambient color keeps moving as you read
   instead of only the two idle drift loops it already had. */
.glow::before, .glow::after { filter: hue-rotate(calc(var(--scroll-p, 0) * 24deg)); }

/* Asymmetric USP layout - one featured tile spanning both rows instead of
   four identical boxes in a row. .card-grid/.reveal-group stay on the same
   element so the existing tilt/spotlight/stagger-fade JS and CSS still
   apply unchanged; this class only adds the grid-template itself. */
.usp-grid {
  display: grid; grid-template-columns: 1.15fr 1fr 1fr; grid-template-rows: auto auto;
  grid-template-areas: "feature b c" "feature d d";
}
.usp-grid > :nth-child(1) { grid-area: feature; display: flex; flex-direction: column; justify-content: center; }
.usp-grid > :nth-child(2) { grid-area: b; }
.usp-grid > :nth-child(3) { grid-area: c; }
.usp-grid > :nth-child(4) { grid-area: d; }
.usp-grid .idx { font-size: 76px; }
.usp-grid > :nth-child(1) .idx { font-size: 120px; }
.usp-grid > :nth-child(1) h3 { font-size: 26px; }
@media (max-width: 860px) {
  .usp-grid { grid-template-columns: 1fr; grid-template-areas: "feature" "b" "c" "d"; }
}

/* Cards read as physical paper on a dark desk when their section goes
   navy instead of pale cream - directly the "kraft paper under a lamp"
   read from the reference research, and a much stronger light/dark beat
   than another pale section. */
.section--desk { background: var(--grad-navy); }
.section--desk .eyebrow { color: var(--gold-light); }
.section--desk h2 { color: #fff; }

.section--ink { background: var(--grad-ink); }
.section--ink .eyebrow { color: var(--gold-light); }
.section--ink h2 { color: #fff; }

/* Pull-quote - one real review given the full-bleed, oversized-type
   treatment instead of another card, so the testimonial section doesn't
   read as "cards, again." The mark is a decorative background glyph, not
   content, hence aria-hidden. */
.pull-quote { position: relative; text-align: center; max-width: 900px; margin: 0 auto; }
.pull-quote .mark {
  position: absolute; top: -.5em; left: 50%; transform: translateX(-50%);
  font-family: var(--font-serif); font-size: 220px; line-height: 1; color: rgba(224,188,107,.16);
  pointer-events: none; z-index: 0; user-select: none;
}
.pull-quote blockquote {
  position: relative; z-index: 1; font-family: var(--font-serif); font-weight: 600; font-style: italic;
  font-size: clamp(28px, 4.2vw, 46px); line-height: 1.22; color: #fff;
}
.pull-quote cite { display: block; margin-top: var(--space-4); font-style: normal; font-family: var(--font-display); font-weight: 700; color: var(--gold-light); font-size: 15px; letter-spacing: .02em; }

/* ===== Box-opening intro (index.html only) ===== *
   The two flaps ARE the full screen - not a small centered box - so
   opening them directly reveals the real page underneath with no separate
   zoom/fade step needed. Branded: real logo on the left flap, wordmark on
   the right. Sequenced from main.js (initHeroCurtain): .opened triggers
   the hinge-open rotation on both flaps; the element is removed once that
   transition ends. Skipped outright under reduced-motion. */
/* top/left + explicit vw/vh instead of inset:0 - inset:0 sizes the box to
   fill whatever the fixed-position containing block resolves to, which
   drifted wider than the true viewport in some mobile emulation setups
   (see the .site-header comment above). */
.hero-curtain { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 500; perspective: 1600px; }
/* Belt-and-braces: the curtain is meant to be removed from the DOM once
   its opening transition ends (see initHeroCurtain in main.js), but
   transitionend is notoriously unreliable on mobile Safari - if it never
   fires, this fixed, full-viewport element was staying in the DOM
   invisibly and swallowing all scroll/touch input on the page. Once
   "opened" is added it can no longer intercept anything, independent of
   whether JS ever manages to remove the element. */
.hero-curtain.opened { pointer-events: none; }

/* Real photo now (images/box-closed.jpg) instead of a CSS-drawn surface -
   one continuous photo of the branded box, split across the two flaps via
   background-size/position so the seam lines up at the center regardless
   of viewport width. */
.box-flap {
  position: fixed; top: 0; width: 50vw; height: 100vh; z-index: 2;
  background-image: url('../images/box-closed.jpg?v=2');
  background-size: 200% 100%; background-repeat: no-repeat;
  transform-style: preserve-3d; backface-visibility: hidden;
  transition: transform 1.1s cubic-bezier(.6,-.2,.3,1.15);
  box-shadow: 0 0 60px rgba(0,0,0,.35);
}
.box-flap--left { left: 0; background-position: left center; transform-origin: left center; }
.box-flap--right { right: 0; background-position: right center; transform-origin: right center; }
.hero-curtain.opened .box-flap--left { transform: rotateY(-105deg); }
.hero-curtain.opened .box-flap--right { transform: rotateY(105deg); }

/* box-closed.jpg is shot/framed for a wide desktop canvas - stretched via
   200%/100% onto a narrow, tall phone viewport it badly distorts. Mobile
   points at a separate file instead, box-closed-mobile.jpg, meant to be a
   portrait (~9:16) crop/generation of the same box - swap that one file
   and this still needs no code changes. Until a real portrait version
   exists it's just a copy of the desktop art, so it'll look the same
   (still stretched) rather than break or 404. */
@media (max-width: 700px) {
  .box-flap { background-image: url('../images/box-closed-mobile.jpg?v=2'); }
}

@media (prefers-reduced-motion: reduce) { .hero-curtain { display: none; } }

/* Word-by-word headline reveal - main.js wraps each word of a .split-words
   element in the two spans below and staggers .word-inner's animation-delay
   inline. The outer .word clips its inner span so each word rises into
   place like a typeset line dropping in, rather than the whole line fading
   at once. */
.split-words .word { display: inline-block; overflow: hidden; vertical-align: top; padding-bottom: .08em; margin-bottom: -.08em; }
.split-words .word-inner { display: inline-block; transform: translateY(115%); animation: wordIn .85s cubic-bezier(.16,1,.3,1) forwards; }
@keyframes wordIn { to { transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) {
  .split-words .word-inner { animation: none; transform: none; }
}

/* ===== v4 sample — editorial paper direction ===== *
   Researched from styles.refero.design (Hyer Aviation: huge tight-tracked
   display type over a nearly-monochrome palette with one warm accent;
   Runway: cream/linen/espresso paper-ledger palette). Layered on top of v3
   as opt-in `--paper` modifier classes rather than replacing it everywhere,
   so only the agreed sample section (hero type scale + the "Why Chicago"
   cards + its stat-bar) switches over for approval before the rest of the
   site follows. The v3 gradient-wash/glass language is untouched elsewhere. */
.card--paper {
  --card-tint: transparent;
  background: var(--paper-warm) !important;
  border-color: var(--linen);
  border-radius: 2px;
  box-shadow: none !important;
}
/* One still accent instead of the v3 navy/red/gold cycle - the editorial
   reference sites spend their whole accent budget on a single hue. */
.card--paper::before { background: var(--gold) !important; height: 3px; }
.card--paper:hover { box-shadow: none !important; border-color: var(--gold); transform: translateY(-2px); }
/* Oversized ghost numeral instead of a small caption label - a background
   watermark the heading sits in front of, in the vein of an editorial
   monograph's numbered plates. Higher specificity than the generic
   `.card > *` spotlight rule on purpose, so it can sit z-index:0 (behind
   the real content) instead of being forced above it. */
.card--paper { padding-top: 58px; }
.card--paper > .idx {
  position: absolute; top: 2px; left: 22px; z-index: 0; pointer-events: none;
  font-family: var(--font-display); font-size: 96px; font-weight: 900; line-height: 1;
  letter-spacing: -.02em; color: transparent; -webkit-text-stroke: 1.5px rgba(184,137,47,.4);
}
.card--paper h3 { font-size: 21px; letter-spacing: -.01em; }

/* Corner frame-marks - a small viewfinder/blueprint-style detail (two
   opposite registration corners) rather than icons or illustration, applied
   to photos. Real elements injected by main.js's initFrameMarks(), not
   pseudo-elements, so it never collides with an element's own ::before/
   ::after (the hero section and .story-visual both already use one). */
/* Only forces a positioning context when the element doesn't already have
   one - .story-visual is already `position: sticky` for its pinned-photo
   scroll effect, and this rule (same specificity, later in the file) was
   silently clobbering that back to `relative`, breaking the whole pinned-
   photo trick. */
.frame-marks { position: relative; }
.story-visual.frame-marks { position: sticky; }
/* The mobile fallback further down (@media max-width:900px, .story-visual
   { position: relative }) has lower specificity than the rule right above
   and was silently losing to it at every screen size, including mobile -
   media queries don't add specificity, only source order + selector
   weight decide the winner. This repeats the same selector weight
   (.story-visual.frame-marks) inside the media query so mobile actually
   gets the un-pinned, stacked layout it was meant to. */
@media (max-width: 900px) {
  .story-visual.frame-marks { position: relative; top: 0; height: 46vh; min-height: 280px; }
}
.frame-mark { position: absolute; width: 26px; height: 26px; pointer-events: none; z-index: 5; }
.frame-mark--tl { top: 18px; left: 18px; border-top: 2px solid var(--gold); border-left: 2px solid var(--gold); }
.frame-mark--br { bottom: 18px; right: 18px; border-bottom: 2px solid var(--gold); border-right: 2px solid var(--gold); }
/* The hero's bottom-right corner mark was positioned for the taller
   desktop photo - once the mobile hero got shorter (shrunk to fit more of
   the crew in frame), that same 18px-from-bottom offset now lands on top
   of the stat-bar card overlapping it instead of on the photo, since the
   stat-bar overlaps the hero's bottom edge by design (negative margin).
   Simplest correct fix is to not show it there at all on mobile. */
@media (max-width: 900px) { .frame-mark { display: none; } }

/* Dashed "route line" behind the How It Works filmstrip - a lightweight
   nod to a logistics/shipping route rather than custom illustration, and it
   scrolls together with the cards since it lives inside .hiw-track. */
.hiw-track { position: relative; }
.hiw-route {
  position: absolute; left: 0; right: 0; top: 50%; height: 3px; z-index: 0;
  background-image: repeating-linear-gradient(90deg, var(--gold) 0 18px, transparent 18px 34px);
  opacity: .75;
}
/* Was an inline grid-template-columns:repeat(4,1fr) - inline styles can't
   be overridden by a media query, which left all four "How It Works"
   steps squeezed into slivers on a phone-width screen instead of
   stacking. */
.hiw-grid { grid-template-columns: repeat(4,1fr); }
@media (max-width: 760px) { .hiw-grid { grid-template-columns: repeat(2,1fr); } .hiw-route { display: none; } }
@media (max-width: 460px) { .hiw-grid { grid-template-columns: 1fr; } }
/* Same inline-grid-template-columns issue as .hiw-grid above, on the
   "Our Services" and testimonials 3-up grids. */
.grid-3 { grid-template-columns: repeat(3,1fr); }
@media (max-width: 760px) { .grid-3 { grid-template-columns: repeat(2,1fr); } }
@media (max-width: 480px) { .grid-3 { grid-template-columns: 1fr; } }

/* contact.html: info column + form side by side, and the Name/Phone and
   From/To field pairs inside the form - same inline-grid-columns issue,
   would otherwise squeeze the form into an unusable sliver on mobile. */
.contact-layout { grid-template-columns: 1fr 1.2fr; }
.form-row-2 { grid-template-columns: 1fr 1fr; }
@media (max-width: 760px) { .contact-layout { grid-template-columns: 1fr; } }
@media (max-width: 480px) { .form-row-2 { grid-template-columns: 1fr; } }

.stat-bar--paper {
  background: var(--paper-warm) !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
  border-color: var(--linen) !important; box-shadow: var(--shadow-card) !important;
}
.stat-bar--paper::before { background: var(--gold) !important; }
.stat-bar--paper > div { border-right-color: var(--linen); }

/* ===== Price calculator ===== *
   Real rates only (2 movers $127/hr, 3 movers $157/hr, $127 travel fee,
   free stairs up to 2 flights then a flat $75, $130 for heavy items) -
   js/main.js's initCalculator() does the math and re-renders the
   breakdown list on every input change. */
.calc-card {
  display: grid; grid-template-columns: minmax(0,1.3fr) minmax(0,1fr); gap: var(--space-6);
  max-width: 100%;
}
/* Grid/flex children refuse to shrink below their content's natural width
   by default (min-width:auto) - on a narrower real window than tested here,
   the crew-toggle buttons' text ("3 Movers - $157/hr") could force the
   whole card, and with it the page, wider than the viewport. minmax(0,...)
   above plus min-width:0 here overrides that default. */
.calc-controls, .calc-result { min-width: 0; }
.calc-toggle button { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.calc-field { margin-bottom: var(--space-4); }
.calc-field:last-child { margin-bottom: 0; }
.calc-label { display: flex; justify-content: space-between; font-family: var(--font-display); font-weight: 700; font-size: 19px; color: var(--navy-deep); margin-bottom: var(--space-2); }
.calc-label .calc-live { color: var(--red); font-weight: 800; }
.calc-hint { margin-top: 6px; font-size: 13.5px; color: var(--text-dim); line-height: 1.4; }

.calc-toggle { display: flex; gap: 12px; }
.calc-toggle button {
  flex: 1; padding: 17px 16px; border-radius: var(--r-sm); border: 1.5px solid var(--line);
  background: var(--paper); font-family: var(--font-display); font-weight: 700; font-size: 16px;
  color: var(--navy-deep); cursor: pointer; transition: background .18s ease, border-color .18s ease, color .18s ease;
}
.calc-toggle button.active { background: var(--grad-navy); color: #fff; border-color: var(--navy); }

.calc-field input[type="range"] {
  -webkit-appearance: none; appearance: none; width: 100%; height: 10px; border-radius: 5px;
  background: var(--line); outline: none; margin: 10px 0;
}
.calc-field input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 32px; height: 32px; border-radius: 50%;
  background: var(--grad-red); border: 4px solid #fff; box-shadow: 0 3px 12px rgba(200,30,46,.5); cursor: pointer;
}
.calc-field input[type="range"]::-moz-range-thumb {
  width: 32px; height: 32px; border-radius: 50%; background: var(--grad-red); border: 4px solid #fff;
  box-shadow: 0 3px 12px rgba(200,30,46,.5); cursor: pointer;
}

/* Checkbox options styled as bordered, clickable rows (same visual
   language as the crew-size toggle buttons) instead of a bare checkbox
   floating next to text - the checkbox, label, and price all sit in one
   tight, obviously-clickable row. */
.calc-toggle-row {
  display: flex; align-items: center; gap: 14px;
  padding: 15px 18px; border: 1.5px solid var(--line); border-radius: var(--r-sm);
  cursor: pointer; transition: border-color .15s ease, background .15s ease;
}
.calc-toggle-row:hover { border-color: var(--gold); }
.calc-toggle-row:has(input:checked) { border-color: var(--red); background: rgba(200,30,46,.05); }
.calc-toggle-row input { width: 24px; height: 24px; accent-color: var(--red); cursor: pointer; flex-shrink: 0; }
.calc-toggle-row-text {
  flex: 1; font-family: var(--font-display); font-weight: 700; font-size: 15.5px;
  letter-spacing: .01em; color: var(--navy-deep); line-height: 1.3;
}
.calc-toggle-row-price { font-family: var(--font-display); font-weight: 800; font-size: 15.5px; color: var(--red); white-space: nowrap; }

.calc-result { background: var(--grad-navy); border-radius: var(--r-md); padding: var(--space-5); color: #fff; display: flex; flex-direction: column; }
.calc-total-label { display: block; font-size: 13.5px; text-transform: uppercase; letter-spacing: .06em; color: rgba(255,255,255,.7); }
.calc-total-value { font-family: var(--font-serif); font-size: clamp(48px,5.5vw,68px); font-weight: 900; color: var(--gold-light); line-height: 1.15; }
.calc-breakdown { margin-top: var(--space-4); list-style: none; font-size: 16.5px; color: rgba(255,255,255,.9); border-top: 1px solid rgba(255,255,255,.15); padding-top: var(--space-3); }
.calc-breakdown li { display: flex; justify-content: space-between; padding: 7px 0; }
.calc-result .btn { margin-top: var(--space-4); width: 100%; justify-content: center; font-size: 17px; padding: 17px 30px; }
.calc-disclaimer { margin-top: var(--space-3); font-size: 12.5px; color: rgba(255,255,255,.6); line-height: 1.45; }

@media (max-width: 820px) {
  .calc-card { grid-template-columns: 1fr; }
}

/* Room picker — "what's in your move" step, shown before the price
   calculator. Base rooms (driven by the size <select>) are locked-in and
   always visible; the three optional cards carry their own checkbox and
   toggle independently, each with a fade+scale-in so checking a box feels
   like it's adding something to the move, not just flipping a switch. */
/* Single card shell wrapping both the room picker and the price
   calculator, so the two read as one connected flow instead of two
   stacked widgets. Only the shell carries the border/shadow/background -
   .calc-card (nested inside step 2) is stripped of its own outer chrome
   above so there's no card-inside-a-card doubling. */
/* The teaser sits inline on the page; clicking its button opens the full
   room-picker + calculator flow as a modal (below) instead of another
   scroll-past section - a deliberate, focused "important step" rather
   than something people can skim over. */
/* A dark, high-contrast panel rather than another white content card -
   next to a page full of white/cream cards, this one deliberately reads
   as "a tool, not an article" the instant it scrolls into view. */
.estimator-teaser {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-5); flex-wrap: wrap;
  background: var(--grad-navy); border-radius: var(--r-lg);
  box-shadow: var(--shadow-lift); padding: var(--space-6); margin-top: var(--space-5);
}
.estimator-teaser-main { display: flex; align-items: center; gap: var(--space-4); flex: 1; min-width: 260px; }
.estimator-teaser-icon {
  flex-shrink: 0; width: 60px; height: 60px; border-radius: var(--r-md);
  background: rgba(224,188,107,.16); display: flex; align-items: center; justify-content: center;
}
.estimator-teaser-icon svg { width: 32px; height: 32px; fill: none; stroke: var(--gold-light); stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
.estimator-teaser-cta { display: flex; flex-direction: column; align-items: center; gap: 9px; flex-shrink: 0; }
/* The one button on the page that pulses - reserved for this single,
   most-important click so the effect still means something the moment
   someone lands on it (used elsewhere too, it would stop registering). */
/* Continuous rapid pulse, no rest gap between beats - the paused-then-beat
   pattern read as too spaced out; this one keeps going back-to-back. */
.estimator-teaser-btn { padding: 21px 40px; font-size: 18px; animation: teaserPulse .9s ease-in-out infinite; }
@keyframes teaserPulse {
  0%, 100% { box-shadow: var(--shadow-btn), 0 0 0 0 rgba(200,30,46,.7); transform: scale(1); }
  50% { box-shadow: var(--shadow-btn), 0 0 0 14px rgba(200,30,46,0); transform: scale(1.07); }
}
.estimator-teaser-hint { font-size: 12.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: rgba(255,255,255,.6); }
@media (prefers-reduced-motion: reduce) { .estimator-teaser-btn { animation: none; } }

/* Modal shell: fixed overlay + backdrop + a scrollable panel. The close
   button is position:fixed (not part of the panel's own scroll), so it
   stays reachable in the same spot no matter how far the panel is
   scrolled - the "I'm in a separate window, I can leave any time" feel. */
.estimator-modal { position: fixed; inset: 0; z-index: 300; display: none; }
.estimator-modal.is-open { display: block; }
.estimator-modal-backdrop {
  position: absolute; inset: 0; background: rgba(6,20,40,.72); backdrop-filter: blur(3px);
  opacity: 0; transition: opacity .3s ease;
}
.estimator-modal.is-open .estimator-modal-backdrop { opacity: 1; }
.estimator-modal-panel {
  position: relative; margin: 5vh auto; width: min(1100px, 94vw); max-height: 90vh; overflow-y: auto;
  background: var(--paper); border-radius: var(--r-lg); box-shadow: 0 40px 100px rgba(0,0,0,.45);
  padding: var(--space-6);
  opacity: 0; transform: translateY(24px) scale(.98); transition: opacity .3s ease, transform .3s ease;
}
.estimator-modal.is-open .estimator-modal-panel { opacity: 1; transform: none; }
/* Sticky-inside-the-panel, not fixed-to-viewport: a viewport-relative
   calc() here had to guess which branch of the panel's own
   min(1100px,94vw) was active and drifted out of alignment with the
   panel's real edge at in-between widths. Sticky positioned inside the
   panel's own padding box tracks the panel's actual rendered edges
   exactly, at any width, while still stays in view as the panel scrolls
   (its nearest scrolling ancestor is the panel itself). */
.estimator-modal-close-row { display: flex; justify-content: flex-end; position: sticky; top: 0; z-index: 6; }
.estimator-modal-close {
  width: 40px; height: 40px; border-radius: 50%; border: none;
  background: rgba(6,20,40,.55); backdrop-filter: blur(6px); color: #fff; font-size: 17px;
  cursor: pointer; display: flex; align-items: center; justify-content: center;
  transition: background .18s ease, transform .18s ease;
}
.estimator-modal-close:hover { background: var(--red); transform: scale(1.06); }
body.modal-open { overflow: hidden; }
/* Sticky rail's top offset is calibrated to the page's fixed header
   elsewhere - inside the modal there's no header sitting above it, just
   the panel's own padding, so it re-pins much closer to the top. */
.estimator-modal .estimator-rail { top: 8px; }
@media (max-width: 640px) {
  .estimator-teaser { flex-direction: column; align-items: stretch; text-align: center; }
  .estimator-teaser-main { flex-direction: column; }
  .estimator-modal-panel { margin: 0; width: 100vw; max-height: 100vh; min-height: 100vh; border-radius: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .estimator-modal-backdrop, .estimator-modal-panel { transition: none; }
}

/* Sticky rail (left) + estimator-shell (right): the same proven sticky
   pattern already used for .story-visual elsewhere on this page - a
   narrow pinned column beside content tall enough to scroll past it,
   with IntersectionObserver toggling which rail step reads "active" and
   revealing the running total once the calculator scrolls into view.
   Deliberately NOT pinning the wide room-grid/calculator content itself
   (that needs its own full width for its internal columns) - only the
   slim tracker rail gets sticky treatment. */
.estimator-layout { display: grid; grid-template-columns: minmax(180px,220px) 1fr; gap: var(--space-5); align-items: start; margin-top: var(--space-5); }
.estimator-rail { position: sticky; top: 108px; display: flex; flex-direction: column; gap: var(--space-4); }
.estimator-rail-step { display: flex; align-items: center; gap: 10px; opacity: .4; transition: opacity .3s ease; }
.estimator-rail-step.active { opacity: 1; }
.estimator-rail-num {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 24px; height: 24px; border-radius: 50%; background: var(--line); color: var(--navy-deep);
  font-family: var(--font-display); font-weight: 800; font-size: 12px; transition: background .3s ease, color .3s ease;
}
.estimator-rail-step.active .estimator-rail-num { background: var(--grad-red); color: #fff; }
.estimator-rail-label { font-family: var(--font-display); font-weight: 700; font-size: 13.5px; color: var(--navy-deep); line-height: 1.25; }
.estimator-rail-total {
  border-top: 1px solid var(--line); padding-top: var(--space-3);
  opacity: 0; transform: translateY(6px); transition: opacity .4s ease, transform .4s ease;
}
.estimator-rail-total.is-visible { opacity: 1; transform: none; }
.estimator-rail-total-label { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-mute); }
.estimator-rail-total-value { font-family: var(--font-serif); font-weight: 900; font-size: 30px; color: var(--red); }
.estimator-rail-total-caveat { display: block; margin-top: 4px; font-size: 11px; line-height: 1.35; color: var(--text-mute); }
@media (max-width: 900px) {
  .estimator-layout { grid-template-columns: 1fr; }
  .estimator-rail { position: static; flex-direction: row; justify-content: space-between; align-items: center; }
  .estimator-rail-total { border-top: none; border-left: 1px solid var(--line); padding-top: 0; padding-left: var(--space-3); }
  .estimator-rail-total.is-visible { transform: none; }
}

.estimator-shell {
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow-lift); padding: var(--space-6);
}
.estimator-step-head { display: flex; align-items: center; gap: 12px; margin-bottom: var(--space-3); }
.estimator-step-num {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 26px; height: 26px; border-radius: 50%; background: var(--grad-red); color: #fff;
  font-family: var(--font-display); font-weight: 800; font-size: 13px;
}
.estimator-step-title { font-family: var(--font-display); font-weight: 800; font-size: 14.5px; text-transform: uppercase; letter-spacing: .04em; color: var(--navy-deep); }
.estimator-connector {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  margin: var(--space-4) 0; color: var(--gold); text-align: center;
}
.estimator-connector span { font-family: var(--font-display); font-weight: 700; font-size: 12.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-mute); }
/* Progressive reveal — room cards and the whole calculator stay collapsed
   until a home size is picked, then open in sequence (JS sets an explicit
   px max-height to animate, then swaps to 'none' once open so later
   content changes, like optional rooms toggling for a different size,
   can't get clipped or leave dead space below them). */
.reveal-collapse { max-height: 0; opacity: 0; overflow: hidden; transition: max-height .6s cubic-bezier(.16,1,.3,1), opacity .5s ease .12s; }
.reveal-collapse.is-open { opacity: 1; }
.room-size-select {
  display: block; width: 100%; max-width: 320px; padding: 14px 18px; margin-bottom: var(--space-4);
  border: 1.5px solid var(--line); border-radius: var(--r-sm); background: var(--paper);
  font-family: var(--font-display); font-weight: 700; font-size: 16px; color: var(--navy-deep);
  cursor: pointer; appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--navy-deep) 50%), linear-gradient(135deg, var(--navy-deep) 50%, transparent 50%);
  background-position: calc(100% - 22px) center, calc(100% - 16px) center; background-size: 6px 6px; background-repeat: no-repeat;
}
.room-size-select:hover { border-color: var(--gold); }
/* Draws the eye to the one control that actually needs a click before
   anything else on the page opens up - stops the instant a real size is
   picked (js/main.js removes the class in that same first-pick branch
   that triggers the reveal cascade), so it never nags after it's done its job. */
.room-size-select.attention-pulse { animation: selectPulse 1.8s ease-in-out infinite; }
@keyframes selectPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(200,30,46,.35); border-color: var(--line); }
  50% { box-shadow: 0 0 0 8px rgba(200,30,46,0); border-color: var(--red); }
}
@media (prefers-reduced-motion: reduce) { .room-size-select.attention-pulse { animation: none; border-color: var(--red); } }

.room-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(128px,1fr)); gap: var(--space-3); margin-top: var(--space-4); }
.room-card {
  position: relative; display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: var(--space-3) var(--space-2); border: 1.5px solid var(--line); border-radius: var(--r-md);
  background: var(--paper); text-align: center; opacity: .32; transform: scale(.94);
  transition: opacity .32s cubic-bezier(.16,1,.3,1), transform .32s cubic-bezier(.16,1,.3,1), border-color .2s ease, background .2s ease;
}
.room-card.is-active { opacity: 1; transform: scale(1); border-color: var(--gold); background: rgba(184,137,47,.06); }
.room-card--optional { cursor: pointer; }
.room-card--optional:hover { border-color: var(--gold); }
.room-card-check {
  position: absolute; top: 10px; right: 10px; width: 20px; height: 20px;
  accent-color: var(--red); cursor: pointer;
}
.room-card-icon { width: 40px; height: 40px; fill: none; stroke: var(--navy-deep); stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; transition: stroke .2s ease; }
.room-card.is-active .room-card-icon { stroke: var(--red); }
.room-card-label { font-family: var(--font-display); font-weight: 700; font-size: 13px; color: var(--navy-deep); letter-spacing: -.01em; }

@media (max-width: 640px) {
  .room-grid { grid-template-columns: repeat(auto-fill, minmax(100px,1fr)); }
}
