/* Reusable components: nav, mosaic rows, marquee, footer.
   Shared across every page — page-specific layout lives in css/home.css
   (and future css/<page>.css files). */

/* ---------- Preloader ----------
   Full-screen intro shown once per page load, before the site underneath is
   visible — pure CSS keyframes, deliberately with no JS/GSAP dependency
   (see index.html comment). Sequence: each letter types in via a clip-path
   wipe (a blinking cursor rides alongside), the accent line draws under the
   wordmark (echoes .eyebrow::before's brand motif), a brief hold, then the
   whole mark scales up and fades — a "zoom past the logo" transition — in
   sync with the overlay itself fading and disabling (visibility +
   pointer-events, so it can't trap focus or block clicks once done).
   Reduced-motion overrides for the delays live in base.css. */

.preloader {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--color-blue-900);
  animation: preloader-out 0.7s ease-in-out 2.3s forwards;
}

.preloader__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  animation: preloader-zoom 0.7s cubic-bezier(0.55, 0, 1, 0.45) 2.3s forwards;
}

.preloader__mark {
  display: flex;
  align-items: baseline;
  letter-spacing: 0.02em;
}

.preloader__letter {
  display: inline-block;
  /* Deliberately hardcoded, not var(--font-display) (Forum, everywhere
     else on the page) — the intro keeps its original Poppins Bold on
     request, decoupled from the rest of the site's typeface. */
  font-family: "Poppins", "Arial Narrow", sans-serif;
  font-weight: 700;
  font-size: clamp(4rem, 15vw, 10rem);
  line-height: 1;
  color: var(--color-white);
  clip-path: inset(0 100% 0 0);
  animation: preloader-type 0.5s steps(6) forwards;
}

.preloader__letter:nth-child(1) {
  animation-delay: 0.1s;
}

.preloader__letter:nth-child(2) {
  animation-delay: 0.32s;
}

.preloader__letter:nth-child(3) {
  animation-delay: 0.54s;
}

.preloader__letter:nth-child(4) {
  animation-delay: 0.76s;
}

.preloader__cursor {
  display: inline-block;
  align-self: stretch;
  width: 0.06em;
  margin-inline-start: 0.05em;
  background: var(--color-accent);
  opacity: 0;
  animation: preloader-cursor-in 0.1s linear 0.1s forwards, preloader-blink 0.8s steps(1) 0.1s infinite;
}

.preloader__line {
  width: 64px;
  height: 2px;
  background: var(--color-accent);
  transform: scaleX(0);
  animation: preloader-line 0.5s ease-out 1.3s forwards;
}

@keyframes preloader-type {
  to {
    clip-path: inset(0 0% 0 0);
  }
}

@keyframes preloader-cursor-in {
  to {
    opacity: 1;
  }
}

@keyframes preloader-blink {
  50% {
    opacity: 0;
  }
}

@keyframes preloader-line {
  to {
    transform: scaleX(1);
  }
}

@keyframes preloader-zoom {
  to {
    transform: scale(9);
    opacity: 0;
  }
}

@keyframes preloader-out {
  from {
    opacity: 1;
  }

  to {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
  }
}

/* Set on <html> by index.html's inline <head> script only when this load
   was routed here by an in-site link click (kiplNavigating), not a fresh
   open — skips the preloader outright for that one case, so clicking back
   to Home from elsewhere on the site doesn't replay the intro. Every
   other way of reaching this page (new tab, typed URL, external link,
   bookmark, refresh) still gets it, every time. */
html.skip-preloader .preloader {
  display: none;
}

