/* ---------- Custom properties ---------- */

:root {
  --c-black: #0a0a0a;
  --c-near-black: #141414;
  --c-white: #fafafa;
  --c-gray-100: #e5e5e5;
  --c-gray-400: #8a8a8a;
  --c-gray-700: #2b2b2b;

  --font-sans: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif;
  --fs-hero: clamp(3rem, 12vw, 10rem);
  --fs-h2: clamp(1.5rem, 4vw, 2.5rem);
  --fs-nav: 0.8rem;
  --tracking-tight: -0.03em;
  --tracking-wide: 0.15em;

  --space-1: 0.5rem;
  --space-2: 1rem;
  --space-3: 2rem;
  --space-4: 4rem;
  --space-5: 8rem;

  --footer-h: 44px;
  --grid-gap: var(--space-2);
  --container-pad: clamp(1rem, 4vw, 3rem);

  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --reveal-duration: 700ms;
  --fields-reveal-duration: 2800ms;

  --z-footer: 50;
  --z-lens: 60;
  --z-menu: 100;
  --z-menu-trigger: 110;

  --lens-size: 170px;
  --lens-blur: 16px;
  --lens-fade-duration: 250ms;

  --fs-menu-primary: clamp(2rem, 9vw, 5.5rem);
  --fs-menu-secondary: clamp(0.85rem, 2vw, 1rem);

  --menu-open-duration: 500ms;
  --menu-close-duration: 350ms;

  --z-stack-base: 2;

  --intro-scale: 2.2;
  --intro-spin-duration: 1400ms;
  --intro-dock-duration: 600ms;
  --z-intro: 9999;

  --brands-duration: 36s;
  --brands-gap: clamp(2rem, 6vw, 4rem);
  --brands-logo-h: 30px;
}

/* ---------- Reset ---------- */

*, *::before, *::after { box-sizing: border-box; }
html, body, h1, h2, p, nav, ul { margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
img, canvas { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; }

/* ---------- Shared type patterns ---------- */

.hero-nav,
.fields-word,
.field-label,
.field-count,
.field-error,
.contact-submit,
.contact-alt,
.site-footer,
.menu-trigger,
.menu-link--secondary,
.menu-meta {
  text-transform: uppercase;
  letter-spacing: var(--tracking-wide);
}

.divider-heading,
.category-heading,
.contact-heading,
.menu-link--primary {
  text-transform: uppercase;
  letter-spacing: var(--tracking-tight);
}

body {
  background: var(--c-black);
  color: var(--c-white);
  font-family: var(--font-sans);
  line-height: 1.4;
  padding-bottom: var(--footer-h);
}

/* ---------- Hero ---------- */

.hero {
  position: relative;
  width: 100vw;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  display: grid;
  place-items: end center;
  padding-bottom: var(--space-5);
}

/* full-bleed hero photo sits underneath; .hero-bg's gradient now acts as a
   translucent dark scrim over it, keeping the title legible rather than
   being the background itself */
.hero-mark {
  position: absolute;
  inset: 0;
  background-image: url("images/test.jpg");
  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;
  pointer-events: none;
}

.hero-bg {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 30% 20%, rgba(255, 255, 255, .08), transparent 60%),
    radial-gradient(circle at 75% 80%, rgba(255, 255, 255, .05), transparent 55%),
    repeating-linear-gradient(115deg, #0d0d0d 0 2px, #050505 2px 4px),
    linear-gradient(160deg, #1a1a1a, #000 70%);
  background-blend-mode: screen, screen, normal, normal;
  opacity: .6;
}

.hero-content {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  text-align: center;
  padding: 0 var(--container-pad);
}

.hero-title {
  font-weight: 900;
  font-size: var(--fs-hero);
  letter-spacing: 0.02em;
  line-height: .88;
  text-transform: uppercase;
  color: var(--c-white);
}

.hero-subtitle {
  font-size: clamp(1rem, 2.2vw, 1.375rem);
  font-weight: 500;
  color: var(--c-gray-100);
  max-width: 42ch;
}

.hero-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-size: var(--fs-nav);
}

.hero-nav a {
  color: var(--c-white);
  opacity: .75;
  transition: opacity .2s, letter-spacing .2s;
}

.hero-nav a:hover, .hero-nav a:focus-visible {
  opacity: 1;
  letter-spacing: calc(var(--tracking-wide) + .03em);
}

.hero-nav .dot {
  opacity: .4;
}

/* ---------- Brands marquee ---------- */
/* Purely decorative continuous ticker, no hover interaction. Seamless
   loop: two identical 8-logo sets sit back to back. Spacing uses
   margin-right on every logo (including the last one in each set)
   rather than flex `gap` — that makes .brands-track exactly 2x one
   set's width with an even gap at the set/set seam too, so
   translateX(-50%) always lands exactly where the first set started
   with zero rounding/seam error. (flex `gap` alone would only add a
   gap *between* items, leaving the seam between sets one gap narrower
   than the internal gaps — a visible stutter at the wrap point.) */

.brands {
  overflow: hidden;
  padding: var(--space-3) 0;
}

.brands-track {
  display: flex;
  width: max-content;
  animation: brands-scroll var(--brands-duration) linear infinite;
  will-change: transform;
}

.brands-set {
  display: flex;
  flex: none;
}

.brands-logo {
  display: block;
  flex: none;
  height: var(--brands-logo-h);
  width: auto;
  margin-right: var(--brands-gap);
}

@keyframes brands-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .brands-track {
    animation: none;
    transform: none;
  }
  /* overflow:hidden alone clips the duplicate set on any viewport
     narrower than one full 8-logo set's width, but a very wide viewport
     could let the duplicate set's first few logos peek in right after
     the real set — invisible today since both sets are identical, but
     would look like an obvious repeat once real distinct logos replace
     the placeholder. Hiding it outright removes that edge case; already
     aria-hidden so no assistive-tech impact either way. */
  .brands-set[aria-hidden="true"] {
    display: none;
  }
}

