/* ==========================================================================
   WEMADE — Page-specific section styles (not shared header/footer chrome —
   see css/components.css for that).
   ========================================================================== */

/* ---- Hero ----
   Art-directed: two different crops of the same photo (a <picture> with a
   mobile <source>, not one image resized), so the king stays well-framed
   at both aspect ratios instead of a single wide shot getting cropped
   into an accidental composition on a phone. Desktop text sits directly
   on the photo's own naturally-light left side (dark/teal text, no
   scrim needed); mobile's text sits over a dark gradient scrim at the
   bottom instead, since the mobile crop doesn't leave a comparably light
   patch to anchor dark text against. */
.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: center;
  overflow: hidden;
  padding-top: calc(var(--nav-h) + var(--nav-float-gap));
}
.hero__media { position: absolute; inset: 0; z-index: 0; overflow: hidden; }
/* Plain center crop, no shift/scale hack — those (object-position:0%,
   translateX, scale) were specifically compensating for the previous
   desktop photo, whose king sat closer to center with the left negative
   space getting cropped away at typical viewport ratios. This image
   (herodesk2 — Kh's replacement) is already framed the way that one was
   trying to fake: real negative space on the left, king genuinely on
   the right, so it needs none of that. */
.hero__img { width: 100%; height: 100%; object-fit: cover; object-position: center; }
/* Fades the photo's own bottom edge into the page background so it
   blends into Why Us below instead of ending on a hard rectangular cut
   (Kh's call) — works because both this section and Why Us share the
   exact same --paper color, so the photo is dissolving into the color
   that's already sitting right below it, not into an unrelated tone.
   Mobile already has its own version of this rule (fading to navy
   instead, since mobile's hero content panel is dark) — that one is
   scoped inside the max-width:760px block further down and fully
   overrides every property set here, so the two never mix. */
.hero__media::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 240px;
  background: linear-gradient(to bottom, transparent, var(--paper));
  pointer-events: none;
}

/* Its own left padding instead of .container (Kh's call — on a genuinely
   wide monitor, .container's own max-width centers it with room to
   spare on both sides, so the text sat in the middle-left of the
   viewport rather than hugging its actual left edge). Padding matches
   .container's own gutter var, so it still lines up with every other
   section's left edge at ordinary widths — just without the centering
   that only shows up past ~1680px. */
.hero__content { position: relative; z-index: 1; max-width: 640px; padding-left: var(--gutter); }
.hero__title {
  font-size: clamp(34px, 5vw, 56px);
  line-height: 1.08;
  color: var(--navy);
}
/* navy-700, not teal (Kh's call — teal was barely readable against the
   photo's own light blue-white tones, too close in hue/lightness to it).
   Darker, so it holds contrast against the image while still reading as
   a distinct second color from --navy above it. */
.hero__title b { color: var(--navy-700); font-weight: 700; }
.hero__subtitle {
  margin-top: 20px;
  max-width: 46ch;
  font-size: 16px;
  line-height: 1.6;
  /* offwhite, not navy-600a (Kh's call) — the new hero photo's left side
     is a medium blue, not the light near-white the old one had, so a
     dark subtitle color no longer has much to contrast against. */
  color: var(--offwhite);
}
/* border is always there (just transparent by default) rather than only
   added on :hover — a border-color swap doesn't shift the button's size,
   adding a border from nothing would. */
.hero__cta { margin-top: 32px; border: 1.5px solid transparent; }

/* Entrance reveal, title -> subtitle -> button, for both breakpoints.
   .is-in toggled by js/components.js's initHeroReveal via
   IntersectionObserver (Kh's call — same replay-on-re-entry behavior as
   Why Us/About/the priorities chart, not a fire-once animation). The
   hero happens to already be in view the instant the page loads, so
   that observer's initial callback plays it immediately too — no
   separate "on load" case needed. prefers-reduced-motion already
   flattens this site-wide via the global rule in css/base.css. */
.hero__title, .hero__subtitle, .hero__cta {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.8s var(--ease-quart-out), transform 0.8s var(--ease-quart-out);
}
.hero__title.is-in { transition-delay: 0.1s; }
.hero__subtitle.is-in { transition-delay: 0.3s; }
.hero__cta.is-in { transition-delay: 0.45s; }
.hero__title.is-in, .hero__subtitle.is-in, .hero__cta.is-in {
  opacity: 1;
  transform: translateY(0);
}
/* navy, not offwhite — against the photo's light blue-white checkered
   board an offwhite border/text on hover read as barely-there (Kh's
   call: "too clear/light"). border-color/color get their own slightly
   longer, eased transition (not the shared 0.3s from .btn in
   components.css) for a visibly smoother hover specifically here — this
   only overrides the timing FUNCTION and duration, not what mobile's own
   override further down sets for the actual hover colors. */
.hero__cta:hover { background: transparent; border-color: var(--navy); color: var(--navy); }
.hero__cta { transition: background-color 0.3s, color 0.4s var(--ease-quart-out), border-color 0.4s var(--ease-quart-out); }

@media (max-width: 760px) {
  /* Restructured to a normal stacked layout — photo, then text right
     under it (Kh's call: "the text under must stick to the image from
     below"). The previous version had .hero__media full-bleed and
     absolute behind .hero__content, which worked for a cover-fit photo
     but left no real relationship between "where the contained photo's
     own edge lands" and "where the text sits" once object-fit switched
     to contain — they were two independent overlapping layers, so
     closing that gap meant calculating exact pixel heights, fragile the
     moment the photo or viewport width changed. Normal flow doesn't have
     that problem: the image is a plain block at its natural (contain-
     equivalent) height, and the text just follows it. This also means
     .hero no longer needs the 100svh+265px height hack from the earlier
     "push the text down" requests — with the photo no longer trying to
     fill the whole first screen, that trade-off (CTA below the fold)
     isn't needed to get the same visual result. */
  .hero {
    flex-direction: column;
    align-items: stretch;
    min-height: 0;
    /* 0, not the nav-height gap the base rule sets — that gap was
       showing as the section's own plain background above the photo
       (Kh's call: "the image must reach the top of the header"). The
       navbar is position:fixed with its own stacking, so it still
       floats correctly on top of the photo with nothing here reserving
       room for it. */
    padding-top: 0;
    padding-bottom: 0;
  }
  .hero__media { display: block; position: relative; inset: auto; z-index: auto; overflow: visible; }
  /* No object-fit needed — a plain responsive image (auto height) IS
     "uncropped, unzoomed, top-aligned" once it's back in normal flow. */
  .hero__img { display: block; width: 100%; height: auto; object-fit: initial; transform: none; }
  /* Fades the photo's own bottom edge toward navy, so the solid navy
     block below doesn't read as a hard cut — the two "merge" (Kh's
     call) instead of just being stacked. */
  .hero__media::after {
    content: "";
    position: absolute;
    inset: auto 0 0 0;
    height: 90px;
    background: linear-gradient(to bottom, transparent, var(--navy));
    pointer-events: none;
  }

  /* Back to a dark background behind the text ("the same background as
     before", Kh's call) — solid navy now rather than the old gradient
     scrim, since it's not sitting on top of photo pixels anymore, just
     continuing the fade from .hero__media::after above with zero gap in
     between (margin-top: -1px trims any subpixel seam). */
  .hero__content {
    position: relative;
    max-width: none;
    margin-top: -1px;
    padding: 28px 20px 40px;
    background: var(--navy);
  }
  .hero__title { color: var(--offwhite); }
  .hero__title b { color: var(--steel); }
  .hero__subtitle { color: var(--offwhite-60); }
  /* .btn--navy's navy background is now the exact same color as
     .hero__content sitting behind it — without this the button has no
     visible shape at all, just floating text. Inverted (light bg, navy
     text) instead, so it actually reads as a button. */
  .hero__cta { background: var(--offwhite); color: var(--navy); }
  /* The shared :hover rule above went navy border/text for desktop (the
     photo behind it there is light) — on this navy content panel that
     would be navy-on-navy, invisible. Pinned back to offwhite here so
     mobile's hover stays visible regardless of what desktop's says. */
  .hero__cta:hover { border-color: var(--offwhite); color: var(--offwhite); }
}

/* ---- About: process timeline ----
   Approved via a standalone artifact preview before landing here (scroll-
   scrubbed line + steps alternating above/below on desktop, left/right of
   a centered line on mobile). js/about.js renders this from
   WM_DATA.process and drives --progress + .is-in on scroll — see that file
   for the full reasoning on why it's scroll-scrubbed rather than a fixed
   on-enter animation. */

/* One grid for the whole thing: 4 step-columns x 3 rows (above-content /
   the line / below-content). The line sits in its own thin middle row
   spanning all columns, so it's naturally level with every dot — no
   absolute-positioning math needed to keep them aligned. */