/* ---------- Page transition ----------
   A quick full-viewport fade-to-brand-navy-and-back for in-site
   navigation, shared by every page (markup is identical everywhere — see
   index.html/services.html). Third design for this component: a sliding
   curtain drew "too big and noticeable" feedback, the top-edge progress
   bar that replaced it swung too far the other way ("looks almost gone")
   — this splits the difference. It's brief (well under half a second
   total) and doesn't slide or move, just a plain opacity fade, but it's
   full-viewport so it still visibly registers rather than being an easy-
   to-miss detail in one corner. Solid color (not translucent) so the
   inevitable blank frame between two separate document loads on a static
   site reads as an intentional brand-colored flash rather than an
   accidental flash of white.

   Default state is opacity: 0 (invisible) so a page that loads with no
   JS, or before JS runs, is never blocked by it — same "never trust JS
   alone to unblock a full-page overlay" reasoning as the preloader above.

   Exit (js/main.js#initPageTransitions intercepts a same-site link click):
   .page-transition--cover fades it in via a plain CSS transition; JS
   waits out a matching setTimeout (not a transitionend/animationend
   listener — those can fail to fire) and only then navigates.

   Entry: the arriving page's inline <head> script (synchronous, before
   first paint) adds html.nav-transition-cover if it detects it just got
   here via that same click — which instantly paints it already fully
   opaque (no transition, no flash of the new page underneath before the
   fade starts), then the rule below runs a pure @keyframes animation, with
   no JS involvement at all, fading it back out. Because it's a
   self-running CSS animation (like the preloader) rather than something
   JS has to remove, it's guaranteed to clear on its own even if a later
   script fails. */

.page-transition {
  position: fixed;
  inset: 0;
  z-index: 1100;
  background: var(--color-blue-900);
  opacity: 0;
  pointer-events: none;
}

.page-transition--cover {
  opacity: 1;
  pointer-events: auto;
  transition: opacity 0.16s ease-out;
}

html.nav-transition-cover .page-transition {
  animation: page-transition-reveal 0.3s ease-in both;
}

@keyframes page-transition-reveal {
  from {
    opacity: 1;
  }

  to {
    opacity: 0;
  }
}

/* ---------- Nav ---------- */

.nav {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 100;
  height: var(--nav-height);
  display: flex;
  align-items: center;
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background-color var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-out),
    backdrop-filter var(--dur-base) var(--ease-out);
}

.nav.is-scrolled {
  background: rgba(250, 250, 250, 0.78);
  border-bottom-color: var(--color-line);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
}

.nav.is-scrolled .nav__link,
.nav.is-scrolled .nav__logo-text {
  color: var(--color-ink);
}

.nav__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}

.nav__logo {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.15rem;
  color: var(--color-white);
  transition: color var(--dur-base) var(--ease-out);
}

.nav__logo-mark {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  object-fit: cover;
}

.nav__links {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
}

.nav__link {
  font-size: var(--fs-small);
  font-weight: 500;
  color: var(--color-white);
  position: relative;
  padding-block: var(--space-3xs);
  transition: color var(--dur-fast) var(--ease-out);
}

.nav__link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 100%;
  bottom: -2px;
  height: 2px;
  background: var(--color-accent);
  transition: right var(--dur-base) var(--ease-out);
}

.nav__link:hover::after,
.nav__link.is-active::after {
  right: 0;
}

/* ---------- Nav dropdown (About) ----------
   The "About" label stays a plain link to about.html (top of page); a
   separate caret button toggles a flyout to the page's three sections
   (Mission & Vision / MD's Desk / Our Team) so a visitor can jump straight
   to one from anywhere on the site, not just from within the About page
   itself. Kept as two elements (<a> + <button>), not one, since an <a>
   can't validly contain a <button>.

   Opens on :hover for real pointer devices (gated behind (hover: hover) so
   touchscreens don't get stuck-hover glitches) OR via .is-open, which
   js/main.js#initNavDropdowns toggles on click/tap — the same .is-open
   class drives both the desktop flyout and the mobile accordion below,
   just interpreted by different rules per breakpoint.

   .nav__dropdown sits flush against the row (top: 100%, no gap) on
   purpose — an earlier version had a 12px gap for visual breathing room,
   but that gap is dead space belonging to neither element, so moving the
   mouse from "About" down into the flyout crossed empty space, broke the
   :hover chain, and closed the menu before a click could land on a link.
   Padding-top on the panel itself gives the same breathing room without
   that dead zone, since padding is still inside the hoverable box. */

.nav__item--dropdown {
  position: relative;
}

