:root {
  --ink: #201c18;
  --white: #f4f9fd;
  --gray-light: #e9f1f8;
  --gray-mid: #d5e3f0;
  --gray-dark: #5f6a75;
  --primary: #1c5d8c;
  --primary-dark: #123f5e;
  --primary-rgb: 28, 93, 140;
  --accent: #d97e2e;
  --accent-dark: #b8641d;
  --font-head: "Lora", Georgia, serif;
  --font-body: "Plus Jakarta Sans", -apple-system, sans-serif;
  --max: 6000px;
  --radius-sm: 8px;
  --radius-md: 14px;
  --radius-lg: 22px;
  --radius-pill: 999px;
  --shadow-sm: 0 2px 10px rgba(16, 32, 48, 0.06);
  --shadow-md: 0 12px 32px rgba(16, 32, 48, 0.10);
  --shadow-lg: 0 24px 60px rgba(16, 32, 48, 0.14);
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --dur-fast: 180ms;
  --dur-mid: 420ms;
  --dur-slow: 620ms;
  --header-h: 100px;
  /* Fixed copies of --white/--gray-light, immune to body.has-bg-slideshow's
     translucent retint (see below) -- for surfaces that should stay
     fully opaque even though the section behind them isn't: the stacking
     cards, and the Long-Term Tenant Headaches box. */
  --card-opaque-bg: #f4f9fd;
  --gray-light-opaque: #e9f1f8;
}

:root[data-theme="dark"] {
  --ink: #f5f1ea;
  --white: #161f2c;
  --gray-light: #1c2836;
  --gray-mid: #26364a;
  --gray-dark: #9fb0c4;
  --card-opaque-bg: #161f2c;
  --gray-light-opaque: #1c2836;
  --primary: #6bafe8;
  --primary-dark: #8cc4ef;
  --primary-rgb: 107, 175, 232;
  --accent: #f5b36e;
  --accent-dark: #f8c88f;
  --shadow-sm: 0 2px 12px rgba(0, 0, 0, 0.35);
  --shadow-md: 0 12px 32px rgba(0, 0, 0, 0.45);
  --shadow-lg: 0 24px 60px rgba(0, 0, 0, 0.55);
}

/* Homepage's locked background slideshow (see .bg-slideshow-layer) shows
   through from the photo-cards section (home-d3) onward -- the hero and the
   intro section right after it (home-d2) keep their normal opaque
   backgrounds unchanged, per an explicit ask to leave those two alone.
   Named sections rather than a `~` general-sibling sweep so late-injected
   chrome (chat widget, mobile action bar) never accidentally picks this up.
   home-d4 (Contact Us) dropped from this list per a later explicit ask:
   with the slideshow showing through, the Long-Term Tenant Headaches card's
   solid, slideshow-immune background (--card-opaque-bg -- see below) sat
   right above a translucent stretch that let the slide underneath show
   through unevenly, reading as a stray seam right under the card. Keeping
   home-d4 fully opaque (same var values as home-d2 above) makes that
   whole run from the card down through Contact Us one flat, uninterrupted
   background instead of card -> slideshow sliver -> Contact Us. */
body.has-bg-slideshow .home-d3,
body.has-bg-slideshow .home-testimonials-wave,
body.has-bg-slideshow footer.site {
  --white: rgba(244, 249, 253, 0.62);
  --gray-light: rgba(233, 241, 248, 0.58);
  --gray-mid: rgba(213, 227, 240, 0.58);
}
:root[data-theme="dark"] body.has-bg-slideshow .home-d3,
:root[data-theme="dark"] body.has-bg-slideshow .home-testimonials-wave,
:root[data-theme="dark"] body.has-bg-slideshow footer.site {
  --white: rgba(22, 31, 44, 0.62);
  --gray-light: rgba(28, 40, 54, 0.58);
  --gray-mid: rgba(38, 54, 74, 0.58);
}

* { box-sizing: border-box; }

/* Global keyboard-focus ring. :focus-visible (not :focus) so mouse/touch
   clicks stay clean and only actual keyboard navigation shows the ring. */
:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
form.contact-form input:focus-visible,
form.contact-form textarea:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}

/* Skip link: visually hidden until keyboard-focused, then jumps straight
   past the header to #main-content (see each page's first content section). */
.skip-link {
  position: absolute;
  top: -100px;
  left: 12px;
  z-index: 10002;
  background: var(--primary);
  color: #ffffff;
  padding: 12px 20px;
  border-radius: var(--radius-sm);
  font-family: var(--font-body);
  font-weight: 600;
  text-decoration: none;
  transition: top 0.15s ease;
}
.skip-link:focus {
  top: 12px;
}
html { scroll-behavior: smooth; }
body { transition: background-color 0.2s ease, color 0.2s ease; }

/* Scroll-reveal (see motion.js). Content is fully visible with no JS --
   this only hides once motion.js confirms it will animate it back in. */
[data-reveal] {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity var(--dur-slow) var(--ease-out), transform var(--dur-slow) var(--ease-out);
  will-change: opacity, transform;
}
[data-reveal].is-revealed { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  [data-reveal] { opacity: 1; transform: none; transition: none; }
}

/* Scroll progress bar, injected by motion.js */
#scroll-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 2px;
  width: 0%;
  background: linear-gradient(90deg, var(--primary), var(--primary-dark));
  z-index: 9999;
  pointer-events: none;
  transition: width 0.1s linear;
}

/* Kinetic hero headline (see motion.js initKineticHero). Words are hidden
   only after JS has actually split them into spans -- .kinetic-ready gates
   this, so a no-JS visitor or a not-yet-run page always sees plain text. */
.hero-title.kinetic-ready .hero-word {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.4em);
  transition: opacity var(--dur-mid) var(--ease-out), transform var(--dur-mid) var(--ease-out);
}
.hero-title.kinetic-ready.kinetic-in .hero-word { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .hero-title.kinetic-ready .hero-word { opacity: 1; transform: none; transition: none; }
}

body {
  margin: 0;
  font-family: var(--font-body);
  color: var(--ink);
  background: var(--white);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }
a { color: inherit; }
p { font-size: 1.08rem; margin: 0 0 1em; }

