/* ==========================================================================
   Glint Development — base styles
   Layout sizing lives largely in element-level `style` attributes, which use
   clamp()/min()/auto-fit and are therefore already fluid. This file carries
   the global defaults, the navigation behaviour, and the interaction states.
   ========================================================================== */

:root {
  --navy:        #1B2A4A;
  --navy-deep:   #101A31;
  --gold:        #B08D2E;
  --gold-light:  #C9A552;
  --gold-dark:   #8F6F1C;
  --paper:       #FAFAF7;
  --ink:         #2E3238;
  --nav-break:   1080px; /* desktop nav <-> mobile menu switch */
}

/* Deliberately NO global `box-sizing: border-box` reset. The layout was authored
   against the browser default (content-box) — max-width containers size and
   centre differently under border-box, shifting page gutters by ~20px. Elements
   that want border-box opt in themselves (e.g. the contact form inputs). */

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--paper);
  font-family: 'Hanken Grotesk', system-ui, -apple-system, sans-serif;
  -webkit-font-smoothing: antialiased;
  color: var(--ink);
}

a { color: var(--navy); }
a:hover { color: var(--gold); }

::selection { background: var(--gold); color: var(--paper); }

img { max-width: 100%; }

/* Respect the platform reduced-motion preference. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

@keyframes glintFloat {
  0%   { transform: translateY(0) rotate(0deg); }
  100% { transform: translateY(-22px) rotate(4deg); }
}

@keyframes glintSpin { to { transform: rotate(360deg); } }

/* Client-name marquee. The strip holds the logo list twice, so translating by
   -50% lands exactly on the duplicate and the loop is seamless. */