/* position: relative anchors the caret button below, which is taken out
   of flex flow entirely (position: absolute) rather than sized inline —
   .nav__links' flex `gap` measures between each item's own layout box, so
   if the caret contributed real width here, .nav__item--dropdown would be
   wider than a plain .nav__link and the gap before "Contact" would end up
   visibly bigger than every other gap in the nav (this is also why a
   naive negative margin-inline-end doesn't fix it — margin shifts the
   *gap* itself, not the box width, so it just shrinks the visual gap
   instead of removing the extra width that caused the problem). With the
   caret out of flow, this item's flex-layout width is just the "About"
   link's own width, same as any other .nav__link. */
.nav__item-row {
  position: relative;
  display: flex;
  align-items: center;
}

.nav__dropdown-toggle {
  position: absolute;
  left: 100%;
  top: 50%;
  transform: translateY(-50%);
  margin-inline-start: var(--space-3xs);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  color: inherit;
  opacity: 0.75;
  transition: opacity var(--dur-fast) var(--ease-out);
}

.nav__item--dropdown:hover .nav__dropdown-toggle,
.nav__item--dropdown.is-open .nav__dropdown-toggle {
  opacity: 1;
}

.nav__caret {
  transition: transform var(--dur-fast) var(--ease-out);
}

.nav__item--dropdown.is-open .nav__caret {
  transform: rotate(180deg);
}

/* Dark + translucent, matching .hero__card rather than a plain white
   card — reads as part of the site's own brand chrome instead of a
   generic browser-default dropdown, and stays legible over both the photo
   hero and the blurred/light nav.is-scrolled state. Motion and shadow are
   both deliberately understated (a few px of movement, a soft near-field
   shadow, no heavy drop shadow) — present enough to register as
   intentional, not so much that opening it competes for attention. */
.nav__dropdown {
  position: absolute;
  top: 100%;
  left: 50%;
  min-width: 190px;
  padding: var(--space-sm) var(--space-2xs) var(--space-2xs);
  border-radius: var(--radius-md);
  background: rgba(20, 33, 61, 0.92);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 10px 24px -12px rgba(20, 33, 61, 0.4);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translate(-50%, -3px);
  transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out), visibility var(--dur-fast) var(--ease-out);
}

/* The actual link list now lives one level in, inside .nav__dropdown-inner
   (added so the mobile accordion below has a single element to clip/size
   — see that rule's comment) — so THIS is what needs to lay the links out
   in a column; .nav__dropdown itself just wraps that one child now. Was
   briefly missing after the wrapper was introduced, which left the 3
   links as default inline content wrapping mid-phrase inside the
   dropdown's fixed min-width, e.g. "Our" / "Team" breaking onto two
   lines. */
.nav__dropdown-inner {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.nav__dropdown-link {
  padding: var(--space-2xs) var(--space-sm);
  border-radius: var(--radius-sm);
  font-size: var(--fs-small);
  font-weight: 500;
  color: rgba(255, 255, 255, 0.88);
  transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}

.nav__dropdown-link:hover {
  background: rgba(255, 255, 255, 0.12);
  color: var(--color-white);
}

.nav__item--dropdown.is-open .nav__dropdown {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translate(-50%, 0);
}

@media (hover: hover) and (pointer: fine) {
  .nav__item--dropdown:hover .nav__caret {
    transform: rotate(180deg);
  }

  .nav__item--dropdown:hover .nav__dropdown {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transform: translate(-50%, 0);
  }
}

.nav__cta {
  flex-shrink: 0;
}

.nav__toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 42px;
  height: 42px;
  border-radius: 50%;
}

.nav__toggle span,
.nav__toggle::before,
.nav__toggle::after {
  content: "";
  display: block;
  width: 20px;
  height: 2px;
  margin-inline: auto;
  background: currentColor;
  transition: transform var(--dur-fast) var(--ease-out), opacity var(--dur-fast) var(--ease-out);
}

.nav.is-scrolled .nav__toggle {
  color: var(--color-ink);
}

.nav:not(.is-scrolled) .nav__toggle {
  color: var(--color-white);
}

/* ---------- Hero ----------
   Full-bleed photo slideshow with a dark translucent card overlaid on top.
   Originally Home-only; now shared with Services (same photos, same
   mosaic-cell dissolve via js/animations.js#initHeroSlideshow) so it lives
   here rather than in css/home.css — same reasoning as the CTA band below. */