.process {
  max-width: 1040px;
  margin: 0 auto;
  padding: 0 20px;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: auto 40px auto;
  align-items: center;
}

/* Line growth is scroll-scrubbed — js/about.js sets --progress every
   frame and this reads it directly, no CSS transition fighting the scrub.
   Dots/steps below still get an eased transition; only *when* each one
   triggers is tied to scroll (the reveal motion itself stays smooth, not
   scrubbed pixel-by-pixel, which read as mechanical when tried). */
.process__line-wrap { grid-row: 2; grid-column: 1 / -1; position: relative; height: 2px; margin: 0 36px; }
.process__line-bg { position: absolute; inset: 0; background: var(--navy-150); }
.process__line-draw {
  position: absolute;
  inset: 0;
  background: var(--teal);
  transform-origin: left center;
  transform: scaleX(var(--progress, 0));
}

.process__dot {
  grid-row: 2;
  justify-self: center;
  width: 9px;
  height: 9px;
  border-radius: 999px;
  background: var(--navy);
  transform: scale(0);
  transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.process__dot.is-in { transform: scale(1); }

.process__step {
  grid-row: 1;
  align-self: end;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-bottom: 22px;
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
.process__step--below { grid-row: 3; align-self: start; padding-top: 22px; padding-bottom: 0; }
.process__step.is-in { opacity: 1; transform: translateY(0); }

/* Desktop needs each step/dot in its own grid COLUMN (1-4); mobile needs
   them in their own grid ROW instead (see the media query below) — that
   grid-column assignment is scoped to desktop only, otherwise its
   specificity (a class + a pseudo-class) would beat the mobile override's
   plain-class selector and win regardless of source order, no matter what
   column the mobile rule asks for. (Confirmed via screenshot in the
   artifact preview — steps drifted rightward off the mobile grid before
   this was scoped.) .process__step and .process__line-wrap are both
   <div>, but line-wrap comes after all 4 steps in the markup, so
   nth-of-type(1..4) still lands on the steps. */
@media (min-width: 761px) {
  .process__step:nth-of-type(1), .process__dot:nth-of-type(1) { grid-column: 1; }
  .process__step:nth-of-type(2), .process__dot:nth-of-type(2) { grid-column: 2; }
  .process__step:nth-of-type(3), .process__dot:nth-of-type(3) { grid-column: 3; }
  .process__step:nth-of-type(4), .process__dot:nth-of-type(4) { grid-column: 4; }
}

.process__badge {
  width: 72px;
  height: 72px;
  border-radius: 999px;
  background: var(--navy);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 10px 24px rgba(0, 39, 74, 0.22);
  flex-shrink: 0;
}
.process__badge svg { width: 30px; height: 30px; color: var(--paper); }
.process__step .process__badge { order: 1; margin-bottom: 14px; }
.process__step--below .process__badge { order: 3; margin-bottom: 0; margin-top: 14px; }

.process__step-index { font-size: 11px; font-weight: 600; letter-spacing: 0.12em; color: var(--steel); order: 2; }
/* An actual <h3> now (was a <div>) — SEO/AEO wants a real heading per
   step, not just a styled div. line-height/letter-spacing are pinned back
   to the body defaults to cancel out base.css's h1-h4 reset, so the tag
   swap has zero visual effect (verified pixel-for-pixel). */
.process__step-title { font-size: 16px; font-weight: 600; line-height: 1.5; letter-spacing: normal; margin-top: 6px; text-align: center; order: 2; }
.process__step-desc { font-size: 12.5px; color: var(--navy-600a); text-align: center; max-width: 20ch; margin-top: 6px; line-height: 1.5; order: 2; }
.process__step--below .process__step-index,
.process__step--below .process__step-title,
.process__step--below .process__step-desc { order: 2; }

/* Mobile: a real vertical timeline with the line down the CENTER and
   steps alternating left/right of it — same idea as desktop's
   above/below zigzag, just turned 90°. 3 grid columns: left content |
   line/dot spine | right content. Odd steps sit right, even sit left. */
@media (max-width: 760px) {
  .process {
    grid-template-columns: 1fr 28px 1fr;
    grid-template-rows: none;
    row-gap: 40px;
    column-gap: 12px;
    position: relative;
    padding: 0 12px;
  }

  .process__step:nth-of-type(1), .process__dot:nth-of-type(1) { grid-row: 1; }
  .process__step:nth-of-type(2), .process__dot:nth-of-type(2) { grid-row: 2; }
  .process__step:nth-of-type(3), .process__dot:nth-of-type(3) { grid-row: 3; }
  .process__step:nth-of-type(4), .process__dot:nth-of-type(4) { grid-row: 4; }

  .process__step,
  .process__step--below {
    align-self: start;
    flex-direction: column;
    padding: 0;
  }
  /* Odd steps (1, 3) sit right of the line; even (2, 4) sit left. */
  .process__step:nth-of-type(odd) { grid-column: 3; align-items: flex-start; text-align: left; }
  .process__step:nth-of-type(even) { grid-column: 1; align-items: flex-end; text-align: right; }

  .process__step .process__badge,
  .process__step--below .process__badge {
    order: 1;
    margin: 0 0 12px;
    width: 52px;
    height: 52px;
  }
  .process__step .process__badge svg,
  .process__step--below .process__badge svg { width: 22px; height: 22px; }
  .process__step-index, .process__step-title, .process__step-desc { max-width: none; }

  /* Dot centers on the badge (52px tall, so ~26px down) rather than the
     row's own top edge. */
  .process__dot { grid-column: 2; justify-self: center; align-self: start; margin-top: 21px; }

  .process__line-wrap { display: none; }
  .process::before,
  .process::after {
    content: "";
    position: absolute;
    left: 50%;
    top: 21px;
    bottom: 21px;
    width: 2px;
    transform: translateX(-50%);
  }
  .process::before { background: var(--navy-150); }
  .process::after {
    background: var(--teal);
    transform-origin: top center;
    /* Same scroll-scrubbed --progress as the desktop line — no transition. */
    transform: translateX(-50%) scaleY(var(--progress, 0));
  }
}

/* ---- About: mini CTA, between the process timeline and the search bar ----
   Extra margin-top (Kh's call — "a little bit" more room off the process
   timeline above it) and a deliberately different texture from the
   section-head title up top: that one has ONE word teal (no highlight
   box), this has ONE word inside a highlight-marker block, so they read
   as related but not identical. Bumped up a size (Kh's call — read as
   too small for a heading) now that only "BRAND" carries the highlight
   treatment, not the whole line. */
.about-cta { max-width: 640px; margin: 148px auto 0; padding-inline: 20px; text-align: center; }
.about-cta__title { font-size: 32px; font-weight: 700; letter-spacing: 0.01em; color: var(--navy); }

/* Highlighter-marker sweep on just "BRAND", on-brand (teal) — .is-in
   (js/about.js, via IntersectionObserver, fires once when scrolled into
   view) grows the ::before block from 0 to full width and swaps the word
   to white so it reads against the now-solid fill. Default state (no
   .is-in — no JS, or just not scrolled to yet) is still fully readable
   on its own: teal word on the plain page background, no highlight
   needed for legibility. Grows from the center outward both directions
   at once (Kh's call — "two ways"), fast (0.35s). */
.about-cta__highlight {
  position: relative;
  z-index: 0;
  display: inline-block;
  padding: 2px 12px;
  color: var(--teal);
  transition: color 0.15s ease 0.16s;
}
.about-cta__highlight::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 2px 0;
  border-radius: 6px;
  background: var(--teal);
  transform: scaleX(0);
  transform-origin: center;
  transition: transform 0.35s var(--ease-quart-out);
}
.about-cta__highlight.is-in { color: var(--white); }
.about-cta__highlight.is-in::before { transform: scaleX(1); }

.about-cta__subtitle { margin-top: 10px; font-size: 14.5px; color: var(--navy-600a); }
@media (max-width: 640px) {
  .about-cta { margin-top: 88px; }
  .about-cta__title { font-size: 23px; letter-spacing: 0.005em; }
}

/* ---- About: floating search-bar card ----
   Sits below the mini CTA above. "Floating" is both the look (rounded,
   elevated shadow, transparent fill so .bg-paper shows through) and a
   slow vertical bob — genuinely floating, not just styled to suggest it.
   js/about.js types WM_DATA.aboutSearchPrompts into .search-float__text
   letter by letter, pauses, deletes, and loops; the blinking caret is
   pure CSS so it doesn't need JS keeping time for it. */
.search-float {
  max-width: 640px;
  margin: 28px auto 0;
  padding-inline: 20px;
}
.search-float__bar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 18px 28px;
  border-radius: 999px;
  border: 1.5px solid var(--navy-200);
  background: transparent;
  box-shadow: 0 18px 40px var(--navy-100), 0 4px 14px var(--navy-050);
  animation: wm-float-bob 4s ease-in-out infinite;
}
@keyframes wm-float-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-8px); }
}
.search-float__icon { flex-shrink: 0; width: 20px; height: 20px; color: var(--navy-450); }
.search-float__icon svg { width: 100%; height: 100%; }
.search-float__text {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 14.5px;
  color: var(--navy);
  white-space: nowrap;
  overflow: hidden;
  /* Safety net, not the fix — js/about.js now types one short phrase at
     a time (not the full joined sentence, which actually overflowed and
     got silently clipped on mobile at 13px). If a future phrase is ever
     longer than the bar, this truncates visibly instead of hiding it. */
  text-overflow: ellipsis;
}
/* Blinking text-input caret, right after however much text has "typed" so
   far — steps(2) gives a hard on/off blink instead of a fade. */