/* ---------- FIELDS divider ---------- */

.divider {
  padding: var(--space-4) var(--container-pad);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-3);
}

.divider-heading {
  font-size: var(--fs-h2);
  font-weight: 800;
}

/* base look for .line — only real usage is .footer-mark .line (Sticky
   footer, below), which sets its own width/flex */
.line {
  height: 1px;
  background: var(--c-gray-400);
}

.fields-intro {
  width: 100%;
  font-size: clamp(1.125rem, 2.4vw, 1.75rem);
  font-weight: 500;
  line-height: 1.45;
  color: var(--c-gray-100);
  /* padding gives the blur room to fade out naturally before hitting the
     clip edge below, instead of getting cut off hard right at the text —
     border-radius+overflow then rounds that clip edge off, so the whole
     blurred block reads as a soft rounded shape rather than a rectangle
     with sharp corners */
  padding: 2.5rem;
  border-radius: 28px;
  overflow: hidden;
  filter: blur(20px);
  transition: filter var(--fields-reveal-duration) var(--ease-out);
}

/* toggled by initFieldsReveal()'s IntersectionObserver — unlike .reveal
   (one-shot, unobserves after first reveal), this one keeps observing and
   toggles both ways, so it blurs back out on scrolling away, not just in.
   Stays fully opaque throughout — only ever blurs, never disappears. */
.fields-intro.is-visible {
  filter: blur(0);
}

.fields-word {
  font-weight: 800;
  font-size: 1.15em;
  color: var(--c-white);
  -webkit-text-fill-color: var(--c-white);
}

/* ---------- Category grids ---------- */

.category {
  padding: var(--space-5) var(--container-pad);
}

.category-heading {
  font-size: var(--fs-h2);
  font-weight: 800;
  margin-bottom: var(--space-3);
}

.category-heading,
.contact-heading,
.menu-link--primary,
.menu-link--secondary,
.menu-meta-location,
.menu-contacts a {
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}

.category-heading,
.contact-heading {
  background-image: linear-gradient(
    90deg,
    var(--c-gray-400) 0%,
    var(--c-gray-400) 40%,
    var(--c-white) 50%,
    var(--c-gray-400) 60%,
    var(--c-gray-400) 100%
  );
  background-size: 300% 100%;
  background-position: 300% 0;
  animation: text-shimmer 18s ease-in-out infinite alternate;
  will-change: background-position;
  transform: translateZ(0);
}

@keyframes text-shimmer {
  from { background-position: 300% 0; }
  to   { background-position: -200% 0; }
}

.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--grid-gap);
}

.tile {
  position: relative;
  display: block;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--c-gray-700);
}

.tile-media {
  position: absolute;
  inset: 0;
  background-color: var(--c-gray-700);
  background-image: url("images/test.jpg");
  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;
}

.tile[data-tone="2"] .tile-media { transform: scaleX(-1); }
.tile[data-tone="3"] .tile-media { transform: rotate(180deg); }