.wrap {
  max-width: var(--max);
  margin: 0 auto;
  padding: 0 max(32px, 4vw);
}

/* Header */
header.site {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--white);
  box-shadow: 0 1px 3px rgba(0,0,0,0.08);
  transition: background-color 0.2s ease, box-shadow 0.2s ease;
}
:root[data-theme="dark"] header.site {
  box-shadow: none;
  border-bottom: 1px solid var(--gray-mid);
}
/* The mobile full-screen nav lives inside header.site, which establishes
   its own stacking context (position:sticky + z-index above). A
   descendant's z-index can never escape that context, so the overlay is
   only guaranteed to paint above everything else (including the chat
   widget, appended straight to body) by raising header.site itself while
   the menu is open, rather than raising .nav-links in isolation. */
header.site.menu-open { z-index: 10000; }
/* Solid (not backdrop-filter) on purpose: this header sits over the
   homepage's full-bleed background slideshow for the entire scroll (see
   .bg-slideshow-layer). Kept solid even after the old always-decoding video
   background (which made backdrop-filter's continuous blur pass heavy
   enough to visibly lag input system-wide) was replaced with static
   slides -- a near-opaque solid still reads the same at a glance and costs
   nothing per frame, so there's no reason to pay for the blur. */
header.site.is-scrolled {
  background: color-mix(in srgb, var(--white) 94%, transparent);
  box-shadow: var(--shadow-sm);
}
header.site .wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: clamp(9px, calc(2.7vw - 5px), 85px);
  padding-bottom: clamp(9px, calc(2.7vw - 5px), 85px);
}
.logo img { height: clamp(100px, 60px + 5vw, 200px); width: auto; }
nav.main { display: flex; align-items: center; }
.nav-links { display: flex; align-items: center; gap: 2vw; }
/* .nav-links' own gap only spaces its two direct children (this row of
   links vs. the call/email actions) -- it does nothing for spacing
   between the individual links, which live one level down. Without a
   gap here the <a> tags fall back to bare HTML whitespace between them,
   which reads as cramped and inconsistent. The mobile media query below
   overrides this with its own column layout and gap. */
.nav-links-primary { display: flex; align-items: center; gap: clamp(14px, 2vw, 32px); }
/* Call/Email only make sense in the mobile full-screen menu (see the
   max-width: 900px override below) -- the desktop header is a plain
   horizontal link row with no room, and no need, for a phone/email
   entry alongside the page links. */
.nav-links a.nav-link-action { display: none; }
nav.main a {
  position: relative;
  text-decoration: none;
  font-family: var(--font-body);
  font-size: 1.08rem;
  color: var(--ink);
  transition: color 0.15s ease;
  padding-bottom: 4px;
}
nav.main a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: 0%;
  background: var(--primary);
  transition: width var(--dur-fast) var(--ease-out);
}
nav.main a.active,
nav.main a:hover {
  color: var(--primary);
}
nav.main a.active::after,
nav.main a:hover::after {
  width: 100%;
}
@media (prefers-reduced-motion: reduce) {
  nav.main a::after { transition: none; }
}

/* Mobile hamburger toggle */
.nav-toggle {
  display: none;
  position: relative;
  z-index: 10001;
  width: 38px;
  height: 38px;
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;
  flex-shrink: 0;
}
.nav-toggle span {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 24px;
  height: 2px;
  border-radius: 1px;
  background: var(--ink);
  transform: translate(-50%, -50%) translateY(-6px);
  transition: transform 0.22s cubic-bezier(0.65, 0, 0.35, 1), width 0.22s cubic-bezier(0.65, 0, 0.35, 1);
}
.nav-toggle span:nth-child(2) { transform: translate(-50%, -50%) translateY(6px); }
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translate(-50%, -50%) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { transform: translate(-50%, -50%) rotate(-45deg); }
.nav-toggle:hover span { width: 26px; }

/* Dark mode toggle */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 0;
  margin-left: clamp(4px, 1vw, 12px);
  border: none;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  flex-shrink: 0;
  transition: opacity 0.15s ease;
}
.theme-toggle:hover { opacity: 0.65; }
.theme-toggle svg { width: 18px; height: 18px; display: block; }
.theme-toggle .icon-moon { display: none; }
:root[data-theme="dark"] .theme-toggle .icon-sun { display: none; }
:root[data-theme="dark"] .theme-toggle .icon-moon { display: block; }

/* Section dividers: exact clip-path curves copied from the live site.
   Each wave clips away part of a section's own bottom edge. Rather than
   clipping the section itself (which would reveal the plain page
   background in the cutout -- fine when that happens to match, a
   visible seam when it doesn't, e.g. dark mode), the clip lives on a
   ::before layer colored with the section's OWN background; the
   section's real background is set to whatever comes right after it,
   so that's what shows through the cutout. Shape and color are both
   correct at any viewport, nothing measured or guessed in pixels. */
.divider {
  position: relative;
  z-index: 0;
  padding-top: 0;
}
.divider::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
}
.divider > * {
  position: relative;
  z-index: 1;
}

.hero-photo { background: var(--gray-light); }
/* Homepage only: the hero's own background would otherwise fully cover
   the fixed slideshow sitting behind it (see .bg-slideshow-layer). Safe to
   drop entirely rather than retint -- home-d2's own ::before wave patch
   that seams into this section paints its own opaque color regardless of
   what's behind the hero, so this doesn't affect that seam. */
body.has-bg-slideshow .hero-photo { background: transparent; }

/* .divider.ID (two classes) so these reliably beat section.gray /
   section.white / section.gray-mid (one class + one type) below,
   regardless of source order. */