.search-float__cursor {
  flex-shrink: 0;
  width: 1.5px;
  height: 18px;
  background: var(--navy);
  animation: wm-caret-blink 0.9s steps(2) infinite;
}
@keyframes wm-caret-blink { 50% { opacity: 0; } }

@media (max-width: 640px) {
  .search-float { margin-top: 48px; }
  .search-float__bar { padding: 15px 20px; gap: 10px; }
  .search-float__text { font-size: 13px; }
}

/* ---- Why Us: zigzag pill list ----
   Reference: Kh's "Steps to create a checklist" infographic —
   alternating pill cards with a numbered badge overlapping one edge.
   js/why-us.js renders the rows from WM_DATA.whyUs; title + description
   are both real, static text (no hover-to-reveal like the previous
   version) so an AI answer engine or Google sees the actual content
   without running any interaction (Kh's SEO/AEO/GEO ask). Deliberately
   different material from About's icon-timeline — pill cards + a badge
   here, plain icon circles + a through-line there — even though both are
   zigzags, per Kh's call, and colors are existing brand tokens only
   (a light→dark→light arc across --steel/--navy-700/--teal/--navy/
   --navy-800), not new hex values. Base rules below are the mobile
   layout (full-width rows) — desktop staggers each row toward its
   badge's side (see the min-width:641px block near the bottom), which
   is the one real difference between the two breakpoints here. */
.why-us { max-width: 640px; margin: 0 auto; }
.why-us__list { display: flex; flex-direction: column; gap: 32px; }

/* Scroll-in reveal — same idea as About's timeline (js/about.js drives
   .is-in there off scroll position; here it's a plain IntersectionObserver
   toggle in js/why-us.js, since there's no line to keep scroll-scrubbed).
   Each row slides in from whichever side its own badge is on, so the
   alternation reads as "one from left, one from right" going down the
   page — toggles both ways (scrolling back up hides it again), matching
   how About's steps behave rather than a fire-once entrance. */
.why-item {
  display: flex;
  align-items: center;
  opacity: 0;
  transition: opacity 0.6s var(--ease-quart-out), transform 0.6s var(--ease-quart-out);
  transition-delay: var(--why-delay, 0ms);
}
.why-item--right { flex-direction: row; transform: translateX(64px); }
.why-item--left { flex-direction: row-reverse; transform: translateX(-64px); }
.why-item.is-in { opacity: 1; transform: translateX(0); }

.why-item__card {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  /* Without this, a 1-line desc ("We Move Fast") makes its own card
     ~19px shorter than every 2-line-desc card — the actual flex `gap`
     between rows stays a fixed 32px either way (confirmed by measuring),
     but the shorter card reads as unevenly spaced from its neighbors
     since there's more open pill around less content. Pinned to the
     tallest current card (2-line desc + padding) so every row matches
     regardless of how long its own sentence is. */
  min-height: 98px;
  display: flex;
  align-items: center;
  gap: 16px;
  background: var(--white);
  border-radius: 999px;
  padding: 18px 30px;
  box-shadow: 0 10px 26px var(--navy-100), 0 2px 6px var(--navy-050);
  transition: box-shadow 0.3s, transform 0.3s;
}
/* Icon always sits at the card's OUTER end (away from the badge) — DOM
   order is icon-then-copy either way, so a --left row's card just runs
   its own flex in reverse to put the icon on the far side from its
   left-side badge. */
.why-item--left .why-item__card { flex-direction: row-reverse; }
.why-item:hover .why-item__card { box-shadow: 0 16px 34px var(--navy-200), 0 4px 10px var(--navy-100); transform: translateY(-2px); }

/* Short accent line above each card, echoing the reference's rhythm —
   lives on the card (not the row) so it doesn't need its own left/right
   flip logic beyond which side the text starts on. */
.why-item__card::before {
  content: "";
  position: absolute;
  top: -16px;
  left: 30px;
  width: 84px;
  height: 2px;
  background: var(--why-accent, var(--teal));
}
.why-item--left .why-item__card::before { left: auto; right: 30px; }

.why-item__icon { flex-shrink: 0; width: 26px; height: 26px; color: var(--navy); }
.why-item__icon svg { width: 100%; height: 100%; }

/* flex-grow:1 is load-bearing, not decoration: without it, a short desc
   that doesn't need to wrap (only "We Move Fast" currently) sizes to its
   own min-content instead of the full row, and on a --left row (card is
   row-reverse) the leftover space collects on the flex-start side, which
   for row-reverse is physically the RIGHT — i.e. between the badge and
   the text, not between the text and the icon. Every other row's desc
   happens to be long enough to wrap and incidentally fill the row, which
   is what hid this until a short one exposed it (confirmed by measuring
   real gaps: 2px on every row except the short one, which was 45px). */
.why-item__copy { flex: 1 1 auto; min-width: 0; }
.why-item__title { font-size: 16px; font-weight: 600; line-height: 1.3; color: var(--navy); }
.why-item__desc { font-size: 13px; line-height: 1.45; color: var(--navy-600a); margin-top: 4px; }
/* No-op outside the mobile media query below — see why-us.js for why
   the desc has this wrapper in the first place. */
.why-item__desc-collapse { display: contents; }

.why-item__badge {
  flex-shrink: 0;
  width: 64px;
  height: 64px;
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--why-accent, var(--navy));
  color: var(--white);
  font-size: 22px;
  font-weight: 800;
  box-shadow: 0 10px 22px var(--navy-200);
  z-index: 1;
}
/* Overlaps the card's rounded edge — the "attached badge" look from the
   reference. Margins aren't flipped by flex-direction, so both sides
   pull the badge the same physical direction (into the card). Both sides
   measured out to the same 2px gap to the nearest content, but a --left
   row reads as more cramped than --right (the badge is followed
   immediately by the first word you read, vs. --right where it trails
   after the text ends) — --left gets a smaller overlap so there's more
   breathing room specifically where reading starts. */
.why-item--right .why-item__badge { margin-left: -28px; }
.why-item--left .why-item__badge { margin-right: -20px; }

/* Desktop only (Kh's call, and the first reference image he sent) — each
   row shrinks a bit and hugs the side its own badge is on, so the whole
   row staggers left/right down the page instead of every row sharing the
   same edges. .why-us__list has no align-items set, so its children
   default to align-items:stretch (full width); align-self here overrides
   that per row, which is also what actually shifts the row horizontally
   — no margin:auto trick needed. */
@media (min-width: 641px) {
  .why-us { max-width: 820px; }
  .why-item { width: 82%; }
  .why-item--right { align-self: flex-end; }
  .why-item--left { align-self: flex-start; }
}

@media (max-width: 640px) {
  .why-us__list { gap: 26px; }
  /* Reset the desktop min-height — mobile's title-only default (desc
     collapsed to 0fr) is naturally uniform on its own, and forcing the
     desktop min-height here would puff up every closed row to a tall
     card that's mostly empty space. */
  .why-item__card { min-height: 0; padding: 14px 20px; gap: 12px; }
  .why-item__card::before { top: -13px; width: 60px; }
  .why-item__icon { width: 22px; height: 22px; }
  .why-item__title { font-size: 14.5px; }
  .why-item__desc { font-size: 12.5px; }
  .why-item__badge { width: 48px; height: 48px; font-size: 17px; }
  .why-item--right .why-item__badge { margin-left: -20px; }
  .why-item--left .why-item__badge { margin-right: -13px; }

  /* Title-only by default (Kh's call) — tap, or hover on the rare
     touch+trackpad hybrid, reveals the sentence. The desc's real text
     never leaves the DOM either way, so raw-HTML crawlers (the AEO/GEO
     case this section was just built around) still see it regardless of
     this being visually collapsed — this is presentation-only. The
     grid-rows trick (0fr -> 1fr) animates smoothly without hard-coding a
     max-height guess for text that can vary in length. */
  .why-item { cursor: pointer; }
  .why-item__desc-collapse {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows 0.35s var(--ease-quart-out);
  }
  .why-item__desc-collapse > .why-item__desc { overflow: hidden; margin-top: 0; }
  .why-item:hover .why-item__desc-collapse,
  .why-item.is-open .why-item__desc-collapse {
    grid-template-rows: 1fr;
  }
  .why-item:hover .why-item__desc-collapse > .why-item__desc,
  .why-item.is-open .why-item__desc-collapse > .why-item__desc {
    margin-top: 4px;
  }
}