@keyframes glintTicker {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* The hero's SCROLL rule: wipes down, then retracts from the top. */
@keyframes glintScroll {
  0%   { transform: scaleY(0); transform-origin: top; }
  45%  { transform: scaleY(1); transform-origin: top; }
  55%  { transform: scaleY(1); transform-origin: bottom; }
  100% { transform: scaleY(0); transform-origin: bottom; }
}

/* Horizontal scrollers scroll but do not show a bar. */
[data-hidescroll]::-webkit-scrollbar { display: none; }

/* Skip link for keyboard users. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 400;
  background: var(--navy);
  color: var(--paper);
  padding: 12px 20px;
  border-radius: 0 0 10px 0;
  font-size: 14px;
  text-decoration: none;
}
.skip-link:focus {
  left: 0;
  color: var(--paper);
}

/* ==========================================================================
   Navigation
   ========================================================================== */

/* Desktop nav and the mobile "Menu" button swap at --nav-break. The original
   build did this in JS off window.innerWidth; CSS avoids the flash on load. */
.nav-mobile { display: none; }

@media (max-width: 1079px) {
  .nav-desktop { display: none; }
  .nav-mobile  { display: block; }
}

/* Nav dropdown targets sit under the fixed header, so an unadjusted jump would
   land with the heading hidden behind it. The header is 62px tall at top: 14px. */
:target { scroll-margin-top: 100px; }

/* Nav dropdown menus — Commercial, Glint Homes, Projects (desktop only).
   `display: contents` keeps the PHP wrapper from generating a box: the panel
   inside is absolutely positioned against <header>, and an animated wrapper
   would create a stacking context that traps it. Animate the panel instead. */
.nav-mega { display: contents; }

.nav-mega[hidden] { display: none; }

.nav-mega > div {
  animation: navMegaIn 0.22s cubic-bezier(0.22, 0.61, 0.21, 1) both;
}

/* The panel centres itself with translateX(-50%); the keyframes must carry that
   through, or the animation's fill state would drop it and shift the menu. */
@keyframes navMegaIn {
  from { opacity: 0; transform: translateX(-50%) translateY(-6px); }
  to   { opacity: 1; transform: translateX(-50%); }
}

@media (max-width: 1079px) {
  .nav-mega { display: none !important; }
}

/* Active page marker, applied from PHP via data-active. These three rules are
   mutually exclusive, mirroring the dark / pill / default branches the original
   applied in JS — each branch changes only the properties that branch touched.

   The default colour tracks --ni (the navbar ink the hero scrub drives) so the
   active link flips to off-white over the night footage like its siblings;
   hardcoding navy leaves it unreadable there. */
[data-navk][data-active]:not([data-dark]):not([data-pill]) {
  color: var(--ni, #1B2A4A) !important;
  border-bottom-color: var(--gold) !important;
  font-weight: 600 !important;
}

/* Contact pill: background and ink only — it keeps its own 700 weight. */
[data-navk][data-active][data-pill]:not([data-dark]) {
  background: var(--gold) !important;
  color: #141F38 !important;
}

/* Mobile overlay menu: colour only. */
[data-navk][data-active][data-dark] {
  color: var(--gold-light) !important;
}

/* Full-screen mobile menu.
   The overlay lives on the wrapper itself. Putting the animation on a static
   wrapper would give it a stacking context, so the fixed panel inside would be
   painted at the wrapper's flow position — underneath the page content. */
.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 300;
  animation: menuIn 0.28s cubic-bezier(0.22, 0.61, 0.21, 1) both;
}

.mobile-menu[hidden] { display: none; }

@keyframes menuIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Locks background scrolling while the mobile menu is open. */
body.menu-open { overflow: hidden; }

/* ==========================================================================
   Components
   ========================================================================== */

/* Projects listing: a fixed three-up grid on desktop.
   The rest of the site uses `repeat(auto-fit, minmax(min(Npx,100%), 1fr))`, but
   here auto-fit is too fragile — at the 1400px container three 420px tracks plus
   gaps need 1346px and only 1344px is available, so it silently collapsed to two
   columns, and on a 1280px screen it would collapse again. Explicit column
   counts keep it at three wherever there is room. The columns are declared here
   rather than inline so these rules do not need !important to win. */
.cards-3up {
  grid-template-columns: repeat(3, 1fr);
}

@media (max-width: 1024px) {
  .cards-3up { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 640px) {
  .cards-3up { grid-template-columns: 1fr; }
}

/* ==========================================================================
   Phone layout
   ========================================================================== */

/*
 * The hero is a 16:9 plate scrubbed by scroll inside a full-height sticky
 * stage. Neither half of that survives a phone: filling a 1:2 screen with 16:9
 * footage crops away everything but the middle of the house, and letterboxing
 * it inside a full-height stage leaves a screenful of empty navy under a small
 * picture. A tall screen simply has no room for both.
 *
 * So phones get a banner instead of a stage: the plate runs full width at its
 * own 16:9 ratio — whole house, nothing cropped — with the headline in normal
 * flow underneath. No sticky, no scroll runway, no overlay, which also means
 * the page reaches its real content on the first swipe rather than the third.
 */
@media (max-width: 760px) {
  /* The scrub runs here too, on the banner-m* sequence: a 9:16 cut of the
     same footage (whole house over its own reflection), so `cover` fills the
     screen without cropping the house out of the picture. The shorter touch
     runway comes from home.js. */

  /* Overlays are tuned for the 16:9 plate; over the portrait cut they only
     muddy it. The copy carries its own scrim instead. */
  .hero-veil-light,
  .hero-veil-dark { display: none !important; }

  .hero-copy {
    background: linear-gradient(180deg, rgba(6, 10, 22, 0) 38%, rgba(6, 10, 22, 0.82) 72%);
  }

  .hero-title {
    color: #FAFAF7 !important;
    text-shadow: 0 2px 20px rgba(6, 10, 22, 0.45) !important;
  }
  .hero-eyebrow {
    background: rgba(252, 252, 250, 0.9) !important;
    color: #664C0E !important;
  }
  .hero-ghost {
    color: #FAFAF7 !important;
    border-color: rgba(250, 250, 247, 0.7) !important;
    background: rgba(6, 10, 22, 0.28) !important;
  }

  /* The Day/Dusk/Night readout and the vertical SCROLL rule are a desktop
     affordance: on a phone they land on top of the headline and buttons, and
     the scrub they describe is driven by a thumb that is already scrolling.
     !important because the element carries its layout inline. */
  .hero-readout { display: none !important; }
}

/*
 * Section rails ("01 / ABOUT" turned on its side) reserve a column that is
 * worth its width on a desktop and is pure dead margin on a phone, squeezing
 * the copy into half the screen. Drop the rail and let the text run full width.
 */
@media (max-width: 760px) {
  .rail-2col { grid-template-columns: 1fr !important; }
  .rail-2col > :first-child { display: none !important; }
}

/*
 * Our process: on desktop a 1125px five-across track the page scrolls through
 * sideways. A phone would clip it to two and a half cards, so the track turns
 * into a vertical list — the outlined number on the left, step text beside it,
 * a rule between steps. The scroll-driven gold fill on the numbers (--procp)
 * still runs; it just advances down the list instead of across it.
 */
@media (max-width: 760px) {
  #procTrack [data-hidescroll] { overflow-x: visible !important; }
  #procTrack [data-hidescroll] > div {
    grid-template-columns: 1fr !important;
    min-width: 0 !important;
  }
  /* The gold progress rule along the track's top edge measures horizontal
     travel; it means nothing stacked. */
  #procTrack [data-hidescroll] > div > span[aria-hidden] { display: none; }

  .proc-step {
    display: grid !important;
    grid-template-columns: 72px 1fr;
    column-gap: 16px;
    row-gap: 6px !important;
    padding: 20px 0 16px !important;
    border-top: 1px solid rgba(27, 42, 74, 0.14);
  }
  .proc-step:first-of-type { border-top: 0; }
  /* The 34px tick tied each card to the horizontal rule above it. */
  .proc-step > span[aria-hidden] { display: none; }
  .proc-step > div[aria-hidden] {
    grid-column: 1;
    grid-row: 1 / span 3;
    font-size: 44px !important;
    align-self: start;
    padding-top: 3px;
  }
  .proc-step > :not(div[aria-hidden]) { grid-column: 2; }
}

/*
 * Footer: auto-fit stacks all four blocks into one long column on a phone.
 * The two link lists are short and narrow — they share a row; the brand blurb
 * and the office block keep the full width.
 */
@media (max-width: 760px) {
  .footer-grid { grid-template-columns: 1fr 1fr !important; gap: 36px 20px !important; }
  .footer-grid > :first-child,
  .footer-grid > :last-child { grid-column: 1 / -1; }
}

/* Featured work: the hover index gives way to one card per project. */
.fp-stack { display: none; }

@media (max-width: 760px) {
  .fp-split { display: none !important; }
  .fp-stack { display: grid; gap: clamp(34px, 7vw, 48px); }
}

/* Project gallery: four to a row. Flex rather than grid so a short last row
   (or a gallery of fewer than four) keeps the same tile size and sits centred
   instead of stretching to fill the width. */
.gallery-4up {
  --gal-gap: clamp(14px, 1.8vw, 26px);
  --gal-cols: 4;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--gal-gap);
}
.gallery-4up > * {
  flex: 0 0 calc((100% - (var(--gal-cols) - 1) * var(--gal-gap)) / var(--gal-cols));
}