/* ---------- Image hover blur lens ---------- */
/* created once and repositioned in main.js (initImageBlurLens()) rather
   than per-tile, so there's only ever one in the DOM. backdrop-filter
   blurs whatever's rendered behind the lens (the tile image), not the
   lens's own (nonexistent) content — mask-image fades that blur out
   toward the edge instead of cutting off in a hard circle, matching the
   soft-edged treatment used elsewhere on the site (Behind the Lens). */
.image-lens {
  position: fixed;
  top: 0;
  left: 0;
  width: var(--lens-size);
  height: var(--lens-size);
  border-radius: 50%;
  pointer-events: none;
  z-index: var(--z-lens);
  backdrop-filter: blur(var(--lens-blur));
  -webkit-backdrop-filter: blur(var(--lens-blur));
  mask-image: radial-gradient(circle, black 35%, transparent 70%);
  -webkit-mask-image: radial-gradient(circle, black 35%, transparent 70%);
  opacity: 0;
  transition: opacity var(--lens-fade-duration) ease-out;
  will-change: transform;
}

.image-lens.is-active {
  opacity: 1;
}

/* cursor doubles as the lens itself while hovering an eligible image, so
   the native pointer is hidden rather than shown alongside it */
.tile:hover,
.hero:hover {
  cursor: none;
}

@media (prefers-reduced-motion: reduce), (hover: none) {
  .image-lens {
    display: none;
  }
  .tile:hover {
    cursor: pointer;
  }
  .hero:hover {
    cursor: auto;
  }
}

/* ---------- Category stack (scroll-pinned cards) ---------- */
/* scoped to body.is-home only — the homepage teases each category with a
   scroll-pinned peek card. projects/index.html has its own, larger-scale
   version of this same mechanic in projects/style.css. Reduced-motion or
   any other page falls back to the plain, unstacked .category/.grid/.tile
   rules defined earlier. */

@media (prefers-reduced-motion: no-preference) {
  :root {
    /* --stack-height is structurally capped at (100vh - 3 * --stack-step): the
       last card sticks at top: inset + step*3, so anything taller than that
       pushes its own bottom edge below the viewport for the entire time it's
       pinned — sticky can't reveal it, since the top offset stays fixed while
       stuck. 60vh leaves a couple of vh of margin under that 64vh ceiling. */
    --stack-height: 60vh;
    --stack-step: 12vh;
    --stack-inset: max(0px, calc((100vh - var(--stack-height) - var(--stack-step) * 3) / 2));
  }

  body.is-home .category {
    height: var(--stack-height);
    padding: 0 var(--container-pad) 3vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: var(--c-black);
  }

  /* height locked to exactly the peek window (--stack-step): the heading's own
     box is structurally guaranteed to consume the entire visible sliver of a
     receded card, so the grid below it can never leak into view — this isn't
     a numeric coincidence tuned to font metrics, it's true by construction.
     Unscoped (not body.is-home): only depends on --stack-step, which the
     projects page scopes on its own, so this is shared as the one copy for
     both pages instead of being duplicated in projects/style.css too. */
  .category-heading {
    flex: none;
    height: var(--stack-step);
    display: flex;
    align-items: center;
    font-size: clamp(1.75rem, 4vh, 3.5rem);
    line-height: 1.2;
    margin: 0;
  }

  body.is-home .grid {
    flex: 1 1 auto;
    min-height: 0;
    grid-template-rows: 1fr;
  }

  body.is-home .tile {
    aspect-ratio: auto;
    height: 100%;
  }

  body.is-home #photography {
    position: sticky;
    top: var(--stack-inset);
    scroll-margin-top: var(--stack-inset);
    z-index: var(--z-stack-base);
  }

  body.is-home #architectural-design {
    position: sticky;
    top: calc(var(--stack-inset) + var(--stack-step));
    scroll-margin-top: calc(var(--stack-inset) + var(--stack-step));
    z-index: calc(var(--z-stack-base) + 1);
  }

  body.is-home #creative-sessions {
    position: sticky;
    top: calc(var(--stack-inset) + var(--stack-step) * 2);
    scroll-margin-top: calc(var(--stack-inset) + var(--stack-step) * 2);
    z-index: calc(var(--z-stack-base) + 2);
  }

  body.is-home #travel-hotels {
    position: sticky;
    top: calc(var(--stack-inset) + var(--stack-step) * 3);
    scroll-margin-top: calc(var(--stack-inset) + var(--stack-step) * 3);
    z-index: calc(var(--z-stack-base) + 3);
  }

  /* sits above every stacked card so it's never hidden behind travel-hotels
     if the page doesn't have enough content after it to fully scroll away */
  body.is-home #contact {
    position: relative;
    z-index: calc(var(--z-stack-base) + 4);
    background: var(--c-black);
  }
}