/* ---- Why Us: research priorities donut chart ----
   Ported ~1:1 from a standalone artifact preview — see js/why-us.js's
   initPrioritiesChart for the animation logic (draw-in + count-up,
   replays every time it scrolls into view). Heading reuses the shared
   .section-head/__eyebrow/__title/__subtitle from css/base.css (Kh's
   call — it must match the same pattern as every other section head,
   not a one-off style) — .priorities__head only adds the extra top
   margin needed since this one isn't the first thing in its container
   like every other .section-head on the page is. */
/* margin-top: 48px read as too tight once actually seen (Kh's call —
   "must be bigger"), bumped back up partway toward .section-head's
   default 96px without going all the way back.
   margin-bottom is negative on purpose — the visible ring/labels sit
   well inside the SVG's own square box (empty margin left there
   deliberately, so labels never clip near the edge — see the
   viewBox/LEADER_BEND comments in js/why-us.js), so a small positive
   margin here still read as a big gap (Kh's call, measured: ~155px of
   that gap was inside the chart's own box, not from this margin). This
   pulls the box up to close most of that dead space while still leaving
   the topmost label some breathing room below the paragraph. */
.priorities__head { margin-top: 114px; margin-bottom: -90px; } /* +~1cm (Kh's call) */
@media (max-width: 640px) {
  .priorities__head { margin-top: 72px; margin-bottom: -50px; }
}

/* .section-head already carries its own margin-bottom (64/96px), so this
   just centers the chart — it doesn't need its own top margin on top of
   that. */
.priorities { margin: 0 auto; }
/* The square chart area (glow + ring + center figure) is its own box,
   separate from .priorities so the legend below can sit in normal flow
   instead of being squeezed inside a 1:1 aspect-ratio box too. */
.priorities__chart {
  position: relative;
  width: min(860px, 100%);
  aspect-ratio: 1 / 1;
  margin: 0 auto;
}
.priorities__svg { position: relative; width: 100%; height: 100%; overflow: visible; display: block; }
.priorities__glow {
  position: absolute;
  inset: 24%;
  border-radius: 50%;
  background: radial-gradient(circle, var(--teal) 0%, var(--steel) 45%, transparent 72%);
  filter: blur(46px);
  opacity: 0.5;
}
/* No visible stroke — gaps (well, overlaps, here) show whatever's behind
   them (the glow, then the page) rather than a painted-on ring shape. */
.priorities__ring-track { display: none; }
.priorities__ring-seg { fill: none; stroke-width: 44; stroke-linecap: round; opacity: 0.5; }

.priorities__leader { stroke: var(--navy-450); stroke-width: 1.6; stroke-dasharray: 1 6; stroke-linecap: round; fill: none; }
.priorities__leader-dot { fill: var(--navy-450); }

.priorities__label { font-family: var(--font-body); }
.priorities__label--pct { font-size: 27px; font-weight: 800; fill: var(--navy); }
.priorities__label--name {
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: 0.03em;
  fill: var(--navy-600a);
  text-transform: uppercase;
}

.priorities__center {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  pointer-events: none;
}
.priorities__center-num { font-size: 46px; font-weight: 800; color: var(--navy); line-height: 1; font-variant-numeric: tabular-nums; }
.priorities__center-label {
  margin-top: 6px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--navy-450);
}

/* Negative margin-top for the same reason as .priorities__head's — the
   ring/labels sit well inside the chart box's own bottom edge (measured:
   ~163px of empty space there), so a small positive margin still left a
   ~190px gap to the labels above it (Kh's call: "decrease this gap"). */
.priorities__legend {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px 22px;
  max-width: 640px;
  /* +~1cm bottom (Kh's call) — more room before the next section.
     Top pull reduced from -140px — measured the real visual gap (chart's
     lowest label to here) at only 23px, and Kh's call was that read as
     too tight; -98px gives ~65px of actual breathing room instead. */
  margin: -98px auto 38px;
}
.priorities__legend-item { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--navy-600a); }
.priorities__legend-swatch { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; }

/* Mobile polish (Kh's call — "enhance the looking"): the legend's ragged
   wrap (3 items, then 2 centered under them) read as messy at this width,
   so it's a clean single column instead; labels/center number get a
   modest size bump since they read as cramped/small there specifically,
   not just "smaller than desktop". */
@media (max-width: 640px) {
  /* margin-top: measured visual gap here was actually -2.8px (legend
     overlapping the lowest label) at the old -70px pull — mobile's dead
     zone is smaller than desktop's in absolute px, so the same pull
     overcorrected. -32px leaves ~35px of real breathing room instead. */
  .priorities__legend { flex-direction: column; align-items: center; gap: 10px; margin-top: -32px; margin-bottom: 28px; }
  /* Bumped again (Kh's call — still too small to read comfortably). */
  .priorities__label--pct { font-size: 27px; }
  .priorities__label--name { font-size: 14px; }
  .priorities__center-num { font-size: 34px; }
}

/* ---- Our Work: dot-nav slider ----
   Replaces the earlier Insights line-chart section, then a plain project
   grid, with this dot-nav + swipeable-card slider (Kh sent a reference
   screenshot: vertical numbered dots + info panel beside a row of
   cards, click a dot OR swipe the cards and the next project becomes the
   focus). See js/projects.js for why this uses scale/opacity for the
   "focused card" read instead of the reference's literal overlapping
   deck, and for how an `image` key swaps a placeholder for a real photo. */
.work-slider {
  display: grid;
  grid-template-columns: 300px 1fr;
  gap: 48px;
  align-items: center;
  max-width: 1080px;
  margin: 56px auto 0;
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.7s var(--ease-quart-out), transform 0.7s var(--ease-quart-out);
}
.work-slider.is-in { opacity: 1; transform: translateY(0); }

.work-slider__dots { position: relative; display: flex; flex-direction: column; gap: 14px; }
/* Connecting line behind the numbered dots, same idea as the process
   timeline's line-through-the-steps, just vertical here. */
.work-slider__dots::before {
  content: "";
  position: absolute;
  left: 17px;
  top: 18px;
  bottom: 18px;
  width: 2px;
  background: var(--navy-150);
  z-index: 0;
}
.work-slider__dot {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  border: none;
  background: none;
  padding: 0;
  cursor: pointer;
  font-size: 13px;
  font-weight: 700;
  color: var(--navy-450);
}
.work-slider__dot-num {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--white);
  border: 1.5px solid var(--navy-200);
  color: var(--navy-450);
  font-size: 12.5px;
  transition: background 0.25s, border-color 0.25s, color 0.25s;
}
.work-slider__dot.is-active .work-slider__dot-num { background: var(--navy); border-color: var(--navy); color: var(--offwhite); }

.work-slider__info { margin-top: 28px; }
.work-slider__title { font-size: 24px; font-weight: 800; color: var(--navy); letter-spacing: -0.01em; }
.work-slider__category { margin-top: 8px; font-size: 13.5px; color: var(--navy-600a); }

/* Cards stay side-by-side in one scroll-snap row (see the file-header
   comment in js/projects.js for why, not a true overlapping deck) —
   title + category are real static text on every card, not just the
   active one, for the same AEO/GEO reasoning as Why Us. */
.work-slider__track {
  display: flex;
  gap: 24px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding: 12px 8px 20px;
}
.work-slider__track::-webkit-scrollbar { display: none; }

/* Real elements (not ::before/::after — this track is built in JS, not
   static markup), sized to exactly half a card's width so the first and
   last cards can actually reach scroll-snap-align:center — see the
   matching comment in js/projects.js for the bug this fixes (the last
   project was unreachable by swipe). */
/* Half of .work-slider__card's own width (380px / 2) — keep these two in
   sync if the card width ever changes again. */
.work-slider__spacer { flex: none; width: calc(50% - 190px); }

/* Plain sharp-cornered rectangle — tried an oblique parallelogram (Kh's
   first ask) then a plain rectangle with no rounding at all (Kh's
   immediate follow-up correction), so this has neither clip-path nor
   border-radius on purpose. */