@media (max-width: 1024px) {
  .gallery-4up { --gal-cols: 3; }
}

@media (max-width: 720px) {
  .gallery-4up { --gal-cols: 2; }
}

@media (max-width: 440px) {
  .gallery-4up { --gal-cols: 1; }
}

/* ==========================================================================
   Scroll reveal
   ========================================================================== */

/* Only content that starts BELOW the fold animates in. Anything already on
   screen at load is left alone, as in the original — hiding it in CSS and
   fading it back would add an entrance animation the original never had (and
   would flash blank if JS were slow). JS adds data-rv-hidden to the rest. */
[data-rv][data-rv-hidden] {
  opacity: 0;
  transform: translateY(26px);
}

[data-rv] {
  transition:
    opacity 0.8s cubic-bezier(0.22, 0.61, 0.21, 1),
    transform 0.8s cubic-bezier(0.22, 0.61, 0.21, 1);
}

@media (prefers-reduced-motion: reduce) {
  [data-rv][data-rv-hidden] { opacity: 1; transform: none; }
}

/* ==========================================================================
   Interaction states (generated from the original style-hover/style-focus)
   ========================================================================== */

.gh1:hover { color: #B08D2E !important; }
.gh2:hover { background: #B08D2E !important; color: #141F38 !important; }
.gh3:hover { background: rgba(27, 42, 74, 0.06) !important; color: #B08D2E !important; }
.gh4:hover { color: #C9A552 !important; }
.gh5:hover { opacity: .88 !important; transform: translateY(-2px) !important; color: #141F38 !important; }
.gh6:hover { transform: translateY(-2px) !important; }
.gh7:hover { animation-play-state: paused !important; }
.gh8:hover { transform: scale(1.05) !important; }
.gh9:hover { flex-grow: 1.4 !important; background: #EFECE2 !important; }
.gh10:hover { flex-grow: 1.4 !important; background: #16233F !important; }
.gh11:hover { color: #B08D2E !important; border-bottom-color: #B08D2E !important; }
.gh12:hover { background: #1B2A4A !important; color: #FAFAF7 !important; border-color: #1B2A4A !important; }
.gh13:hover { transform: scale(1.04) !important; }
.gh14:hover { background: #B08D2E !important; color: #141F38 !important; border-color: #B08D2E !important; }
.gh15:hover { color: #E0BC66 !important; border-bottom-color: #E0BC66 !important; }
.gh16:hover { color: #D9BC66 !important; }
.gh17:hover { opacity: .88 !important; transform: translateY(-2px) !important; }

.gf1:focus-visible { outline: 2px solid #B08D2E !important; outline-offset: 4px !important; border-radius: 4px !important; }
.gf2:focus-visible { outline: 2px solid #B08D2E !important; outline-offset: 4px !important; }
.gf3:focus-visible { outline: 2px solid #B08D2E !important; outline-offset: 3px !important; }
.gf4:focus-visible { outline: 2px solid #C9A552 !important; outline-offset: 4px !important; }
.gf5:focus-visible { outline: 2px solid #FAFAF7 !important; outline-offset: 3px !important; }
.gf6:focus-visible { outline: 2px solid #B08D2E !important; outline-offset: -2px !important; }
.gf7:focus-visible { outline: 2px solid #C9A552 !important; outline-offset: -2px !important; }
.gf8:focus-visible { outline: 2px solid #1B2A4A !important; outline-offset: 3px !important; }
.gf9:focus-visible { outline: 2px solid #C9A552 !important; outline-offset: 3px !important; }
.gf10:focus-visible { outline: 2px solid #FAFAF7 !important; outline-offset: 2px !important; }
.gf11:focus-visible { border-bottom-color: #B08D2E !important; }

/* Hover effects are meaningless on touch and cause sticky states — disable. */
@media (hover: none) {
  .gh6:hover,
  .gh8:hover,
  .gh13:hover,
  .gh17:hover,
  .gh5:hover { transform: none !important; }

  .gh9:hover,
  .gh10:hover { flex-grow: 1 !important; }
}

/* -------------------------------------------------------------- reCAPTCHA v3
   v3 shows no puzzle, only a floating badge. Google's terms allow hiding it
   provided the equivalent notice is shown instead, which contact.php renders
   beside the form.

   The badge is hidden by default and revealed only while a protected form is on
   screen — assets/js/recaptcha.js toggles .rc-visible on <html> from an
   IntersectionObserver. Without that class the badge never floats over pages
   that have no form on them.

   visibility + opacity rather than display:none: Google's script measures the
   element, and display:none has been known to make it re-inject itself. */
.grecaptcha-badge {
  visibility: hidden;
  opacity: 0;
  transition: opacity .3s ease, visibility .3s ease;
}

.rc-visible .grecaptcha-badge {
  visibility: visible;
  opacity: 1;
}

/* Keep the badge clear of the fixed footer CTA on small screens. */
@media (max-width: 700px) {
  .grecaptcha-badge { bottom: 12px !important; }
}

@media (prefers-reduced-motion: reduce) {
  .grecaptcha-badge { transition: none; }
}

/* --------------------------------------------------------- button spinner
   The contact form's Send button carries its styling inline, so the spinner is
   a child element that inherits colour rather than a restyle of the button. */
.btn-spinner {
  display: inline-block;
  width: 15px; height: 15px;
  margin-right: 8px;
  vertical-align: -2px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: btnSpin .6s linear infinite;
}
@keyframes btnSpin { to { transform: rotate(360deg); } }

button.is-loading { cursor: progress; opacity: .85; }
button[disabled] { pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  .btn-spinner { animation: none; border-right-color: currentColor; opacity: .55; }
}

/* Form controls that are not text entry still get 16px, purely so an audit of
   "inputs under 16px" comes back clean. Neither can trigger the iOS zoom that
   rule guards against — the honeypot is off-screen and never focused, and a
   checkbox has no text to zoom into — but an unexplained flag costs more to
   re-derive later than this rule costs to keep. */
input[type="checkbox"],
input[type="radio"] { font-size: 16px; }