/* ---------- Scroll reveal ---------- */

.reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity var(--reveal-duration) var(--ease-out),
              transform var(--reveal-duration) var(--ease-out);
}

.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .reveal {
    transition: none;
    opacity: 1;
    transform: none;
  }
  .fields-intro {
    transition: none;
    filter: none;
  }
  .category-heading,
  .contact-heading,
  .menu-link--primary,
  .menu-link--secondary,
  .menu-meta-location,
  .menu-contacts a {
    animation: none;
    background-image: none;
    -webkit-text-fill-color: var(--c-white);
    color: var(--c-white);
  }
  .field.is-shaking input,
  .field.is-shaking textarea {
    animation: none;
  }
  .site-footer, .footer-inner, .w-mark {
    transition: none !important;
    animation: none !important;
  }
  body.intro-cover .footer-inner { transform: none; }
  body.intro-cover .w-mark {
    animation: none;
    background-image: none;
    -webkit-text-fill-color: var(--c-white);
    color: var(--c-white);
  }
  body.intro-active .site-footer,
  body.intro-cover .site-footer {
    height: var(--footer-h);
    background: rgba(10, 10, 10, .85);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border-top: 1px solid rgba(255, 255, 255, .08);
    z-index: var(--z-footer);
  }
}

/* ---------- Contact ---------- */

.contact {
  min-height: 100vh;
  min-height: 100svh;
  padding: var(--space-5) var(--container-pad);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-4);
}

.contact-intro {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.contact-heading {
  font-size: var(--fs-menu-primary);
  font-weight: 900;
  line-height: .95;
}

.contact-lede {
  font-size: clamp(1.125rem, 3vw, 1.5rem);
  font-weight: 500;
  color: var(--c-gray-100);
  max-width: 32ch;
}

.contact-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  width: 100%;
  max-width: 640px;
}

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.field-label {
  font-size: var(--fs-nav);
  font-weight: 700;
  color: var(--c-gray-400);
}

.field-label-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2);
}

.field input,
.field textarea {
  background: none;
  border: none;
  border-bottom: 1px solid var(--c-gray-400);
  border-radius: 0;
  width: 100%;
  padding: var(--space-1) 0;
  font: inherit;
  font-size: clamp(1.125rem, 2.5vw, 1.5rem);
  font-weight: 700;
  color: var(--c-white);
  resize: none;
  transition: border-color .2s, border-bottom-width .2s;
}

.field textarea {
  overflow: hidden;
}

.field input:focus,
.field textarea:focus {
  outline: none;
  border-bottom-color: var(--c-white);
}

.field--honeypot {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.field-count {
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--c-gray-400);
  transition: color .2s;
}

.field-count.is-met {
  color: var(--c-white);
}

.field-error {
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--c-white);
  min-height: 1em;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity .2s, transform .2s;
}

.field.is-invalid .field-error {
  opacity: 1;
  transform: translateY(0);
}

.field.is-invalid input,
.field.is-invalid textarea {
  border-bottom-width: 2px;
}

@keyframes field-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-6px); }
  40% { transform: translateX(6px); }
  60% { transform: translateX(-4px); }
  80% { transform: translateX(4px); }
}

.field.is-shaking input,
.field.is-shaking textarea {
  animation: field-shake .4s ease-in-out;
}

.contact-submit {
  appearance: none;
  -webkit-appearance: none;
  align-self: flex-start;
  background: none;
  border: none;
  border-bottom: 2px solid var(--c-white);
  border-radius: 0;
  color: var(--c-white);
  font: inherit;
  font-size: var(--fs-nav);
  font-weight: 700;
  padding: var(--space-1) var(--space-2);
  margin-top: var(--space-1);
  cursor: pointer;
  transition: background-color .2s, color .2s;
}

.contact-submit:hover,
.contact-submit:focus-visible {
  background-color: var(--c-white);
  color: var(--c-black);
}

.contact-alt {
  font-size: var(--fs-nav);
  color: var(--c-gray-400);
}

.contact-alt a {
  color: var(--c-white);
  opacity: .85;
  transition: opacity .2s;
}