.work-slider__card {
  flex: 0 0 auto;
  width: 380px;
  scroll-snap-align: center;
  overflow: hidden;
  background: var(--white);
  box-shadow: 0 10px 26px var(--navy-100), 0 2px 6px var(--navy-050);
  opacity: 0.55;
  transform: scale(0.92);
  transition: transform 0.35s var(--ease-quart-out), opacity 0.35s;
}
.work-slider__card.is-active { opacity: 1; transform: scale(1); }

/* Portrait 3:4 frame (was 4:3 landscape) — the real shots Kh sent are all
   vertical phone photos, so a taller frame keeps more of each one visible
   before object-fit:cover crops it. Nudge toward 2/3 or 9/16 for even less
   crop if the cards need to show more. */
.work-slider__card-media { aspect-ratio: 3 / 4; overflow: hidden; background: var(--navy-050); }
.work-slider__card-media--placeholder { display: flex; align-items: center; justify-content: center; background: linear-gradient(160deg, var(--navy) 0%, var(--navy-700) 100%); }
.work-slider__card-placeholder-icon svg { width: 42px; height: 42px; color: var(--offwhite); opacity: 0.6; }
.work-slider__card-img { display: block; width: 100%; height: 100%; object-fit: cover; }


@media (max-width: 760px) {
  .work-slider { grid-template-columns: 1fr; gap: 24px; margin-top: 40px; }
  /* nowrap + clamp() on the dot size/gap (not the fixed 36px/14px the
     column layout used) — Kh's call: all 6 need to stay on one line down
     to the narrowest phones, not shrink-to-fit by accident or wrap. Sized
     so even at a 320px-wide screen, 6 dots + 5 gaps comfortably clears
     the container width. */
  /* justify-content:space-between, not the column layout's own gap, is
     what actually stretches the dots — Kh's call: they should span the
     full row width (dot 1 at the left edge, dot 6 at the right, matching
     the image below) instead of clustering to their own natural size on
     the left with empty space after the last one. The clamped gap below
     still applies as a MINIMUM if space-between would ever pack them
     tighter than that (very narrow screens). */
  .work-slider__dots { flex-direction: row; flex-wrap: nowrap; justify-content: space-between; gap: clamp(6px, 2vw, 14px); }
  .work-slider__dot-num { width: clamp(26px, 8vw, 36px); height: clamp(26px, 8vw, 36px); font-size: clamp(10.5px, 3vw, 12.5px); }
  /* top:50% + translateY, not a fixed px top — the line has to stay
     centered on the dots regardless of which clamped size they land on. */
  .work-slider__dots::before { left: 18px; right: 18px; top: 50%; bottom: auto; width: auto; height: 2px; transform: translateY(-50%); }
  .work-slider__info { margin-top: 16px; }
  /* The desktop bump to 380px (Kh's call, twice) was never meant for a
     phone screen — full width there just pushes the image itself far
     down under the dots/title/category before you ever see it (Kh's
     screenshot). Spacer stays in sync at half this width, same as
     desktop's own comment explains. */
  .work-slider__card { width: 240px; }
  .work-slider__spacer { width: calc(50% - 120px); }
}

/* ---- Services: jigsaw-puzzle reveal ----
   Ported ~1:1 from a standalone artifact preview — see js/services.js for
   the piece geometry + reveal-then-assemble animation (replays every time
   this scrolls into view, same standing pattern as everywhere else on the
   site). Kh's brief was specific: literal interlocking jigsaw pieces (not
   just connector lines) "because they rely on each other" — the copy
   below leans into that same idea rather than a generic services blurb. */
.services { margin: 0 auto; }
.services__puzzle {
  position: relative;
  max-width: 900px;
  margin: 56px auto 0;
  aspect-ratio: 880 / 440;
}
.services__svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.services__piece {
  fill: var(--white);
  stroke: var(--navy-150);
  stroke-width: 2;
  transition: opacity 0.5s var(--ease-quart-out);
}
.services__piece--tint { fill: var(--navy-050); }

.services__grid {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(2, 1fr);
  pointer-events: none;
}
.services__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  opacity: 0;
  transform: scale(0.6);
  transition: opacity 0.5s var(--ease-quart-out), transform 0.5s var(--ease-quart-out);
}
.services__item.is-in { opacity: 1; transform: scale(1); }
.services__icon { flex-shrink: 0; width: 34px; height: 34px; color: var(--navy-700); }
.services__icon svg { width: 100%; height: 100%; }
.services__label { font-size: 14px; font-weight: 600; color: var(--navy); text-align: center; }

/* Mobile: 2 cols x 4 rows instead of 4 x 2 — js/services.js switches its
   own grid math (R/C, the tab sign tables, the viewBox) to match at this
   exact breakpoint, so this and that stay in sync. 4 columns squeezed
   into a phone width would leave each piece's icon+label too small to
   read; reflowing to 2 columns keeps each piece a readable size instead
   of just shrinking the 4x2 layout down. */
@media (max-width: 640px) {
  /* Widened again (Kh's call, twice now) to close the left/right gutter —
     each step trims a bit more of the "fits with zero scroll on every
     phone" guarantee from the original mobile pass. At this width even a
     standard ~390x844 phone needs a short scroll to reach the last row
     (trimmed the spacing above as far as reasonably possible to keep that
     scroll small); a small/older screen (iPhone SE's 667px) needs more.
     #services only, not .section generally — every other section keeps
     the shared 96px top padding. */
  #services { padding-top: 36px; }
  .services__head { margin-bottom: 12px; }
  /* container-type turns the puzzle box itself into a sizing reference —
     icon/label below are set in cqw (% of THIS box's own rendered width),
     not vw (% of viewport). They're different once the box hits its
     max-width cap: a narrow phone (~320px) shrinks this box below 300px
     and a wide one holds it at exactly 300px, so vw-based text would
     mismatch the box at one end or the other (Kh's call — "responsive...
     doesn't look ugly"); cqw tracks the box itself at every width. */
  .services__puzzle { aspect-ratio: 440 / 880; max-width: 300px; margin-top: 8px; container-type: inline-size; }
  .services__grid { grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(4, 1fr); }
  .services__icon { width: clamp(20px, 9cqw, 28px); height: clamp(20px, 9cqw, 28px); }
  .services__label { font-size: clamp(10px, 4.2cqw, 13px); }
  .services__item { gap: clamp(4px, 2.3cqw, 7px); }
}

/* ---- Team: photo-beside-intro row, on an oblique dark section ----
   Rebuilt a second time (Kh sent a fuller, more specific brief this
   round): members sit beside each other — photo, then name/role/intro
   right next to it — directly on the section's own dark background, no
   card box behind either (Kh's call: "the text is written in the
   background of the section, not in another background"). If all 7 don't
   fit the viewport the row scrolls/swipes, same overflow-x + scroll-snap
   + mask-fade mechanism as the header's mobile links strip. See
   js/team.js for the render logic and how a `photo` key (still pending —
   Kh's sending the 7 real photos) replaces the gradient+icon placeholder
   below with a real one automatically. */
/* --team-skew is the "30 degree" slanted top/bottom edge Kh asked for —
   a literal 30deg-from-horizontal cut across a full 1440px-wide section
   would need an ~830px vertical rise (tan(30deg) x width), which would
   force absurd amounts of padding just to keep the text out of the
   clipped corners. This clamps the rise to a tasteful diagonal instead
   (40-90px depending on viewport) that reads as "an oblique section," not
   a literal 30deg angle at every width — flagged here since it's a
   deliberate deviation from the literal ask, not an oversight. Padding is
   bumped by the same amount on top/bottom so real content never sits in
   the clipped-off corners. */
#team {
  --team-skew: clamp(40px, 6vw, 90px);
  position: relative;
  /* Promotes this to its own GPU layer up front — without it, a fast
     scroll can occasionally show clip-path visibly "catching up" a frame
     or two behind the section's own scroll position on some browser/GPU
     combos, reading as if the diagonal edge itself were animating in. */
  will-change: clip-path;
  clip-path: polygon(0 var(--team-skew), 100% 0, 100% calc(100% - var(--team-skew)), 0 100%);
  padding-top: calc(96px + var(--team-skew));
  padding-bottom: calc(96px + var(--team-skew));
}
@media (min-width: 768px) {
  #team { padding-top: calc(140px + var(--team-skew)); padding-bottom: calc(140px + var(--team-skew)); }
}