.home-d2::before { clip-path: url(#home-d2); background: var(--gray-light); }
.divider.home-d2 { background: var(--white); }
/* home-d2's own background above is only ever visible through the thin
   wave cutout at its very bottom edge (see the .divider comment above) --
   it exists purely to preview home-d3's color for a seamless transition.
   But home-d3's real rendered color there is the translucent slideshow-
   tinted wash from body.has-bg-slideshow below, not solid var(--white);
   previewing the solid color instead shows up as a flat, noticeably darker
   strip wedged between the wave curve and the actual (lighter) slide, most
   visible in dark mode where the solid preview is a flat navy slab next
   to a slide that isn't nearly that dark. Matching the same translucent
   values here removes the seam. */
body.has-bg-slideshow .divider.home-d2 { background: rgba(244, 249, 253, 0.62); }
:root[data-theme="dark"] body.has-bg-slideshow .divider.home-d2 { background: rgba(22, 31, 44, 0.62); }
.home-d3::before { clip-path: url(#home-d3); background: var(--white); }
.divider.home-d3 { background: var(--white); }
.home-testimonials-wave::before { clip-path: url(#home-d1-inv); background: var(--white); }
.divider.home-testimonials-wave { background: var(--gray-mid); }
/* Testimonials section is currently hidden (see index.html) -- home-d3 now
   sits directly above home-d4, so its wave reveals home-d3's white instead
   of the testimonials section's gray-mid. Revert both rules below together
   with the HTML comment when testimonials come back. */
.home-d3 + section.divider .contact-grid {
  padding-top: clamp(48px, 8vw, 96px);
}
.home-d4::before { clip-path: url(#home-d4); background: var(--white); }
.divider.home-d4 { background: var(--gray-light); }
/* Same fix as home-d2 above, one section later: home-d4's own background is
   only visible in the wave cutout at its bottom, previewing footer.site's
   color -- but the footer is still in the slideshow overlay (unlike
   home-d4, which was pulled out of it earlier), so its real rendered color
   is a translucent, slideshow-tinted gray-light, not the solid one above.
   Without this, that mismatch shows up as a flat, off-color slab between
   the wave curve and the footer's actual (slideshow-showing) background. */
body.has-bg-slideshow .divider.home-d4 { background: rgba(233, 241, 248, 0.58); }
:root[data-theme="dark"] body.has-bg-slideshow .divider.home-d4 { background: rgba(28, 40, 54, 0.58); }

.wwd-d2::before { clip-path: url(#wwd-d2); background: var(--gray-light); }
.divider.wwd-d2 { background: var(--white); }
.wwd-d3::before { clip-path: url(#wwd-d3); background: var(--white); }
.divider.wwd-d3 { background: var(--gray-light); }
.wwd-d4::before { clip-path: url(#wwd-d4); background: var(--gray-light); }
.divider.wwd-d4 { background: var(--white); }

.about-d1::before { clip-path: url(#about-d1); background: var(--gray-light); }
.divider.about-d1 { background: var(--white); }
.about-d2::before { clip-path: url(#about-d2); background: var(--white); }
.divider.about-d2 { background: var(--gray-mid); }
.about-d3::before { clip-path: url(#about-d3); background: var(--gray-mid); }
.divider.about-d3 { background: var(--gray-light); }

.pf-d1::before { clip-path: url(#pf-d1); background: var(--white); }
.divider.pf-d1 { background: var(--gray-light); }

.faqs-d1::before { clip-path: url(#faqs-d1); background: var(--gray-light); }
.divider.faqs-d1 { background: var(--white); }
.faqs-d2::before { clip-path: url(#faqs-d2); background: var(--white); }
.divider.faqs-d2 { background: var(--gray-light); }

.contact-d1::before { clip-path: url(#contact-d1); background: var(--white); }
.divider.contact-d1 { background: var(--gray-light); }

/* Homepage's locked background slideshow. Lives as the very first element
   in body, position:fixed with no z-index of its own -- being first in DOM
   order is enough for it to paint behind every later element by default,
   without needing a negative z-index (which can fight the body/html
   background in some engines). The hero's scrim (still wave-clipped,
   unchanged) just dims this same fixed layer showing through, so the hero
   looks identical while the rest of the page (translucent, see
   body.has-bg-slideshow above) reads the current slide through every
   section behind it.
   Replaced the old always-decoding <video> here with a pair of crossfaded
   <img> elements cycling through the portfolio photos (see
   initBgSlideshow in motion.js): a looping video keeps the GPU decoding
   and compositing a fresh frame continuously for as long as the tab is
   open, which is real, unavoidable cost even when nothing on screen is
   changing quickly. Static slides only cost anything during their own
   ~1.8s crossfade / slow zoom, then sit idle as a plain composited layer
   the rest of the ~7s they're shown -- lighter on memory and battery for
   the same full-bleed effect. */
.bg-slideshow-layer {
  position: fixed;
  inset: 0;
  overflow: hidden;
  background: var(--gray-mid);
}
.bg-slideshow-layer .bg-slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 1.8s ease;
}
.bg-slideshow-layer .bg-slide.is-active { opacity: 1; }
/* Four Ken Burns variants, picked at random per slide by initBgSlideshow in
   motion.js. pan-ltr/pan-rtl hold a constant 1.1x overscan and only
   translate, so the pan never reveals an edge outside the source photo;
   pan-in/pan-out only scale, from/to the same 1x baseline object-fit:cover
   already renders at. animation-fill-mode: forwards holds each variant's
   end position rather than snapping back once the animation completes
   (a slide can sit fully visible for a moment before the next swap starts
   its fade-out). Duration kept equal to SLIDE_MS in motion.js so the pan
   finishes right as the next slide begins covering it. */
.bg-slideshow-layer .bg-slide.pan-ltr { animation: bg-pan-ltr 7s linear forwards; }
.bg-slideshow-layer .bg-slide.pan-rtl { animation: bg-pan-rtl 7s linear forwards; }
.bg-slideshow-layer .bg-slide.pan-in { animation: bg-pan-in 7s linear forwards; }
.bg-slideshow-layer .bg-slide.pan-out { animation: bg-pan-out 7s linear forwards; }
@keyframes bg-pan-ltr {
  from { transform: scale(1.1) translateX(-2.5%); }
  to { transform: scale(1.1) translateX(2.5%); }
}
@keyframes bg-pan-rtl {
  from { transform: scale(1.1) translateX(2.5%); }
  to { transform: scale(1.1) translateX(-2.5%); }
}
@keyframes bg-pan-in {
  from { transform: scale(1); }
  to { transform: scale(1.12); }
}
@keyframes bg-pan-out {
  from { transform: scale(1.12); }
  to { transform: scale(1); }
}
/* Ken Burns panning is pure motion with no functional purpose, so it's the
   first thing to drop; the crossfade stays since losing it would make
   slide changes hard cuts rather than a subtler thing to turn off. */
@media (prefers-reduced-motion: reduce) {
  .bg-slideshow-layer .bg-slide.pan-ltr,
  .bg-slideshow-layer .bg-slide.pan-rtl,
  .bg-slideshow-layer .bg-slide.pan-in,
  .bg-slideshow-layer .bg-slide.pan-out {
    animation: none;
    transform: none;
  }
}

/* Hero (full-bleed photo/video) */
.hero-photo {
  position: relative;
  height: calc(27.6vw + 39px);
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: center;
  justify-content: center;
}
.hero-photo.short { height: calc(20.3vw - 4px); }
/* The wave clips the video, not the section -- so the section's own
   background (set above, matching the section right after it) is what
   shows through the cutout instead of being clipped away right along
   with the video. */
.hero-photo .hero-clip {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
.wwd-d1 .hero-clip { clip-path: url(#wwd-d1); }
/* Homepage hero's wave divider. The slideshow is a single fixed layer
   behind the whole page (see .bg-slideshow-layer), not nested media local
   to this section, so .hero-clip can't clip a slide itself the way
   .wwd-d1's local video is clipped above. Instead this paints the color
   home-d2 actually appears as into just the wave-shaped sliver at the
   bottom, using the home-d1 curve inverted (kept region flipped from top to
   bottom) -- everywhere above that sliver stays transparent so the fixed
   slideshow keeps showing through untouched. gray-light, not home-d2's own
   white background: home-d2's ::before wave (see .home-d2::before below)
   paints gray-light over almost all of home-d2's box, including where
   its heading sits, so gray-light is what this edge needs to match. */
.home-d1 .hero-clip {
  clip-path: url(#home-d1-bottom);
  background: var(--gray-light);
}
.hero-photo .overlay {
  position: absolute;
  inset: 0;
  background: rgba(0,0,0,0.35);
}
.hero-photo video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}
.hero-photo h1 {
  position: relative;
  color: #ffffff; /* always over a photo + dark overlay, independent of page theme */
  font-family: var(--font-head);
  font-weight: 400;
  font-size: clamp(2.1rem, 4.6vw, 3.6rem);
  text-align: center;
  margin: 0;
  padding: 0 20px;
}
.hero-photo h1 strong { font-weight: 700; }

/* Homepage + What We Do hero content overlay. .hero-content sits as a
   sibling of .hero-clip (never inside it), so the clip-path never cuts
   the overlaid text off. Same-specificity rules placed after the base
   .hero-photo block above, so they win the cascade without needing !important. */
.hero-home { height: auto; min-height: 68vh; }
.hero-wwd { height: auto; min-height: 58vh; }
@media (max-width: 900px) {
  .hero-home { min-height: 78vh; min-height: 78svh; }
  .hero-wwd { min-height: 52vh; min-height: 52svh; }
  .hero-home .hero-sub, .hero-home .hero-pills { display: none; }
}

.hero-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(8,16,26,0.55) 0%, rgba(8,16,26,0.30) 45%, rgba(8,16,26,0.70) 100%);
}

.hero-content {
  position: relative;
  z-index: 1;
  text-align: center;
  padding: 40px 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
}
/* Blurred per explicit request, despite backdrop-filter forcing a fresh
   blur pass every video frame (see the note on header.site.is-scrolled
   above for the general cost) -- background tint kept light since the
   blur itself now carries most of the contrast against the video.
   Scoped to just the text (not .hero-content as a whole, and not sized
   to the wrap's width) so the panel stays small and the video stays
   visible around it, with the buttons and pills sitting below it. */
.hero-home .hero-text-panel {
  background: rgba(8, 16, 26, 0.22);
  border-radius: 20px;
  padding: 28px 36px;
  backdrop-filter: blur(0.7px);
  -webkit-backdrop-filter: blur(0.7px);
}
.hero-eyebrow {
  color: #ffffff;
  opacity: 0.92;
  font-family: var(--font-body);
  font-weight: 700;
  letter-spacing: 0.12em;
  font-size: 0.8rem;
  margin: 0 0 16px;
}
.hero-title {
  position: relative;
  color: #ffffff;
  font-family: var(--font-head);
  font-weight: 400;
  font-size: clamp(2.1rem, 4.6vw, 3.6rem);
  text-align: center;
  margin: 0 0 18px;
}
.hero-title strong { font-weight: 700; }
.hero-sub {
  color: #ffffff;
  opacity: 0.92;
  font-size: 1.15rem;
  max-width: 640px;
  margin: 0 0 8px;
}
.hero-home .hero-text-panel .hero-sub { margin-bottom: 0; }
.hero-actions { display: flex; gap: 16px; flex-wrap: wrap; justify-content: center; }
.hero-home .hero-actions { margin-top: 24px; }
/* Sized down from the base .btn (see far below) since these sit as a
   secondary row underneath the hero text panel rather than as the page's
   main call to action. */
.hero-home .hero-actions .btn {
  padding: 10px 22px;
  font-size: 0.92rem;
}
/* Below ~480px the two buttons at the size above are too wide to fit
   side by side in the wrap's padded width and wrap onto their own lines
   instead -- shrink them further so they still sit next to each other. */
@media (max-width: 480px) {
  .hero-home .hero-actions { gap: 10px; }
  .hero-home .hero-actions .btn { padding: 9px 14px; font-size: 0.8rem; }
}
/* Hardcoded light background rather than var(--white): this sits on the
   hero photo/video, not the page background, so it needs to read as
   "light" in both themes rather than flipping to dark-navy along with
   the rest of the UI (same reasoning as .hero-photo h1's fixed #ffffff
   above). Text stays on var(--primary) so the blue itself still matches
   whichever theme is active. */
.btn.btn-ghost {
  background: #ffffff;
  border: 2px solid #ffffff;
  color: var(--primary);
}
.btn.btn-ghost:hover {
  background: #e9f1f8;
  border-color: #e9f1f8;
  color: var(--primary-dark);
  box-shadow: none;
}
.hero-pills {
  list-style: none;
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  justify-content: center;
  margin: 28px 0 0;
  padding: 0;
}
/* Solid, not blurred, and for the same reason as .hero-home .hero-content
   above -- there are 3 of these over the video, so 3 separate blur passes
   every frame instead of one. Opacity bumped up from the old 0.14 since it
   no longer has blur's contrast boost to lean on. */
.hero-pills li {
  color: #ffffff;
  font-size: 0.85rem;
  font-weight: 600;
  background: rgba(20, 30, 42, 0.45);
  border: 1px solid rgba(255,255,255,0.3);
  border-radius: var(--radius-pill);
  padding: 7px 16px;
}
/* Compact page-header band (Portfolio, About, FAQs, Contact -- no hero video) */
.page-header-eyebrow {
  text-align: center;
  color: var(--primary);
  font-family: var(--font-body);
  font-weight: 700;
  letter-spacing: 0.12em;
  font-size: 0.8rem;
  margin: 56px 0 8px;
}
.page-header-eyebrow + .page-title { padding-top: 12px; }
.page-header-eyebrow.left { text-align: left; }

/* Sections */
section { padding: clamp(40px, 4vw, 58px) 0 clamp(48px, 6vw, 86px); }
section.pb-lg { padding-bottom: clamp(56px, 8vw, 115px); }
section.pb-sm { padding-bottom: clamp(40px, 5.4vw, 78px); }
section.pb-xs { padding-bottom: clamp(24px, 2.4vw, 35px); }
section.gray { background: var(--gray-light); }
section.gray-mid { background: var(--gray-mid); }
section.white { background: var(--white); }

h1, h2, h3 { font-family: var(--font-head); font-weight: 500; margin: 0 0 0.5em; }

.page-title {
  text-align: center;
  font-size: clamp(2.1rem, 4.6vw, 3.6rem);
  padding: 56px 0 8px;
}
/* Homepage only: home-d2's wave curve above this heading (see the
   home-d2 clip-path in index.html) tapers off well before the section's
   own edge, so the translucent, video-showing background that's the
   whole point of home-d3 doesn't really start reading as "there" until
   past this heading's default 56px top padding -- per an explicit ask to
   carry that look up closer to the wave instead of leaving a flat gap
   before it. Scoped to home-d3 so wwd-d3/about-d2's own page-title
   spacing, not part of this ask, stays on the shared rule above. */
.home-d3 .page-title {
  padding-top: 16px;
}
.page-title.left { text-align: left; }
.page-title.md { font-size: clamp(1.7rem, 3.4vw, 2.5rem); padding-top: 40px; }
.page-sub { text-align: center; color: var(--gray-dark); font-size: 1.1rem; margin: 0 0 1.5em; }

/* Two-column intro (photo hero pages don't need, but text sections do) */
.intro-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 48px;
  align-items: start;
}
.intro-grid h2 {
  font-size: clamp(1.7rem, 3.4vw, 2.5rem);
  line-height: 1.28;
}
.intro-grid p { font-size: 1.2rem; }

/* Breathing room between a hero video/photo and the section right after it.
   padding, not margin -- margin here would collapse through the section
   (divider has padding-top:0) and push the section's own background down
   with it, leaving a white gap above the colored background. */
.hero-photo + section .intro-grid {
  padding-top: clamp(48px, 8vw, 96px);
}
/* Homepage only, tighter than the general case above: 30px flat instead of
   a viewport-scaled clamp, per explicit request to sit the heading right
   under the wave rather than scale the gap with screen width. Extra class
   in the selector (not just a higher-specificity duplicate) so wwd-d2's
   own spacing on the What We Do page -- same hero-photo+intro-grid shape,
   not part of this ask -- stays on the shared rule above. */
.hero-photo + .home-d2 .intro-grid {
  padding-top: 10px;
}

/* Buttons */
.btn {
  display: inline-block;
  background: linear-gradient(135deg, var(--primary), var(--primary-dark));
  color: #ffffff;
  border: none;
  border-radius: 10px;
  padding: 15px 29px;
  text-decoration: none;
  font-family: var(--font-body);
  font-size: 1.05rem;
  font-weight: 600;
  cursor: pointer;
  transition: box-shadow 0.2s ease, transform 0.2s ease, filter 0.2s ease;
}
.btn:hover {
  filter: brightness(1.08);
  transform: translateY(-1px);
  box-shadow: 0 6px 20px rgba(var(--primary-rgb), 0.35);
}
.btn:active { transform: translateY(0); filter: brightness(0.97); }
.btn-row { text-align: center; margin-top: 28px; }
/* Full-bleed band behind the Long-Term Tenant Headaches card and its
   button: .wrap has no real max-width (see --max above) so it already
   spans the section edge-to-edge, but its own side padding left a strip
   of the section's translucent, video-showing background visible beside
   the (already opaque) card and below the button -- against the request
   that the card's solid look run uninterrupted down to Contact Us. Negative
   margins cancel .wrap's padding so this band's background reaches the
   true section edges; the matching padding brings the card and button
   back to their normal inset. flow-root keeps problems-box's own
   margin-top from collapsing through this band's top and dragging its
   background up with it. home-d3's own bottom padding moves onto this
   band (painted with the fixed opaque copy of --white, immune to the
   video retint -- same var the stacking cards use) instead of being left
   as one last translucent sliver before the section's true edge. */
.home-d3 { padding-bottom: 0; }
/* Admin-controlled spacer between the stacking-cards deck and the band
   below (see the .d3-gap div in index.html for why this can't just be
   padding/margin on either of its neighbors). min-height, not 0 -- the
   Layout admin is click-to-select now, and a truly empty box has no
   rendered area to click. 6px is an imperceptible sliver of the section's
   own background until the owner opens it up further. */
.d3-gap { display: block; min-height: 6px; }
.home-d3 .problems-band {
  display: flow-root;
  background: var(--card-opaque-bg);
  margin-left: calc(max(32px, 4vw) * -1);
  margin-right: calc(max(32px, 4vw) * -1);
  padding-left: max(32px, 4vw);
  padding-right: max(32px, 4vw);
  padding-bottom: clamp(48px, 6vw, 86px);
}

/* Problems box (matches surrounding gray section) */
.problems-box {
  background: var(--gray-light-opaque);
  padding: 48px 40px;
  text-align: center;
  border-radius: var(--radius-lg);
}
.problems-box h3 {
  font-family: var(--font-body);
  font-weight: 700;
  letter-spacing: 0.06em;
  font-size: 1.2rem;
  color: var(--primary);
  margin-bottom: 40px;
}
.icon-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 32px;
}
.icon-grid .icon-item svg { width: 44px; height: 44px; margin: 0 auto 16px; display: block; color: var(--primary); }
.icon-grid .icon-item p { font-size: 0.94rem; color: var(--ink); margin: 0; }

/* Photo feature grid (4 columns, photo + caption). Captions get a fixed
   min-height (enough for the longest caption's 3 lines) so every
   image starts at the same row position regardless of how many lines
   a given caption wraps to. */
.feature-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 32px;
}
.feature-grid figure { margin: 0; display: flex; flex-direction: column; gap: 16px; }
.feature-grid img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  transition: transform 0.5s var(--ease-out);
}
.feature-grid figure:hover img { transform: scale(1.04); }
@media (prefers-reduced-motion: reduce) {
  .feature-grid img { transition: none; }
}
.feature-grid h3 {
  font-size: 1.2rem;
  margin: 0;
  min-height: 6.9rem;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
}
.feature-grid p { font-size: 0.98rem; color: var(--ink); margin: 0; }

/* Homepage feature grid: photo above title (What We Do page keeps the
   default title-above-photo order, so this is scoped to home-d3 only). */
.home-d3 .feature-grid img { order: -1; }
.home-d3 .feature-grid h3 { min-height: 0; }

/* Scroll-pinned feature showcase (home-d3): a single photo pins on the
   left while the four captions scroll past on the right; motion.js swaps
   which photo is active as each caption crosses the viewport's center.
   The four source photos have different native aspect ratios, so
   motion.js also resizes .scrollpin-media's height (via inline style, from
   the active photo's real loaded dimensions) to match whichever photo is
   active -- the box hugs the photo instead of letterboxing it inside a
   fixed shape. The plain `height` below is only a no-JS/pre-JS fallback
   (deliberately not `aspect-ratio`: paired with an auto/stretch-derived
   width, aspect-ratio makes the browser solve width from height * ratio
   instead, overriding the grid stretch and shrinking the whole box, which
   is exactly the bug this component used to have); object-fit:contain is
   a safety net for the rare case where max-height still has to clamp a
   very tall photo. Falls back to a stacked, still-pinned layout on narrow
   viewports rather than an unpinned grid, so distinct scroll-reveal isn't
   duplicated in the DOM for mobile. */
.scrollpin {
  display: grid;
  grid-template-columns: minmax(0, 0.9fr) minmax(0, 1fr);
  gap: 56px;
  align-items: start;
}
/* Mobile-only stacking cards (see the max-width: 900px override below and
   motion.js's initScrollPinCards); empty shells above that breakpoint. */
.scrollpin-cards { display: none; }
.scrollpin-media {
  position: sticky;
  /* Fallback only -- sizeMedia() in motion.js overwrites this inline with
     an exact px value once the active photo's rendered height is known,
     so the box centers on whatever height it actually rendered at. */
  top: 50vh;
  height: 60vh;
  max-height: calc(100vh - var(--header-h) - 64px);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--gray-light);
  box-shadow: var(--shadow-md);
  transition: height var(--dur-slow) var(--ease-out);
}
.scrollpin-media-inner { position: relative; width: 100%; height: 100%; }
.scrollpin-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
}
.scrollpin-img.is-active { opacity: 1; }
.scrollpin-steps { display: flex; flex-direction: column; }
.scrollpin-step {
  min-height: 62vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 24px 0 24px 28px;
  border-left: 3px solid var(--gray-mid);
  opacity: 0.45;
  transition: opacity var(--dur-mid) var(--ease-out), border-color var(--dur-mid) var(--ease-out);
}
.scrollpin-step.is-active { opacity: 1; border-color: var(--primary); }
.scrollpin-step h3 { font-size: 1.4rem; margin: 0 0 12px; }
.scrollpin-step p { margin: 0; color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
  .scrollpin-img, .scrollpin-step, .scrollpin-media { transition: none; }
}

/* Testimonials */
.testimonial-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 28px;
}
.testimonial-card {
  background: var(--white);
  padding: 32px 28px;
}
.testimonial-card .quote-mark {
  font-family: var(--font-head);
  font-size: 2.2rem;
  line-height: 1;
  color: var(--accent);
  margin-bottom: 8px;
  display: block;
}
.testimonial-card p.quote { font-size: 1.05rem; margin: 0 0 16px; }
.testimonial-card p.attribution { font-size: 1rem; color: var(--gray-dark); margin: 0; }

/* Accordion (How it works). padding-top, not margin-top: this is now the
   first element in .wwd-d4 (its old heading/paragraph moved into the
   hero), and .divider sections deliberately zero out their own
   padding-top for the wave clip-path math -- a margin here would collapse
   straight through that zero padding and escape as a gap of plain page
   background between the two sections' waves, instead of staying inside
   wwd-d4's own background. */
.accordion-title {
  text-align: center;
  font-weight: 600;
  font-size: 1.7rem;
  padding-top: 56px;
  margin: 0 0 16px;
}
details.acc {
  border-top: 1px solid var(--gray-mid);
  padding: 18px 0;
}
details.acc:last-of-type { border-bottom: 1px solid var(--gray-mid); }
details.acc summary {
  cursor: pointer;
  font-family: var(--font-head);
  font-weight: 500;
  font-size: 1.1rem;
  list-style: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  transition: color 0.15s ease;
}
details.acc summary:hover { color: var(--primary); }
details.acc summary::-webkit-details-marker { display: none; }
details.acc summary::after {
  content: "\2304";
  font-size: 1.3rem;
  margin-left: 16px;
  color: var(--primary);
  transition: transform 0.2s ease;
}
details.acc[open] summary::after { transform: rotate(180deg); }
details.acc p { margin: 14px 0 0; font-size: 1.05rem; }
details.acc[open] p { animation: accContentIn 0.32s var(--ease-out); }
@keyframes accContentIn {
  from { opacity: 0; transform: translateY(-6px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  details.acc[open] p { animation: none; }
}

/* Portfolio grid */
.photo-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
}
.photo-grid figure {
  margin: 0;
  aspect-ratio: 1/1;
  overflow: hidden;
  border-radius: var(--radius-md);
  transition: transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.photo-grid figure:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
.photo-grid img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.5s var(--ease-out); }
.photo-grid figure:hover img { transform: scale(1.04); }
@media (prefers-reduced-motion: reduce) {
  .photo-grid figure, .photo-grid img { transition: none; }
}

/* FAQ card grid */
.faq-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 24px;
}
.faq-card {
  background: var(--white);
  padding: 28px 24px;
  border-top: 3px solid var(--primary);
  border-radius: 0 0 var(--radius-md) var(--radius-md);
  transition: transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.faq-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
@media (prefers-reduced-motion: reduce) {
  .faq-card { transition: none; }
}
.faq-card h3 { font-size: 1.3rem; margin-bottom: 12px; }
.faq-card p { font-size: 1.02rem; color: var(--gray-dark); margin: 0; }

/* Team */
.team-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 28px;
}
.team-card {
  position: relative;
  background: #1a1a1a;
  overflow: hidden;
  border-radius: 14px;
  transition: transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
}
.team-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
@media (prefers-reduced-motion: reduce) {
  .team-card { transition: none; }
}
/* height:auto (not a fixed aspect-ratio + object-fit:cover crop) so the
   card hugs each photo's own real proportions -- without it, the browser
   falls back to the HTML height attribute as a literal pixel value
   instead of scaling with width, so the box ends up the wrong shape and
   the photo gets force-cropped/stretched to fill it. */
.team-card img { width: 100%; height: auto; display: block; }
.team-card .plate {
  position: absolute;
  left: 12.8%;
  bottom: 7.3%;
  width: 74.4%;
  box-sizing: border-box;
  background: var(--gray-light);
  padding: 12px 20px;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 1.1rem;
  text-align: center;
  border-radius: 8px;
}
/* height:auto for the same reason as .team-card img above -- otherwise
   the box takes its height from the HTML height attribute literally
   instead of scaling with width, stretching the photo to fill it. */
.about-photo img { width: 100%; max-width: 620px; height: auto; margin: 0 auto 40px; display: block; }

/* Contact form */
.contact-section .wrap { }
.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1.3fr;
  gap: 56px;
  align-items: start;
}
.contact-grid h2 { font-size: clamp(2.1rem, 4.6vw, 3.6rem); margin-bottom: 20px; }
.contact-grid > div:first-child p { font-size: 1.2rem; }
form.contact-form label {
  display: block;
  font-family: var(--font-body);
  font-size: 1rem;
  margin-bottom: 4px;
}
form.contact-form .req { color: #777; font-size: 0.9rem; font-weight: 400; }
form.contact-form .row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 32px;
  margin-bottom: 22px;
}
form.contact-form .field { margin-bottom: 22px; }
form.contact-form input[type="text"],
form.contact-form input[type="email"],
form.contact-form input[type="tel"],
form.contact-form textarea {
  width: 100%;
  border: none;
  border-bottom: 1px solid var(--gray-dark);
  background: transparent;
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1.05rem;
  padding: 6px 2px;
  font-family: inherit;
}
form.contact-form input:focus,
form.contact-form textarea:focus { outline: none; border-bottom: 2px solid var(--primary); }
form.contact-form textarea { resize: vertical; min-height: 80px; }
form.contact-form .checkbox-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 22px;
  font-size: 1rem;
}
form.contact-form .checkbox-row input { width: 16px; height: 16px; }
.form-status { margin-top: 16px; font-size: 0.98rem; }
.form-status.ok { color: #196619; }
.form-status.err { color: #a11; }
:root[data-theme="dark"] .form-status.ok { color: #5fd576; }
:root[data-theme="dark"] .form-status.err { color: #ff7a7a; }

/* Footer */
footer.site {
  position: relative;
  background: var(--gray-light);
  padding: 56px 0 32px;
}
footer.site .wrap {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 40px;
}
footer.site .logo-foot img { height: 92px; }
footer.site .foot-cols { display: flex; gap: 64px; }
footer.site h4 { font-family: var(--font-head); font-weight: 500; font-size: 1.05rem; color: var(--primary); margin: 0 0 12px; }
footer.site p, footer.site a { font-family: var(--font-body); font-size: 0.98rem; color: var(--ink); text-decoration: none; margin: 0 0 6px; transition: color 0.15s ease; }
footer.site a:hover { color: var(--primary); text-decoration: underline; }

@media (max-width: 900px) {
  .logo img { height: clamp(64px, 12vw, 100px); }
  .icon-grid { grid-template-columns: repeat(2, 1fr); }
  .feature-grid { grid-template-columns: repeat(2, 1fr); }
  .faq-grid { grid-template-columns: repeat(2, 1fr); }
  /* Below the two-column breakpoint there's no room for a photo pinned
     beside scrolling captions. Below the breakpoint the desktop pin/swap
     layout (.scrollpin) is hidden entirely; motion.js relocates each real
     photo and caption into the matching .scrollpin-card-content instead
     (see index.html), producing the CodyHouse/scroll-driven-animations
     "stacking cards" effect: each card sticks at the same spot as the next
     one arrives, shrinking to peek out from behind it. Moving the actual
     elements (not clones) means there's still exactly one copy of each,
     so the admin photo/text editor keeps working regardless of which
     layout currently holds them.
     The deck itself (which cards show, how much each has shrunk, when the
     whole pile releases and scrolls away) is computed and applied directly
     by motion.js from scroll position -- see initScrollPinCards there for
     why: 4 independent position:sticky cards sharing one containing block
     each release at a slightly different scroll position (based on each
     one's own accumulated height), so the deck fell apart card-by-card
     instead of moving as one pile. `.is-stacking` is added by that same
     script once it's built the deck; without it (motion.js failed to
     load, or prefers-reduced-motion) cards fall back to this plain
     stacked list, each fully shown, no shrinking or pinning. */
  .scrollpin { display: none; }
  .scrollpin-cards {
    display: block;
    list-style: none;
    margin: 0;
    padding: 0;
    position: relative;
  }
  /* Overrides the inline margin-top:48px on .problems-box (see
     index.html) -- sibling margins collapse to the larger of the two, so
     a margin-bottom set here on .scrollpin-cards instead would just be
     absorbed by that 48px and change nothing. !important because it's
     specifically countering an inline style, which otherwise wins
     regardless of this rule's own specificity. */
  .problems-box { margin-top: 30px !important; }
  .scrollpin-card + .scrollpin-card { margin-top: 24px; }
  .scrollpin-cards.is-stacking .scrollpin-stage {
    position: sticky;
    /* Fallback only -- measure() in motion.js overwrites this inline with
       an exact px value once stageHeight is known, so the stack centers
       on whatever height it actually rendered at instead of assuming one. */
    top: 50vh;
    /* Explicit stacking order for the pinned deck: it already paints above
       .problems-box by default (a positioned descendant beats a plain
       in-flow sibling in paint order), but that default is implicit and
       silently breaks if a future change gives .problems-box (or anything
       between it and .scrollpin-cards) its own position/transform. Pinning
       this explicitly guarantees the last card stays fully opaque over the
       section below for its whole sticky hold, instead of depending on
       paint-order trivia holding forever. */
    z-index: 1;
  }
  .scrollpin-cards.is-stacking .scrollpin-card {
    position: absolute;
    inset: 0;
    width: 100%;
  }
  .scrollpin-cards.is-stacking .scrollpin-card + .scrollpin-card { margin-top: 0; }
  .scrollpin-card-content {
    transform-origin: 50% 0%;
    will-change: transform;
    background: var(--card-opaque-bg);
    border-radius: var(--radius-md);
    overflow: hidden;
    box-shadow: var(--shadow-lg);
  }
  /* Reset the desktop pin/swap presentation now that these are plain,
     always-visible children of a card: no absolute stacking, no
     crossfade, no rounding of their own (the card clips both to match,
     so the photo's bottom edge meets the caption's top edge as one
     seamless card instead of two stacked rounded rectangles). */
  .scrollpin-card-content .scrollpin-img {
    position: static;
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 10;
    object-fit: cover;
    opacity: 1 !important;
    transition: none;
    /* !important: site-images.js sets an inline border-radius here per the
       admin panel's own per-photo radius setting (see symmetricRadius in
       site-images.js) -- meant for the desktop pinned photo, not this
       card, and would otherwise beat any plain CSS rule. */
    border-radius: 0 !important;
    display: block;
  }
  .scrollpin-card-content .scrollpin-step {
    min-height: 0;
    padding: 24px 20px;
    background: transparent;
    border-left: none;
    border-radius: 0;
    box-shadow: none;
    opacity: 1 !important;
  }
  .testimonial-grid { grid-template-columns: 1fr; }
  .team-grid { grid-template-columns: 1fr; }
  .photo-grid { grid-template-columns: repeat(2, 1fr); }
  .intro-grid { grid-template-columns: 1fr; }
  .contact-grid { grid-template-columns: 1fr; }
  form.contact-form .row { grid-template-columns: 1fr; gap: 0; }
  footer.site .foot-cols { gap: 40px; flex-wrap: wrap; }

  nav.main { gap: 10px; }
  .nav-toggle { display: flex; order: 2; }
  .theme-toggle { order: 1; margin-left: 0; }
  .nav-links {
    display: flex;
    flex-direction: column;
    align-items: center;
    position: fixed;
    inset: 0;
    order: 3;
    background: var(--white);
    padding: 96px 32px 40px;
    z-index: 10000;
    visibility: hidden;
    opacity: 0;
    transform: scale(0.97);
    transition: opacity 0.22s var(--ease-out), transform 0.22s var(--ease-out), visibility 0s linear 0.22s;
  }
  .nav-links.open {
    visibility: visible;
    opacity: 1;
    transform: none;
    transition: opacity 0.22s var(--ease-out), transform 0.22s var(--ease-out), visibility 0s linear 0s;
  }
  .nav-links-primary {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    flex: 1;
    width: 100%;
  }
  .nav-links-primary a {
    padding: 14px 0;
    width: auto;
    font-size: 1.7rem;
    opacity: 0;
    transform: translateY(10px);
    transition: opacity 0.2s var(--ease-out), transform 0.2s var(--ease-out), color 0.15s ease;
  }
  .nav-links.open .nav-links-primary a { opacity: 1; transform: none; }
  .nav-links-primary a:nth-child(1) { transition-delay: 0.04s; }
  .nav-links-primary a:nth-child(2) { transition-delay: 0.07s; }
  .nav-links-primary a:nth-child(3) { transition-delay: 0.1s; }
  .nav-links-primary a:nth-child(4) { transition-delay: 0.13s; }
  .nav-links-primary a:nth-child(5) { transition-delay: 0.16s; }
  .nav-links-primary a:nth-child(6) { transition-delay: 0.19s; }
  /* Call/Email: icon-only actions, side by side, pinned to the bottom
     of the panel and set apart from the page links with a rule. */
  .nav-links-actions {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 24px;
    width: 100%;
    max-width: 220px;
    padding-top: 20px;
    border-top: 1px solid var(--gray-mid);
    opacity: 0;
    transform: translateY(10px);
    transition: opacity 0.2s var(--ease-out), transform 0.2s var(--ease-out);
    transition-delay: 0.2s;
  }
  .nav-links.open .nav-links-actions { opacity: 1; transform: none; }
  .nav-links-actions a.nav-link-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: 1px solid var(--gray-mid);
    color: var(--ink);
    transition: border-color 0.15s ease, opacity 0.15s ease;
  }
  .nav-links-actions a.nav-link-action:hover { border-color: var(--ink); }
  .nav-links-actions a.nav-link-action svg { width: 18px; height: 18px; display: block; }
  @media (prefers-reduced-motion: reduce) {
    .nav-links, .nav-links-primary a, .nav-links-actions { transition: none !important; }
  }
}

@media (max-width: 640px) {
  .icon-grid, .photo-grid { grid-template-columns: 1fr 1fr; }
  .faq-grid, .feature-grid { grid-template-columns: 1fr; }
  .faq-card h3 { font-size: 1.3rem; }
  .faq-card p { font-size: 1.05rem; }
  .feature-grid h3 { min-height: 0; }
}