.contact-alt a:hover,
.contact-alt a:focus-visible {
  opacity: 1;
}

/* ---------- Sticky footer ---------- */

.site-footer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--z-footer);
  height: var(--footer-h);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  padding: 0 var(--container-pad);
  background: rgba(10, 10, 10, .85);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border-top: 1px solid rgba(255, 255, 255, .08);
  font-size: 0.7rem;
  pointer-events: none;
}

.site-footer * {
  pointer-events: auto;
}

.footer-inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  transform-origin: center center;
}

.footer-mark {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  width: auto;
  max-width: none;
  gap: var(--space-1);
}

.footer-mark .line {
  width: clamp(24px, 6vw, 64px);
  flex: none;
}

.w-mark {
  font-weight: 900;
}

.footer-text {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.footer-text--left {
  text-align: right;
}

.footer-text--right {
  text-align: left;
}

@media (max-width: 480px) {
  :root {
    --intro-scale: 1.3;
  }
  .site-footer {
    font-size: 0.6rem;
  }
  .footer-inner {
    gap: var(--space-2);
  }
  .footer-mark .line {
    width: clamp(12px, 4vw, 32px);
  }
}

/* ---------- Footer intro / loading sequence ---------- */
/* Plays once per browser session (gated in main.js via sessionStorage +
   prefersReducedMotion). body.intro-active spans the whole sequence
   (spin+type hold, then dock/shrink) and owns z-index — it must NOT be
   removed until the dock transition's transitionend fires, or the
   shrinking-but-still-large overlay would drop behind .menu-trigger
   mid-shrink. body.intro-cover is the inner "enlarged, full-viewport,
   holding" sub-state: present only for the spin+type phase, removed to
   *start* the dock — the transition rules below only match once
   .intro-cover is gone, so the initial snap-to-cover is instant (no
   unwanted grow-in) while the dock-out animates smoothly. */

html.intro-lock {
  overflow: hidden;
}

body.intro-active .site-footer {
  z-index: var(--z-intro);
}

body.intro-cover .site-footer {
  height: 100vh;
  background: var(--c-black);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-top: none;
}

body.intro-active:not(.intro-cover) .site-footer {
  transition: height var(--intro-dock-duration) var(--ease-out),
              background-color var(--intro-dock-duration) var(--ease-out);
}

body.intro-cover .footer-inner {
  /* shrink the LAYOUT width by 1/scale before scale() magnifies it back
     up visually — nets out to content that renders --intro-scale times
     bigger (transform magnifies actual pixel/font rendering) while the
     scaled-up box's footprint still lands within the original 100% width,
     instead of scale() blowing up an already-100%-wide row past the
     viewport edges with no way back */
  width: calc(100% / var(--intro-scale));
  transform: scale(var(--intro-scale));
}

/* belt-and-braces: the narrower pre-scale layout width above means the
   already-tight-fitting text is more likely to need wrapping to avoid
   overflowing even that — .footer-text normally relies on nowrap+ellipsis
   because the docked footer bar is tight on width, but wrapping here
   drops to a second line instead of clipping mid-word */
body.intro-cover .footer-text {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}

body.intro-active:not(.intro-cover) .footer-inner {
  transition: transform var(--intro-dock-duration) var(--ease-out);
}

body.intro-cover .w-mark {
  display: inline-block;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  background-image: linear-gradient(
    90deg,
    var(--c-gray-400) 0%,
    var(--c-gray-400) 40%,
    var(--c-white) 50%,
    var(--c-gray-400) 60%,
    var(--c-gray-400) 100%
  );
  background-size: 300% 100%;
  animation: w-spin-shimmer var(--intro-spin-duration) var(--ease-out) 1 forwards;
  will-change: transform, background-position;
}

@keyframes w-spin-shimmer {
  from { transform: rotate(0deg);   background-position: 300% 0; }
  to   { transform: rotate(360deg); background-position: -200% 0; }
}

/* ---------- Menu ---------- */

.menu-trigger {
  position: fixed;
  top: var(--container-pad);
  right: var(--container-pad);
  z-index: var(--z-menu-trigger);
  background: none;
  border: none;
  color: var(--c-white);
  font: inherit;
  font-size: var(--fs-nav);
  font-weight: 700;
  cursor: pointer;
  padding: var(--space-1) var(--space-2);
  min-height: 44px;
}

html.menu-open {
  overflow: hidden;
}

.menu-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-menu);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: flex-end;
  text-align: right;
  gap: var(--space-4);
  padding: var(--space-5) var(--container-pad) var(--container-pad);
  overflow-y: auto;
  background:
    radial-gradient(circle at 85% 10%, rgba(255, 255, 255, .06), transparent 55%),
    var(--c-near-black);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--menu-open-duration) var(--ease-out),
              visibility 0ms linear var(--menu-open-duration);
}