.team-row {
  display: flex;
  gap: 56px;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding: 8px 8px 28px;
  mask-image: linear-gradient(90deg, transparent 0, #000 40px, #000 calc(100% - 40px), transparent 100%);
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 40px, #000 calc(100% - 40px), transparent 100%);
}
.team-row::-webkit-scrollbar { display: none; }

.team-row__item {
  flex: none;
  display: flex;
  align-items: center;
  gap: 22px;
  max-width: 340px;
  scroll-snap-align: start;
  /* Per-item scroll-in reveal, staggered left-to-right via --team-delay
     (js/team.js) — replays every time the row scrolls back into view,
     same standing pattern as Why Us/Services/the process timeline. Slides
     in from the right, matching the row's own swipe direction, with a
     bigger scale dip for a clear "settle into place" feel (Kh's call —
     the first pass read as too subtle to notice). */
  opacity: 0;
  transform: translateX(100px) scale(0.75);
  transition: opacity 0.55s var(--ease-quart-out), transform 0.55s var(--ease-quart-out);
  transition-delay: var(--team-delay, 0ms);
}
.team-row__item.is-in { opacity: 1; transform: translateX(0) scale(1); }
/* Hover — photo + text zoom together as one unit (Kh's call), only once
   the entrance reveal has actually settled (.is-in), and with its own
   fast transition/no delay so it responds instantly regardless of which
   position in the stagger this item was — otherwise it would inherit the
   entrance's 0.55s duration and --team-delay lag, reading as sluggish. */
.team-row__item.is-in:hover {
  transform: translateX(0) scale(1.06);
  transition: transform 0.3s var(--ease-quart-out);
  transition-delay: 0s;
}
/* Every member is one role icon (Kh's call — photos dropped for a single
   consistent, professional icon set; icons in js/team.js), sitting
   straight on the section's dark background with no tile/box behind it
   (Kh's call — same "on the background, not in another background" idea
   as the name/role/desc text beside it). The fixed-width box is just to
   keep every member's text starting at the same x. */
.team-row__photo {
  flex-shrink: 0;
  width: 88px;
  height: 88px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.team-row__icon svg { width: 76px; height: 76px; color: var(--offwhite); }

.team-row__name { font-size: 16px; font-weight: 700; color: var(--offwhite); }
.team-row__role { margin-top: 2px; font-size: 12.5px; color: var(--offwhite-60); }
.team-row__intro { margin-top: 8px; font-size: 13px; line-height: 1.5; color: var(--offwhite); }

@media (max-width: 640px) {
  .team-row { gap: 32px; padding: 8px 4px 24px; }
  .team-row__item { max-width: 82vw; }
  .team-row__photo { width: 68px; height: 68px; }
  .team-row__icon svg { width: 58px; height: 58px; }
}

/* ---- Contact: map + form panel, on an oblique dark section ----
   Rebuilt a third time (Kh's call): same oblique navy treatment as Team
   — --contact-skew below is the identical "tasteful diagonal, not a
   literal 30deg" call already made (and explained) for #team/#insights.
   Everything that used to assume a light section background (eyebrow,
   heading, fields, button, the direct-contact line) is recolored for
   navy here — see each rule's comment for why. Mobile drops the map
   entirely (Kh's call) rather than stacking it below the form. */
#contact {
  --contact-skew: clamp(40px, 6vw, 90px);
  position: relative;
  /* Same GPU-layer promotion as #team, and the same reasoning — see that
     comment. */
  will-change: clip-path;
  clip-path: polygon(0 var(--contact-skew), 100% 0, 100% calc(100% - var(--contact-skew)), 0 100%);
  padding-top: calc(96px + var(--contact-skew));
  padding-bottom: calc(96px + var(--contact-skew));
}
@media (min-width: 768px) {
  #contact { padding-top: calc(140px + var(--contact-skew)); padding-bottom: calc(140px + var(--contact-skew)); }
}

.contact {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 48px;
  max-width: 1080px;
  margin: 0 auto;
  align-items: stretch;
}

/* Real Google Maps embed (no API key — the plain iframe embed URL).
   align-items:stretch on .contact above makes this match the form
   panel's own natural height; min-height is a floor for when the form is
   short enough that the map would otherwise end up too shallow. */
.contact-map {
  min-height: 460px;
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.25);
}
.contact-map iframe { display: block; width: 100%; height: 100%; }

.contact-panel {
  display: flex;
  flex-direction: column;
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.7s var(--ease-quart-out), transform 0.7s var(--ease-quart-out);
}
.contact-panel.is-in { opacity: 1; transform: translateY(0); }

/* navy-600a -> offwhite-45, same swap as .bg-navy .section-head__eyebrow
   elsewhere — the navy-tinted version is built for a light background
   and is nearly invisible on this section's own navy now. */
.contact-panel__eyebrow { display: flex; align-items: center; gap: 12px; font-size: 12px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--offwhite-45); }
.contact-panel__eyebrow-icon { position: relative; width: 18px; height: 10px; flex-shrink: 0; }
.contact-panel__eyebrow-icon::before,
.contact-panel__eyebrow-icon::after { content: ""; position: absolute; left: 0; height: 2px; border-radius: 1px; background: var(--teal); }
.contact-panel__eyebrow-icon::before { top: 0; width: 18px; }
.contact-panel__eyebrow-icon::after { top: 8px; width: 12px; }
.contact-panel__heading { margin-top: 8px; font-size: clamp(30px, 3.6vw, 40px); font-weight: 800; color: var(--offwhite); letter-spacing: -0.01em; }
.contact-panel__heading b { color: var(--teal); font-weight: 800; }

.contact-form { display: flex; flex-direction: column; gap: 16px; margin-top: 28px; }
.contact-form__row { display: flex; gap: 16px; }
.contact-form__row .contact-form__field-filled { flex: 1 1 0; min-width: 0; }

/* Shaded fields — light filled box, no visible border (Kh's reference).
   offwhite-10, not navy-050 — that tint was sized for contrast against a
   white/paper field on a light section; against this section's own navy
   it would be nearly invisible instead of reading as "a field". */
.contact-form__field-filled input,
.contact-form__field-filled textarea {
  width: 100%;
  font: inherit;
  font-size: 14.5px;
  padding: 14px 16px;
  border: none;
  border-radius: 10px;
  background: var(--offwhite-10);
  color: var(--offwhite);
  resize: vertical;
  transition: background 0.2s;
}
.contact-form__field-filled input::placeholder,
.contact-form__field-filled textarea::placeholder { color: var(--offwhite-45); }
.contact-form__field-filled input:focus,
.contact-form__field-filled textarea:focus { outline: none; background: var(--offwhite-25); }

/* Inverted from the light-section version (light fill/navy text, not
   navy fill/offwhite text) — a navy button on this section's own navy
   background would have no visible shape, same issue already solved for
   the hero's mobile CTA earlier. border is always there (transparent by
   default) so the hover border-color swap doesn't shift the button's size. */
.contact-form__submit-full {
  width: 100%;
  margin-top: 4px;
  padding: 16px;
  border: 1.5px solid transparent;
  border-radius: 10px;
  background: var(--offwhite);
  color: var(--navy);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.contact-form__submit-full:hover { background: transparent; border-color: var(--offwhite); color: var(--offwhite); }
.contact-form__submit-full:disabled { opacity: 0.6; cursor: default; }

.contact-form__status { min-height: 20px; font-size: 13px; }
.contact-form__status.is-success { color: var(--steel); font-weight: 600; }
.contact-form__status.is-error { color: #ff8a75; font-weight: 600; }

/* Compact real-contact line under the form — see the matching HTML
   comment for why this exists even though the reference doesn't have it. */
.contact-panel__direct { margin-top: 20px; font-size: 12.5px; color: var(--offwhite-45); }
.contact-panel__direct a { color: var(--offwhite-60); font-weight: 600; }
.contact-panel__direct a:hover { color: var(--steel); }

@media (max-width: 760px) {
  .contact { grid-template-columns: 1fr; gap: 32px; }
  /* Map dropped entirely on mobile (Kh's call) — just the form. */
  .contact-map { display: none; }
  .contact-form__row { flex-direction: column; gap: 16px; }
}

/* ---- Book an Appointment (book-appointment.html) ----
   A custom month calendar + time-slot picker, no library — same
   plain-JS/CSS approach as every other interactive piece on this site.
   Availability itself (WM_BOOKING) is a placeholder default for now; see
   js/booking-data.js and js/booking.js's file-header comment for the
   Firestore data model + Security Rules this needs.

   Structure (Kh's latest call, corrected from an earlier whole-page-navy
   pass): #booking-intro is a plain rectangle section (no oblique), same
   as Why Us/Services/About's own section-heads. Below it the four
   numbered steps each sit on the normal white page as their OWN
   full-bleed <section> (#booking-step-1..4, styled via the shared
   .booking-step-section class) so the oblique card is genuinely 100%
   viewport width at its edges — the same clip-path-on-the-<section>
   pattern as #team/#contact on index.html, not a div capped by
   .container's own max-width. Steps alternate light/navy: 1 and 3 keep
   the light background a calendar day-grid / readable form fields want;
   2 (Your Information) and 4 (Your Budget) are navy, matching the hero. */
/* This page's own "hero" (Kh's word for it) — flat top (it's the very
   first thing on the page, nothing above it to blend into) but an
   oblique "base": the bottom edge is cut on the same tasteful diagonal
   as every other oblique section here, so the navy visually flows/
   blends into the white page below instead of ending in a hard
   straight line. Same clamp() as .booking-step-section below — kept as
   its own variable rather than sharing --step-skew since this section
   only clips ONE edge (needs only half the extra padding a two-edge
   clip needs). */
/* Background (Kh's call: evolved several times — a stock photo with real
   app logos, a static "digital network" photo, a 3D-glassmorphism white
   base with floating glossy spheres — settled back on navy with the
   animated dot-network directly on it, no glass panel around the
   heading this time). js/hero-network.js (shared with index.html's
   #team/#contact and admin.html) draws the canvas below. */
#booking-intro {
  position: relative;
  overflow: hidden;
  will-change: clip-path;
  --intro-skew: clamp(40px, 6vw, 90px);
  clip-path: polygon(0 0, 100% 0, 100% calc(100% - var(--intro-skew)), 0 100%);
  padding-bottom: calc(96px + var(--intro-skew));
}
@media (min-width: 768px) {
  #booking-intro { padding-bottom: calc(140px + var(--intro-skew)); }
}

/* Animated dot-network (Kh's call — back from a detour through floating
   glossy spheres). js/hero-network.js draws it; this just gives the
   canvas the box to fill — on book-appointment.html's #booking-intro
   hero and, with data-network-theme="dark", index.html's navy #team +
   #contact. Content needs to sit above it (both are children of the same
   position:relative section) — z-index makes that explicit rather than
   relying on source order alone; pointer-events:none keeps the mesh from
   ever eating clicks meant for the form/links on top of it. */
.hero-network-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; z-index: 0; pointer-events: none; }
#booking-intro .container,
#booking-step-2 .container,
#booking-step-4 .container,
#team .container,
#contact .container { position: relative; z-index: 1; }