.hero {
  position: relative;
}

/* Full-bleed slideshow stage: height is derived from the source photos' own
   ~16:9 aspect ratio rather than an arbitrary fixed height, per the actual
   hero photography in assets/images/hero/. Nav sits fixed on top, so the
   stage intentionally runs edge-to-edge behind it — no top padding here.
   background-color is just the pre-load/no-JS fallback tone. */
.hero__stage {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  min-height: 480px;
  overflow: hidden;
  background-color: var(--color-blue-900);
}

.hero__slide-base {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
}

/* Above the photo/mosaic-cell layers (mosaic-grid is z-index 2) so the fixed
   nav and hero card stay legible against any of the rotating photos. */
.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: linear-gradient(180deg,
      rgba(20, 33, 61, 0.65) 0%,
      rgba(20, 33, 61, 0.08) 22%,
      rgba(20, 33, 61, 0.08) 62%,
      rgba(20, 33, 61, 0.55) 100%);
  pointer-events: none;
}

.hero__stage-inner {
  position: relative;
  z-index: 4;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Reserve clearance under the fixed nav before centering the card in
     whatever space is left, so it can never sit under the nav bar. */
  padding-block: calc(var(--nav-height) + var(--space-md)) var(--space-lg);
}

.hero__card {
  max-width: 820px;
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  padding: clamp(2rem, 2vw + 1.5rem, 3.5rem);
  border-radius: var(--radius-lg);
  background: rgba(20, 33, 61, 0.82);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: var(--color-white);
  box-shadow: 0 32px 64px -24px rgba(20, 33, 61, 0.55);
}

.hero__card .eyebrow {
  color: var(--color-orange-100);
}

/* The site-wide --fs-h1 clamp (up to 84px) assumes a free-flowing column;
   this card sits inside a fixed-height stage (locked to the photos' aspect
   ratio), so an oversized heading here would just overflow instead of
   growing the section. Sized to comfortably wrap the headline to 2-3 lines
   at the card's own width. */
.hero__card h1 {
  font-size: clamp(2rem, 2vw + 1.4rem, 3.25rem);
}

.hero__lead {
  font-size: var(--fs-body-lg);
  color: rgba(255, 255, 255, 0.82);
  max-width: 46ch;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-2xs);
}

@media (max-width: 700px) {

  /* 16:9 gets too short on phones to hold the card comfortably — trade the
     exact photo aspect ratio for a fixed floor here; background-size:cover
     still crops the photo cleanly, just shows less of it top-to-bottom. */
  .hero__stage {
    aspect-ratio: auto;
    min-height: 620px;
  }

  .hero__card {
    max-width: none;
    padding: var(--space-lg);
  }
}

/* ---------- Mosaic rows (full-bleed) ----------
   Shared row component: a full-bleed list of panel/detail pairs, each
   panel driven by animations.js#initMosaicReveal ([data-mosaic-reveal]).
   Used by both the Core Values section (data-mosaic-fill, flat color) and
   the Services/domains section (generated texture, no fill) — genuinely
   the same visual pattern, so one component rather than two near-copies. */

.mosaic-row-list {
  border-top: 1px solid rgba(255, 255, 255, 0.14);
}

.mosaic-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  min-height: clamp(160px, 20vw, 220px);
  border-bottom: 1px solid rgba(255, 255, 255, 0.14);
}

.mosaic-row__panel {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding-inline: var(--content-pad);
  /* Fallback if JS/canvas can't run: a plain brand-toned gradient so it
     still reads as intentional, not an empty panel. */
  background: linear-gradient(155deg, var(--color-blue-500), var(--color-blue-900));
  background-size: cover;
  background-position: center;
}