/* one shimmer sweeping top-to-bottom through every line in the menu, as a
   single surface. background-clip:text applied to the outer .menu-overlay
   (which wraps several nested flex containers) rendered nothing — it
   doesn't reliably composite across multiple levels of block-level nested
   children. Applying it to each leaf text element individually, but with
   background-attachment:fixed so every one of them samples the exact same
   viewport-anchored gradient at the exact same animated position, produces
   the same "one continuous surface" result without relying on DOM nesting. */
.menu-link--primary,
.menu-link--secondary,
.menu-meta-location,
.menu-contacts a {
  background-image: linear-gradient(
    180deg,
    var(--c-gray-400) 0%,
    var(--c-gray-400) 40%,
    var(--c-white) 50%,
    var(--c-gray-400) 60%,
    var(--c-gray-400) 100%
  );
  background-size: 100% 300%;
  background-position: 0 300%;
  background-attachment: fixed;
}

/* only animate while the overlay is actually open — this used to run
   continuously (infinite alternate) even while the menu was closed and
   invisible, wasting main-thread/compositor work on every page view */
.menu-overlay.is-open .menu-link--primary,
.menu-overlay.is-open .menu-link--secondary,
.menu-overlay.is-open .menu-meta-location,
.menu-overlay.is-open .menu-contacts a {
  animation: menu-shimmer 10s ease-in-out infinite alternate;
}

@keyframes menu-shimmer {
  from { background-position: 0 300%; }
  to   { background-position: 0 -200%; }
}

.menu-overlay.is-open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transition: opacity var(--menu-open-duration) var(--ease-out);
}

.menu-link,
.menu-meta {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 450ms var(--ease-out),
              transform 450ms var(--ease-out),
              letter-spacing .2s;
}

.menu-overlay.is-open .menu-link,
.menu-overlay.is-open .menu-meta {
  opacity: 1;
  transform: translateY(0);
}

.menu-overlay.is-closing {
  transition: opacity var(--menu-close-duration) var(--ease-out),
              visibility 0ms linear var(--menu-close-duration);
}

.menu-overlay.is-closing .menu-link,
.menu-overlay.is-closing .menu-meta {
  transition-delay: 0ms;
  transition-duration: 250ms;
}

.menu-primary {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);
}

.menu-link--primary {
  font-size: var(--fs-menu-primary);
  font-weight: 900;
  line-height: 1;
}

.menu-link--primary:hover,
.menu-link--primary:focus-visible {
  letter-spacing: calc(var(--tracking-tight) + .01em);
}

.menu-categories {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-2) var(--space-3);
}

.menu-link--secondary {
  font-size: var(--fs-menu-secondary);
  font-weight: 600;
}

.menu-link--secondary:hover,
.menu-link--secondary:focus-visible {
  letter-spacing: calc(var(--tracking-wide) + .03em);
}

.menu-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);
  font-size: 0.75rem;
}

.menu-contacts {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-2) var(--space-3);
}

.menu-contacts a {
  opacity: .8;
  transition: opacity .2s;
}

.menu-contacts a:hover,
.menu-contacts a:focus-visible {
  opacity: 1;
}

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

  .menu-overlay {
    gap: var(--space-3);
  }
  .menu-categories {
    flex-direction: column;
    align-items: flex-end;
  }
  .contact {
    min-height: auto;
    padding: var(--space-3) var(--container-pad) var(--space-2);
    justify-content: flex-start;
    gap: var(--space-1);
  }

  /* leaves room for the fixed menu-trigger button, which otherwise sits
     right over the last word once the heading moves up near the top */
  .contact-heading {
    padding-right: 5.5rem;
  }

  .contact-lede {
    padding-bottom: 10vh;
  }

  .contact-form {
    gap: var(--space-1);
  }

  .contact-submit {
    align-self: center;
    margin-bottom: var(--space-1);
  }

  /* mobile keeps the same sticky peek-card stack as desktop (defined
     above, scoped to body.is-home), just with one photo per card instead
     of three — there's no "view more" expand any more, so showing all 3
     never fits the 60vh peek-card budget. */
  body.is-home .tile:nth-child(n+2) {
    display: none;
  }
}