/* The rest of the page (outside the header and the navy steps) reads
   off-white against these white step cards — Kh's call: this page's own
   background should be plain white, not paper/off-white. The footer goes
   white here too now (Kh's call — was paper). Both are scoped to this
   page via body.page-booking rather than touching the shared --paper
   token, which every bg-paper section on index.html and the footer on
   index.html still rely on. */
body.page-booking { background: var(--white); }
body.page-booking .footer { background: var(--white); }

/* Steps 2 & 4 — same navy as the hero (Kh's call). Overrides .booking-
   step-section's own white background (ID beats class) and re-inverts
   every child that assumed a light section — heading text, the numbered
   badge, the "you selected..." summary box, the eyebrow/optional labels,
   the input fields, and (step 4 only) the Confirm button — same navy-
   section treatment already established for Team/Contact, or this
   recreates the "same color as its own background" bug in several places
   at once. */
#booking-step-2,
#booking-step-4 { background: var(--navy); color: var(--offwhite); }
#booking-step-2 .booking-step__num,
#booking-step-4 .booking-step__num { background: var(--offwhite); color: var(--navy); }
#booking-step-2 .booking-step__heading h2,
#booking-step-4 .booking-step__heading h2 { color: var(--offwhite); }
#booking-step-4 .booking-step__eyebrow { color: var(--offwhite-45); }
#booking-step-4 .booking-step__optional { color: var(--offwhite-60); }
#booking-step-2 .booking-form__selected { background: var(--offwhite-10); color: var(--offwhite-60); }
#booking-step-2 .booking-form__selected strong { color: var(--offwhite); }
#booking-step-2 .booking-form__field input,
#booking-step-4 .booking-form__field input { background: var(--offwhite-10); color: var(--offwhite); }
#booking-step-2 .booking-form__field input::placeholder,
#booking-step-4 .booking-form__field input::placeholder { color: var(--offwhite-45); }
#booking-step-2 .booking-form__field input:focus,
#booking-step-4 .booking-form__field input:focus { background: var(--offwhite-25); }
/* Confirm button sits on navy now — invert it (offwhite fill / navy text,
   teal on hover) instead of the .btn--navy default that vanishes here. */
#booking-step-4 .booking-form__submit { background: var(--offwhite); color: var(--navy); }
#booking-step-4 .booking-form__submit:hover:not(:disabled) { background: var(--teal); color: var(--navy); }
#booking-step-4 .booking-form__status.is-error { color: #ff8a80; }

.booking-step-section {
  position: relative;
  background: var(--white);
  /* Same "tasteful diagonal, not a literal angle" clamp already used (and
     explained) for #team/#contact — now that this is a real full-width
     section again (not a ~720px card), it goes back to their vw-relative
     sizing instead of the cqw version that only made sense at card scale. */
  --step-skew: clamp(40px, 6vw, 90px);
  clip-path: polygon(0 var(--step-skew), 100% 0, 100% calc(100% - var(--step-skew)), 0 100%);
  padding-block: calc(56px + var(--step-skew));
  margin-block: 40px;
}
/* Alternating direction (Kh's call: "one oblique from right and the
   other from left") — the mirror image of the base polygon above,
   applied to steps 1 and 3 while steps 2 and 4 keep the original
   direction, so the diagonal zig-zags hero -> step1 -> step2 -> step3
   -> step4 down the page instead of every section leaning the same way. */
.booking-step-section--mirror {
  clip-path: polygon(100% var(--step-skew), 0 0, 0 calc(100% - var(--step-skew)), 100% 100%);
}
/* :first-of-type/:last-of-type won't do here — #booking-intro is also a
   <section>, so it (not #booking-step-1) is the actual first-of-type
   among these siblings. Targeting the two end steps by id instead. */
#booking-step-1 { margin-top: 0; }
#booking-step-4 { margin-bottom: 0; }
/* Re-centers the actual calendar/form/questions to a readable width
   inside the now-page-wide section, so the background spans edge to
   edge while a stretched-to-1680px calendar grid or checkbox row
   doesn't. */
.booking-step__body { max-width: 640px; margin: 0 auto; }
/* Same 640px/centered treatment as .booking-step__body — otherwise the
   heading (flush to the section's own edge) and the content (centered
   within it) would start at two different horizontal positions instead
   of reading as one aligned column. */
.booking-step__heading { display: flex; align-items: center; gap: 14px; max-width: 640px; margin: 0 auto 24px; }
.booking-step__num {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--navy);
  color: var(--offwhite);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 700;
}
.booking-step__heading h2 { font-size: 19px; font-weight: 700; color: var(--navy); }
/* "WEMADE — Before We Meet" — Kh's own label for the questionnaire step,
   sits right under that step's numbered heading. */
.booking-step__eyebrow { margin: -12px 0 24px 46px; font-size: 11px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--navy-600a); }
/* Lowercase/not-bold/no letter-spacing on purpose — sits right next to
   the all-caps eyebrow above and needs to read as a quiet aside ("you
   don't have to answer these"), not another heading fighting for
   attention. */
.booking-step__optional { text-transform: none; font-weight: 500; letter-spacing: normal; color: var(--navy-450); }

/* Scroll-in reveal for steps 1 & 2 (Kh's call) — same .is-in toggle
   pattern as Why Us/Team/Contact/the Hero (js/booking.js's
   initStepReveal()). The body's extra transition-delay makes the
   heading settle a beat before its content, a small cascade rather than
   both popping in at once. */
.booking-reveal { opacity: 0; transform: translateY(28px); transition: opacity 0.6s var(--ease-quart-out), transform 0.6s var(--ease-quart-out); }
.booking-reveal.is-in { opacity: 1; transform: translateY(0); }
.booking-step__body.booking-reveal { transition-delay: 0.1s; }

/* Each question its own card (Kh's call: no alternating colors — the
   earlier navy/white split got flattened to one plain treatment). */
/* White fill + a clearly blue border (Kh's call) — navy-200 matches the
   same border weight/color already used on this page's checkbox pills,
   slots, and nav buttons, so the question boxes read as part of the
   same family instead of introducing a new blue. */
.booking-question { padding: 20px 24px; border-radius: 14px; margin-bottom: 16px; background: var(--white); border: 1.5px solid var(--navy-200); }
.booking-question:last-child { margin-bottom: 0; }
.booking-question__title { font-size: 15px; font-weight: 700; color: var(--navy); }
.booking-question__hint { margin-top: 4px; font-size: 13px; color: var(--navy-600a); line-height: 1.5; }
.booking-question .booking-form__field { margin-top: 12px; }