/* ---------- Core Values photo panels ----------
   Real photography (assets/images/core-values/) for these four panels only
   — the Services/domains rows below keep the generated grain texture (no
   photos for that content yet) — so this lives as an addition on top of
   the shared rule above rather than changing it. The photo itself is set
   inline via background-image on the panel (same convention as About's
   team mosaic photos, e.g. .md-desk__portrait in about.html), which
   initMosaicReveal auto-detects and slices instead of generating a
   texture. Two overlay layers sit on top of the sliced photo, both
   z-index 3 (above .mosaic-grid's z-index 2) but before __index/__name in
   markup so text still paints above both:
     1. __duotone — mix-blend-mode: color, full brand-color coverage, so
        each photo reads as one duotone tone instead of its own raw stock
        color. Alternates blue/orange per row (--tint-orange modifier) so
        all four read as one branded set.
     2. __scrim — normal-blend fine diagonal grain (same texture as
        .cta-band__texture) plus a fade toward the detail column's own
        navy (--color-bg-dark), so the photo dissolves into it instead of
        a hard seam down the middle of the row. */

.mosaic-row__duotone,
.mosaic-row__scrim {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}

.mosaic-row__duotone {
  mix-blend-mode: color;
  background: var(--color-blue-700);
}

.mosaic-row__panel--tint-orange .mosaic-row__duotone {
  background: var(--color-orange-600);
}

.mosaic-row__scrim {
  background-image:
    repeating-linear-gradient(118deg, rgba(255, 255, 255, 0.07) 0 1px, transparent 1px 7px),
    linear-gradient(90deg, transparent 45%, var(--color-bg-dark) 102%);
}

.mosaic-row__index {
  position: relative;
  z-index: 3;
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  color: rgba(255, 255, 255, 0.6);
}

.mosaic-row__name {
  position: relative;
  z-index: 3;
  color: var(--color-white);
  font-size: clamp(1.75rem, 3.4vw, 2.75rem);
}

.mosaic-row__detail {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-xs);
  padding: var(--space-lg) var(--content-pad);
  border-inline-start: 1px solid rgba(255, 255, 255, 0.14);
}

.mosaic-row__detail .eyebrow {
  color: rgba(255, 255, 255, 0.5);
}

.mosaic-row__desc {
  color: rgba(255, 255, 255, 0.82);
  max-width: 46ch;
}

.mosaic-row__desc strong {
  color: var(--color-white);
  font-weight: 600;
}

.mosaic-row__link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  color: var(--color-accent);
  margin-top: var(--space-2xs);
}

@media (max-width: 700px) {
  .mosaic-row {
    grid-template-columns: 1fr;
    min-height: 0;
  }

  .mosaic-row__panel {
    padding-block: var(--space-lg);
  }

  .mosaic-row__detail {
    border-inline-start: none;
    border-block-start: 1px solid rgba(255, 255, 255, 0.14);
  }
}

/* ---------- Mosaic reveal ----------
   Built by animations.js#initMosaicReveal for any [data-mosaic-reveal] host:
   a single generated (or real, once supplied) image is sliced into a grid of
   mixed 1x1/2x2 cells, which pop in — each already showing its correct slice
   — in random order as the host scrolls into view. Host needs position:
   relative/overflow:hidden — already true of .mosaic-row__panel. The
   hero's own photo dissolve (js/animations.js#initHeroSlideshow) reuses the same
   packMosaicGrid helper directly rather than going through this init. */

.mosaic-grid {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  pointer-events: none;
}

.mosaic-cell {
  opacity: 0;
  will-change: opacity;
}

/* ---------- Marquee ---------- */

.marquee {
  overflow: hidden;
  border-block: 1px solid var(--color-line);
  padding-block: var(--space-md);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

.marquee__track {
  display: flex;
  align-items: center;
  width: max-content;
  gap: var(--space-2xl);
  /* GSAP Draggable (js/animations.js#initMarqueeDrag) sets this itself
     once it initializes, switching to "grabbing" mid-drag — set here too
     as a static fallback so it still reads as draggable before JS runs. */
  cursor: grab;
}

/* Each source logo was cropped to its own content box, so widths vary
   naturally (a wordmark vs. a badge) — fixing height and letting width
   follow keeps every logo reading at the same visual size, with the flex
   gap above giving them all identical, evenly-spaced gutters. */
.marquee__logo {
  height: 48px;
  width: auto;
  flex-shrink: 0;
}

/* ---------- CTA band ----------
   Originally home-page-only; now shared (Services reuses it) so it lives
   here rather than in css/home.css. */

.cta__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-lg);
  padding: var(--space-xl) var(--space-xl);
  background: var(--color-blue-100);
  border-radius: var(--radius-lg);
}

.cta__inner h2 {
  max-width: 20ch;
  color: var(--color-blue-700);
}

@media (max-width: 860px) {
  .cta__inner {
    flex-direction: column;
    align-items: flex-start;
    padding: var(--space-lg);
  }
}

/* ---------- CTA band, full-bleed orange variant ----------
   A second CTA treatment, sitting alongside .cta__inner above rather than
   replacing it: a full-bleed burnt-orange field with a diagonal-grain
   texture, built for Services and reused as-is on Projects, and now Home
   too (its final CTA) — same "originally one page, now shared" path as
   .cta__inner itself. A page picks whichever of the two suits it, they're
   not different states of one component. */

.cta-band {
  position: relative;
  overflow: hidden;
  padding-block: var(--space-xl);
  color: var(--color-white);
  background: linear-gradient(100deg,
      var(--color-blue-900) 0%,
      #2E1711 26%,
      #8A3316 58%,
      var(--color-orange-600) 88%,
      var(--color-orange-500) 100%);
}

/* Fine diagonal grain so the gradient doesn't read as flat vinyl — the
   same "generated texture, no image file" approach as the mosaic panels. */
.cta-band__texture {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.5;
  background-image:
    repeating-linear-gradient(118deg, rgba(255, 255, 255, 0.05) 0 1px, transparent 1px 7px),
    radial-gradient(80% 140% at 8% 50%, rgba(20, 33, 61, 0.55), transparent 62%);
}

/* ---------- CTA band, photo modifier (Home only) ----------
   Additive only — .cta-band with no modifier (Services/Projects) renders
   exactly as before. --photo layers a real photo under the same gradient
   + grain, darkened enough that the existing white text/button still sit
   comfortably on top, for the cinematic full-bleed close the Home redesign
   calls for. .cta-band__photo's background-image is set inline per page
   (same convention as .hero__slide-base). */
.cta-band--photo {
  background: var(--color-blue-900);
}

.cta-band--photo .cta-band__photo {
  position: absolute;
  inset: 0;
  z-index: 0;
  background-size: cover;
  background-position: center;
}

.cta-band--photo .cta-band__texture {
  z-index: 1;
  opacity: 0.85;
  background-image:
    repeating-linear-gradient(118deg, rgba(255, 255, 255, 0.05) 0 1px, transparent 1px 7px),
    linear-gradient(100deg,
      rgba(20, 33, 61, 0.88) 0%,
      rgba(46, 23, 17, 0.55) 30%,
      rgba(138, 51, 22, 0.35) 60%,
      rgba(205, 88, 32, 0.28) 100%);
}

.cta-band--photo .cta-band__inner {
  z-index: 2;
}

.cta-band__inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-lg) var(--space-xl);
}

.cta-band__copy {
  flex: 1 1 22rem;
}

.cta-band__title {
  color: var(--color-white);
  font-size: clamp(1.75rem, 1.8vw + 1rem, 2.5rem);
  line-height: 1.1;
  max-width: 18ch;
}

.cta-band__lead {
  margin-top: var(--space-sm);
  color: rgba(255, 255, 255, 0.84);
  max-width: 44ch;
}

.cta-band__btn {
  flex: none;
}

.cta-band__rail {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
  padding-inline-start: var(--space-lg);
  border-inline-start: 1px solid rgba(255, 255, 255, 0.3);
  font-family: var(--font-mono);
  font-size: var(--fs-eyebrow);
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.82);
}

@media (max-width: 860px) {
  .cta-band__rail {
    padding-inline-start: 0;
    border-inline-start: none;
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--space-2xs) var(--space-md);
  }
}

/* ---------- Footer ---------- */

.footer {
  background: var(--color-blue-900);
  color: rgba(255, 255, 255, 0.7);
  padding-block-start: var(--space-2xl);
}

.footer__grid {
  display: grid;
  grid-template-columns: 1.4fr 1fr 1fr;
  gap: var(--space-xl);
  padding-bottom: var(--space-xl);
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}