/* ---- Step 3's quiz slider — one question at a time ----
   Same dot-nav idea as #work-slider on index.html, but a real horizontal
   slide (track translateX, one question at 100% width each) instead of
   that slider's dim/scale-fade treatment — these are form fields, not
   photos, so the inactive ones need to be fully OUT of view, not just
   dimmed, or a visitor could end up typing into a half-transparent field
   without noticing it's not the current question. */
.booking-quiz { margin-top: 8px; }
.booking-quiz__dots { display: flex; justify-content: center; gap: 10px; margin-bottom: 20px; }
.booking-quiz__dot {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 1.5px solid var(--navy-200);
  background: none;
  font: inherit;
  font-size: 12px;
  font-weight: 700;
  color: var(--navy-450);
  cursor: pointer;
  transition: background 0.2s, border-color 0.2s, color 0.2s;
}
.booking-quiz__dot:hover { border-color: var(--teal); }
/* Answered-but-not-current: a filled ring rather than the solid navy
   reserved for "this is the one you're on" — so glancing at the dots
   tells you both where you are AND which ones you've already filled in. */
.booking-quiz__dot.is-answered { border-color: var(--navy); color: var(--navy); }
.booking-quiz__dot.is-active { background: var(--navy); border-color: var(--navy); color: var(--offwhite); }

.booking-quiz__viewport { overflow: hidden; transition: height 0.35s var(--ease-quart-out); }
.booking-quiz__track { display: flex; transition: transform 0.45s var(--ease-quart-out); }
.booking-quiz__track .booking-question { flex: 0 0 100%; min-width: 0; margin-bottom: 0; }

.booking-quiz__nav { display: flex; align-items: center; justify-content: space-between; margin-top: 16px; }
.booking-quiz__back,
.booking-quiz__skip {
  border: none;
  background: none;
  font: inherit;
  font-size: 13.5px;
  font-weight: 700;
  color: var(--navy-600a);
  cursor: pointer;
  padding: 8px 4px;
  transition: color 0.2s;
}
.booking-quiz__back:hover:not(:disabled),
.booking-quiz__skip:hover { color: var(--navy); }
.booking-quiz__back:disabled { opacity: 0; pointer-events: none; }

/* Pill-style checkboxes — a real (visually hidden, still keyboard-
   focusable) checkbox input driving a styled <span>, not a custom-drawn
   fake control, so this stays a genuine, accessible checkbox group. */
.booking-checkbox-group { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
.booking-checkbox { position: relative; display: inline-flex; }
.booking-checkbox input { position: absolute; opacity: 0; width: 0; height: 0; }
.booking-checkbox span {
  display: inline-flex;
  align-items: center;
  padding: 10px 18px;
  border-radius: 999px;
  border: 1.5px solid var(--navy-200);
  font-size: 13px;
  font-weight: 600;
  color: var(--navy-600a);
  cursor: pointer;
  transition: background 0.2s, border-color 0.2s, color 0.2s;
}
.booking-checkbox:hover span { border-color: var(--teal); }
.booking-checkbox input:checked + span { background: var(--navy); border-color: var(--navy); color: var(--offwhite); }
.booking-checkbox input:focus-visible + span { outline: 2px solid var(--teal); outline-offset: 2px; }

/* ---- Step 4's budget picker (navy section) ----
   Single-choice card grid — same visually-hidden-radio-drives-a-styled-
   <span> pattern as the checkbox pills above, just inverted for the navy
   background (light borders/text, offwhite fill when picked) like every
   other control on #booking-step-2/4. "Other" reveals .booking-budget__
   custom for a free number (js/booking.js's initBookingBudget()). */
.booking-budget { margin-top: 4px; }
.booking-budget__options { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.booking-budget__option { position: relative; display: flex; }
.booking-budget__option input { position: absolute; opacity: 0; width: 0; height: 0; }
.booking-budget__option span {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 16px 18px;
  border-radius: 12px;
  border: 1.5px solid var(--offwhite-25);
  font-size: 14px;
  font-weight: 600;
  color: var(--offwhite-60);
  cursor: pointer;
  transition: background 0.2s, border-color 0.2s, color 0.2s;
}
.booking-budget__option:hover span { border-color: var(--teal); color: var(--offwhite); }
.booking-budget__option input:checked + span { background: var(--offwhite); border-color: var(--offwhite); color: var(--navy); }
.booking-budget__option input:focus-visible + span { outline: 2px solid var(--teal); outline-offset: 2px; }
.booking-budget__custom { margin-top: 12px; }

@media (max-width: 760px) {
  .booking-budget__options { grid-template-columns: 1fr; }
}

.booking__calendar-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.booking__nav-btn {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1.5px solid var(--navy-200);
  background: none;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
  line-height: 1;
  color: var(--navy);
  cursor: pointer;
  transition: background 0.2s, color 0.2s;
}
.booking__nav-btn:hover:not(:disabled) { background: var(--navy); color: var(--offwhite); }
.booking__nav-btn:disabled { opacity: 0.3; cursor: default; }
.booking__month-label { font-size: 15px; font-weight: 700; color: var(--navy); }

.booking__weekdays {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  text-align: center;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--navy-450);
  margin-bottom: 8px;
}
.booking__days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.booking__day {
  aspect-ratio: 1;
  border: none;
  border-radius: 10px;
  background: none;
  font: inherit;
  font-size: 13.5px;
  color: var(--navy);
  cursor: pointer;
  transition: background 0.2s, color 0.2s;
}
.booking__day:disabled { color: var(--navy-200); cursor: default; }
.booking__day:hover:not(:disabled):not(.is-selected) { background: var(--navy-050); }
.booking__day.is-selected { background: var(--navy); color: var(--offwhite); font-weight: 700; }
.booking__day--empty { visibility: hidden; cursor: default; }

.booking__slots { margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--navy-150); }
.booking__slots-hint { font-size: 13px; color: var(--navy-450); }
.booking__slots-note { font-size: 11.5px; color: var(--navy-450); margin-bottom: 12px; }
.booking__slots-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.booking__slot {
  padding: 10px 6px;
  border-radius: 8px;
  border: 1.5px solid var(--navy-200);
  background: var(--white);
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--navy);
  cursor: pointer;
  transition: border-color 0.2s, background 0.2s, color 0.2s;
}
.booking__slot:hover:not(:disabled):not(.is-selected) { border-color: var(--teal); }
.booking__slot.is-selected { background: var(--navy); border-color: var(--navy); color: var(--offwhite); }
.booking__slot:disabled { opacity: 0.35; cursor: default; text-decoration: line-through; }

.booking-form__selected { padding: 14px 16px; border-radius: 10px; background: var(--navy-050); font-size: 13.5px; color: var(--navy-600a); min-height: 20px; margin-bottom: 20px; }
.booking-form__selected strong { color: var(--navy); }
/* margin-bottom, not just the row's own internal gap — the two rows
   (name/email, phone/company) had zero space between THEM, reading as
   one solid stuck-together block (Kh's call: "too close"). */
.booking-form__row { display: flex; gap: 16px; margin-bottom: 16px; }
.booking-form__row:last-of-type { margin-bottom: 0; }
.booking-form__row .booking-form__field { flex: 1 1 0; min-width: 0; }

.booking-form__field input,
.booking-form__field textarea {
  width: 100%;
  font: inherit;
  font-size: 14.5px;
  padding: 14px 16px;
  border: none;
  border-radius: 10px;
  background: var(--navy-050);
  color: var(--navy);
  resize: vertical;
  transition: background 0.2s;
}
.booking-form__field input::placeholder,
.booking-form__field textarea::placeholder { color: var(--navy-450); }
.booking-form__field input:focus,
.booking-form__field textarea:focus { outline: none; background: var(--navy-100); }

/* The Confirm button now closes step 4 (Your Budget), which is navy —
   so its .btn--navy fill is inverted to offwhite/navy up in the
   #booking-step-4 block above, not here. This just handles spacing. */
.booking-form__submit-wrap { margin-top: 24px; }
.booking-form__submit { width: 100%; cursor: pointer; }
.booking-form__submit:disabled { opacity: 0.5; cursor: not-allowed; }

.booking-form__status { min-height: 20px; font-size: 13px; }
.booking-form__status.is-success { color: var(--teal); font-weight: 600; }
.booking-form__status.is-error { color: #c0392b; font-weight: 600; }

@media (max-width: 760px) {
  #booking-intro { padding-bottom: calc(56px + var(--intro-skew)); }
  .booking-step-section { margin-block: 28px; padding-block: calc(28px + var(--step-skew)); }
  #booking-step-1 { margin-top: 0; }
  #booking-step-4 { margin-bottom: 0; }
  .booking-step__eyebrow { margin-left: 0; }
  .booking-form__row { flex-direction: column; gap: 16px; }
  .booking__slots-grid { grid-template-columns: repeat(2, 1fr); }
}