.footer__brand {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.footer__logo {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  color: var(--color-white);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.15rem;
}

.footer__logo-mark {
  width: 30px;
  height: 30px;
  border-radius: 50%;
}

.footer__tagline {
  max-width: 34ch;
}

.footer h4 {
  color: var(--color-white);
  font-family: var(--font-mono);
  font-size: var(--fs-eyebrow);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 500;
  margin-bottom: var(--space-sm);
}

.footer__links li+li,
.footer__contact li+li {
  margin-top: var(--space-xs);
}

.footer__links a:hover {
  color: var(--color-white);
}

.footer__bottom {
  display: flex;
  justify-content: space-between;
  gap: var(--space-md);
  padding-block: var(--space-md);
  font-size: var(--fs-small);
}

@media (max-width: 860px) {
  .nav__links {
    position: fixed;
    inset-block-start: var(--nav-height);
    inset-inline: 0;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    background: var(--color-off-white);
    border-bottom: 1px solid var(--color-line);
    padding: var(--space-sm) var(--content-pad) var(--space-md);
    transform: translateY(-8px);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
  }

  .nav__links.is-open {
    opacity: 1;
    transform: translateY(0);
    visibility: visible;
    pointer-events: auto;
  }

  .nav__links .nav__link {
    color: var(--color-ink);
    width: 100%;
    padding-block: var(--space-sm);
    border-bottom: 1px solid var(--color-line);
  }

  /* About's dropdown becomes an inline accordion here — a flyout menu
     doesn't make sense inside the already-vertical slide-down mobile
     menu. The label + caret row keeps the same border used by every other
     .nav__link above; the sub-links no longer need their own, or every
     open accordion would show a double line under its header row. */
  .nav__item--dropdown {
    width: 100%;
  }

  .nav__item-row {
    width: 100%;
    justify-content: space-between;
  }

  .nav__item-row .nav__link {
    border-bottom: none;
  }

  /* Back into normal flow here — the desktop rule takes it out of flow
     (position: absolute) to avoid widening the flex item and throwing off
     nav spacing (see the desktop comment above), but that's irrelevant to
     this already-vertical mobile layout, which instead needs the caret
     pinned to the row's far end via .nav__item-row's justify-content:
     space-between above. */
  .nav__dropdown-toggle {
    position: static;
    transform: none;
    margin-inline-start: 0;
    color: var(--color-ink);
    opacity: 1;
  }

  /* Grid 0fr->1fr accordion, not a hardcoded max-height — a fixed
     max-height (220px, tuned for 3 links) always overshoots the real
     content (127px for the current 3 links), leaving a dead gap below
     the links before the next nav item. grid-template-rows: 0fr/1fr
     tracks the wrapper's actual content height instead, so it stays
     correct regardless of how many links this dropdown ever has.
     .nav__dropdown-inner (the single grid item spanning that row) is
     what actually gets clipped by overflow: hidden mid-transition. */
  .nav__dropdown {
    position: static;
    transform: none;
    width: 100%;
    min-width: 0;
    background: transparent;
    box-shadow: none;
    padding: 0;
    margin-inline-start: var(--space-sm);
    display: grid;
    grid-template-rows: 0fr;
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: grid-template-rows var(--dur-base) var(--ease-out);
  }

  .nav__dropdown-inner {
    overflow: hidden;
  }

  .nav__item--dropdown.is-open .nav__dropdown {
    grid-template-rows: 1fr;
  }

  .nav__item--dropdown.is-open .nav__dropdown-inner {
    padding-block-end: var(--space-2xs);
  }

  .nav__dropdown-link {
    display: block;
    padding: var(--space-2xs) 0;
    color: var(--color-ink-soft);
  }

  .nav__cta {
    display: none;
  }

  .nav__toggle {
    display: flex;
  }

  .nav__toggle.is-active span {
    opacity: 0;
  }

  .nav__toggle.is-active::before {
    transform: translateY(3.5px) rotate(45deg);
  }

  .nav__toggle.is-active::after {
    transform: translateY(-3.5px) rotate(-45deg);
  }

  .footer__grid {
    grid-template-columns: 1fr;
    gap: var(--space-lg);
  }

  .footer__bottom {
    flex-direction: column;
  }
}