/* ============================================================
   Cab Co. — Sunshine Coast Cabinetry
   Foundation stylesheet
   ------------------------------------------------------------
   1. Design tokens
   2. Reset & base
   3. Typography
   4. Layout primitives
   5. Buttons
   6. Reveal / motion
   7. Header
   8. Mobile nav
   9. Hero
  10. Scaffold sections & footer
  11. Reduced motion
   ============================================================ */


/* ============================================================
   1. DESIGN TOKENS
   ============================================================ */
:root {

  /* --- Colour ---
     Ink-blue on warm paper, pulled from the coastal logo. Nothing on this
     site is black or charcoal; "dark" always means the blue.

     Contrast, measured against --paper:
       ink        8.5:1   body and headlines
       muted      4.9:1   secondary text — passes AA at small sizes
       brass-ink  5.0:1   eyebrows and small brass text
       brass      3.5:1   FAILS small text — rules and large marks only  */
  --paper:        #faf7f1;   /* base background          */
  --paper-alt:    #fffdf8;   /* lifted panels            */
  --ink:          #1f4e5f;   /* primary text & headlines */
  --ink-deep:     #153a47;   /* the one dark section     */
  --brass:        #a67c52;   /* accent: rules, large marks */
  --brass-ink:    #886445;   /* accent: small text (5.0:1) */
  --brass-light:  #d3ab7e;   /* accent on the dark section (5.7:1) */
  --muted:        #5c6f78;   /* soft warm grey-blue      */

  /* Derived / utility */
  --hairline:       rgba(31, 78, 95, 0.14);
  --hairline-light: rgba(250, 247, 241, 0.26);
  --shadow-header:  0 1px 0 rgba(31, 78, 95, 0.07),
                    0 18px 40px -30px rgba(21, 58, 71, 0.55);
  /* photo scrims — a deep blue-black rather than neutral, so the imagery
     sits in the same family as the type */
  --scrim: 10, 30, 38;

  /* --- Type --- */
  --font-display: 'Fraunces', Georgia, 'Times New Roman', serif;
  --font-body:    'Nunito Sans', system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif;

  --headline-tracking: -0.015em;
  --headline-leading:  1.08;

  /* --- Motion: the soft-close drawer ---
     Everything on this site eases with --ease. Never bounce, never snap. */
  --ease:        cubic-bezier(0.16, 1, 0.3, 1);
  --dur:         0.8s;    /* the site default */
  --dur-fast:    0.45s;   /* micro-interactions only */
  --dur-slow:    1.2s;    /* hero settle */
  --dur-drama:   1.5s;    /* the two peak sections arriving */

  /* --- Space ---
     Three content widths drive the whole page rhythm. A section picks
     one; nothing sets a bespoke max-width of its own. */
  --w-narrow:    40rem;      /*  640px — quiet, centred moments      */
  --w-standard:  68.75rem;   /* 1100px — structured content          */
  --w-wide:      87.5rem;    /* 1400px — hero moments and imagery    */

  --gutter:      clamp(1.5rem, 5vw, 5rem);
  --gutter-bleed: clamp(1rem, 2.5vw, 2.5rem);    /* near-full-bleed sections */

  /* One vertical scale, two steps. --section-y is the page default;
     --section-y-air is reserved for the two deliberate pauses (the
     positioning strip and the process section). */
  --section-y:     clamp(4.5rem, 9vw, 10rem);
  --section-y-air: clamp(6.5rem, 13vw, 14rem);

  --header-h:    clamp(78px, 9vw, 104px);

  /* --- Safe area ---
     With viewport-fit=cover the page runs under the status bar / notch,
     so the fixed header has to pad itself by the top inset and paint its
     background across it — otherwise the page scrolls through that strip
     above the bar. Zero on every desktop browser, so nothing shifts there.

     --header-total is the header's real occupied height, inset included.
     Anything clearing the header measures against this, not --header-h. */
  --safe-top:     env(safe-area-inset-top, 0px);
  --header-total: calc(var(--header-h) + var(--safe-top));

  /* --- Reveal --- */
  --reveal-shift: 30px;
}


/* ============================================================
   2. RESET & BASE
   ============================================================ */
*,
*::before,
*::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: var(--header-total);
}

body {
  margin: 0;
  background-color: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(1rem, 0.96rem + 0.2vw, 1.0625rem);
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

body.is-locked { overflow: hidden; }

/* Paper tooth. A fixed grain multiplied over the page so the background
   reads as warm stock rather than flat colour. Generated inline as an SVG
   so there's no asset to ship; kept low enough that you feel it rather
   than see it. */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  opacity: 0.035;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
}

img,
picture,
svg { display: block; max-width: 100%; }

h1, h2, h3, h4, p, ul, ol, dl, dd, figure, blockquote { margin: 0; }
ul, ol { padding: 0; list-style: none; }

a { color: inherit; text-decoration: none; }

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

:focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 4px;
}

::selection {
  background: var(--brass-light);
  color: var(--ink);
}

.skip-link {
  position: absolute;
  top: 0;
  left: 0;
  /* Has to clear the header, which now sits at 1000 — otherwise the one
     control that must be reachable first lands behind it. */
  z-index: 1100;
  padding: 0.85rem 1.4rem;
  background: var(--paper);
  color: var(--ink);
  font-size: 0.8rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transform: translateY(-120%);
  transition: transform var(--dur) var(--ease);
}
.skip-link:focus-visible { transform: translateY(0); }


/* ============================================================
   3. TYPOGRAPHY
   ============================================================ */

/* Headlines — Fraunces 400, warm and characterful */
.h1, .h2, .h3,
h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: var(--headline-tracking);
  line-height: var(--headline-leading);
  text-wrap: balance;
}

.h2 {
  font-size: clamp(2.25rem, 5.2vw, 4rem);
}

/* Peak scale. Used where a headline needs to carry a section on its
   own rather than introduce one. */
.h2--xl {
  font-size: clamp(2.75rem, 7vw, 5.5rem);
}

.h3 {
  font-size: clamp(1.5rem, 2.6vw, 2.125rem);
}

/* Eyebrow label — Nunito Sans 600, uppercase, wide tracking, brass.
   Uses --brass-ink, not --brass: at 12px the lighter brass is 3.5:1. */
.eyebrow {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.75rem;
  line-height: 1.4;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--brass-ink);
}

.lede {
  max-width: 46ch;
  font-weight: 400;
  font-size: clamp(1.0625rem, 1vw + 0.75rem, 1.25rem);
  line-height: 1.75;
  color: var(--muted);
}


/* ============================================================
   4. LAYOUT PRIMITIVES
   ============================================================ */
/* --- Width system ---
   .container defaults to wide. Add a modifier to change the measure.
   --w is the *content* width; the gutter is added on top so the number
   in the token is what you actually read across. */
.container {
  --w: var(--w-wide);
  width: 100%;
  max-width: calc(var(--w) + var(--gutter) * 2);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.container--narrow   { --w: var(--w-narrow); }
.container--standard { --w: var(--w-standard); }
.container--wide     { --w: var(--w-wide); }

/* Edge to edge, bar a small safety gutter */
.container--bleed {
  max-width: none;
  padding-inline: var(--gutter-bleed);
}

/* --- Vertical rhythm --- */
.section { padding-block: var(--section-y); }

/* The two deliberate pauses */
.section--air { padding-block: var(--section-y-air); }

.section--alt { background: var(--paper-alt); }


/* ============================================================
   5. BUTTONS
   ------------------------------------------------------------
   Outlined, brass border. The brass fill wipes upward on hover
   with the soft-close ease — it settles, it doesn't snap.
   ============================================================ */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  isolation: isolate;
  padding: 1.05em 2.15em;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.75rem;
  line-height: 1;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  white-space: nowrap;
  border: 1px solid var(--brass);
  color: var(--ink);
  background: transparent;
  transition: color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}

.btn > span { position: relative; z-index: 1; }

/* Desktop shows the full label; the short one appears at ≤900px */
.site-header__cta .cta-short { display: none; }

.btn::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--brass);
  transform: scaleY(0);
  transform-origin: 50% 100%;
  transition: transform var(--dur) var(--ease);
}

.btn:hover,
.btn:focus-visible { color: var(--paper); }

.btn:hover::before,
.btn:focus-visible::before { transform: scaleY(1); }

/* Over photography / dark backgrounds */
.btn--on-dark {
  color: var(--paper);
  border-color: rgba(250, 247, 241, 0.55);
}
.btn--on-dark:hover,
.btn--on-dark:focus-visible {
  color: var(--paper);
  border-color: var(--brass);
}


/* ============================================================
   6. REVEAL / MOTION
   ------------------------------------------------------------
   Add .reveal to anything. JS adds .is-visible when it enters
   the viewport (or on load, inside [data-reveal-on="load"]).
   Wrap a group in [data-reveal-group] with an optional
   [data-reveal-stagger="120"] to cascade its .reveal children.
   ============================================================ */
.js .reveal {
  opacity: 0;
  transform: translateY(var(--reveal-shift));
  transition: opacity var(--dur) var(--ease),
              transform var(--dur) var(--ease);
  transition-delay: var(--reveal-delay, 0ms);
  will-change: opacity, transform;
}

.js .reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* Once settled, stop hinting the compositor */
.js .reveal.is-settled { will-change: auto; }

/* Peak sections arrive slower and travel further. Only the duration is
   overridden, so the shorthand keeps --ease and the property list. */
.js .reveal--drama {
  --reveal-shift: 48px;
  transition-duration: var(--dur-drama);
}


/* ============================================================
   7. HEADER
   ------------------------------------------------------------
   Transparent over the hero, then eases into cream + shadow
   once the hero is behind us.
   ============================================================ */
.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  /* Above everything the page can produce. The only thing deliberately
     over it is the paper-grain overlay, which is texture, not content. */
  z-index: 1000;
  /* Pushes the bar clear of the notch. env() is written out in full here
     rather than read from --safe-top: WebKit is unreliable about
     re-resolving env() through a custom property when the inset changes
     (which is exactly what happens as the toolbar collapses on scroll),
     and a stale 0 here would drop the bar under the status bar. */
  padding-top: env(safe-area-inset-top, 0px);
  color: var(--paper);
  background-color: transparent;
  box-shadow: 0 0 0 0 rgba(31, 78, 95, 0);
  transition: background-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease),
              color var(--dur) var(--ease);
}

/* Soft top scrim so nav stays legible over a bright photo */
.site-header::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: calc(var(--header-total) * 1.9);
  background: linear-gradient(to bottom,
              rgba(var(--scrim), 0.42) 0%,
              rgba(var(--scrim), 0.18) 45%,
              rgba(var(--scrim), 0) 100%);
  pointer-events: none;
  transition: opacity var(--dur) var(--ease);
}

/* --- The safe-area / status-bar fill ---
   The header's own background already covers its padding box, inset
   included — that is not the gap. The gap is that iOS repositions fixed
   elements asynchronously: while the toolbar collapses, and through any
   rubber-band overscroll, Safari can draw the bar a few pixels down from
   the top while the page keeps scrolling underneath. For those frames a
   background that stops at the header's own top edge leaves a strip of
   live page content above it.

   So the fill is painted *past* that edge. It starts 200px above the
   header and runs down through the inset, which means the cream still
   reaches physical pixel 0 even when iOS has the bar drawn low. When the
   header is where it should be the overhang is simply off-screen and
   costs nothing.

   background-color: inherit is doing real work — it keeps this in step
   with the header's own state, so it is cream on mobile and once
   scrolled, and stays transparent over the hero on desktop where the
   bar is meant to be see-through. ::before is already the photo scrim. */
.site-header::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: -200px;
  height: calc(200px + env(safe-area-inset-top, 0px));
  background-color: inherit;
  pointer-events: none;
}

.site-header.is-scrolled {
  background-color: var(--paper);
  color: var(--ink);
  box-shadow: var(--shadow-header);
}
.site-header.is-scrolled::before { opacity: 0; }

.site-header__inner {
  display: flex;
  align-items: center;
  gap: clamp(1.5rem, 4vw, 3.5rem);
  width: 100%;
  max-width: calc(var(--w-wide) + var(--gutter) * 2);
  min-height: var(--header-h);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* --- Logo ---
   The original badge, transparent, in two colourways stacked one over the
   other. Intrinsic ratio 1.181 — see tools/prep-logo.py: make_pair(), which
   crops both to one shared box so they stay registered through the fade. */
.logo {
  position: relative;
  flex: 0 0 auto;
  display: block;
  line-height: 0;
  /* Sized against --header-h (104px desktop, 72px phone). Width is the knob,
     but height is what has to clear: at ratio 1.181 these land the badge at
     ~76px in the 104px desktop bar and ~53px in the 72px phone bar, keeping
     about 14px and 10px of air above and below respectively. */
  --logo-w: clamp(62px, 7vw, 90px);
  width: var(--logo-w);
  aspect-ratio: 1.181;
  transition: opacity var(--dur) var(--ease);
}
.logo:hover { opacity: 0.72; }

.logo__img,
.logo__img img {
  display: block;
  width: 100%;
  height: auto;
}

/* Only the white one moves. It is the same silhouette with a hair softer an
   edge, so at full opacity it hides the colour beneath it completely — which
   means the pair never both sit at part-opacity at once. Cross-fading the
   two against each other would dip to 75% coverage at the midpoint and flash
   the background through the badge. */
.logo__img--white {
  position: absolute;
  inset: 0;
  opacity: 0;
  /* Same duration and curve as the header's own background transition, so
     the badge and the bar it sits on turn over together. */
  transition: opacity var(--dur) var(--ease);
}

/* Colour is the default, and stays the default anywhere the bar is solid —
   which below 900px is everywhere, at every scroll position. White is only
   swapped in for the one case that needs it: a transparent bar over the dark
   hero on a wide screen. */
@media (min-width: 901px) {
  .site-header:not(.is-scrolled):not(.is-nav-open) .logo__img--white {
    opacity: 1;
  }
}

/* --- Desktop nav --- */
.nav { margin-left: auto; }

.nav__list {
  display: flex;
  align-items: center;
  gap: clamp(1.5rem, 2.6vw, 2.75rem);
}

.nav__link {
  position: relative;
  display: inline-block;
  padding-block: 0.5rem;
  font-size: 0.8125rem;
  font-weight: 400;
  letter-spacing: 0.06em;
  color: inherit;
  transition: color var(--dur) var(--ease);
}

.nav__link::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform var(--dur) var(--ease);
}

.nav__link:hover::after,
.nav__link:focus-visible::after,
.nav__link.is-current::after { transform: scaleX(1); }

.site-header.is-scrolled .nav__link:hover { color: var(--brass-ink); }

/* --- Header CTA --- */
.site-header__cta {
  flex: 0 0 auto;
  padding: 0.95em 1.9em;
  color: inherit;
  border-color: rgba(250, 247, 241, 0.55);
  transition: color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.site-header__cta:hover,
.site-header__cta:focus-visible { color: var(--paper); }

.site-header.is-scrolled .site-header__cta { border-color: var(--brass); }

/* --- Hamburger --- */
.nav-toggle {
  display: none;
  flex: 0 0 auto;
  margin-left: auto;
  width: 44px;
  height: 44px;
  align-items: center;
  justify-content: center;
}

.nav-toggle__bars {
  position: relative;
  display: block;
  width: 26px;
  height: 10px;
}

.nav-toggle__bars span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transition: transform var(--dur) var(--ease),
              opacity var(--dur) var(--ease);
}
.nav-toggle__bars span:nth-child(1) { top: 0; }
.nav-toggle__bars span:nth-child(2) { top: 9px; }

.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(1) {
  transform: translateY(4.5px) rotate(45deg);
}
.nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(2) {
  transform: translateY(-4.5px) rotate(-45deg);
}

/* When the overlay is open the header always reads on cream */
.site-header.is-nav-open {
  color: var(--ink);
  background-color: transparent;
}
.site-header.is-nav-open::before { opacity: 0; }


/* ============================================================
   8. MOBILE NAV OVERLAY
   ============================================================ */
.mobile-nav {
  position: fixed;
  inset: 0;
  z-index: 150;
  display: flex;
  align-items: center;
  background: var(--paper);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur) var(--ease),
              visibility 0s linear var(--dur);
}

.mobile-nav[hidden] { display: none; }

.mobile-nav.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur) var(--ease),
              visibility 0s linear 0s;
}

.mobile-nav__inner {
  width: 100%;
  padding: calc(var(--header-total) + 2rem) var(--gutter) 3rem;
}

.mobile-nav__list {
  display: flex;
  flex-direction: column;
  gap: clamp(0.75rem, 3vw, 1.25rem);
  margin-bottom: clamp(2.5rem, 8vw, 4rem);
}

.mobile-nav__list a {
  display: inline-block;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.25rem, 11vw, 3.5rem);
  letter-spacing: var(--headline-tracking);
  line-height: 1.1;
  opacity: 0;
  transform: translateY(var(--reveal-shift));
  transition: opacity var(--dur) var(--ease),
              transform var(--dur) var(--ease),
              color var(--dur) var(--ease);
  transition-delay: var(--reveal-delay, 0ms);
}
.mobile-nav__list a:hover { color: var(--brass-ink); }

.mobile-nav__cta,
.mobile-nav__meta {
  opacity: 0;
  transform: translateY(var(--reveal-shift));
  transition: opacity var(--dur) var(--ease),
              transform var(--dur) var(--ease);
  transition-delay: var(--reveal-delay, 0ms);
}

.mobile-nav.is-open .mobile-nav__list a,
.mobile-nav.is-open .mobile-nav__cta,
.mobile-nav.is-open .mobile-nav__meta {
  opacity: 1;
  transform: translateY(0);
}

.mobile-nav__cta { display: inline-flex; }

.mobile-nav__meta {
  margin-top: 2.5rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--muted);
}


/* ============================================================
   9. HERO
   ============================================================ */
/* --- Timed before/after cross-dissolve -----------------------------
   A single-viewport hero running an 11s loop entirely in CSS. No JS,
   no scroll coupling, no input of any kind — which is precisely what
   makes it identical on a phone, a trackpad and a mouse.

   The loop:
       0.0s  hold on the empty alcove          3s
       3.0s  dissolve to the finished pantry   2s
       5.0s  hold on the pantry                4s   ← the payoff, longest
       9.0s  dissolve back                     2s
      11.0s  loop

   The Ken Burns drift shares the same 11s period and returns to
   scale(1) at 100%, so the loop point is seamless — nothing snaps
   back. The drift lives on the shared .hero__media wrapper, never on
   the individual layers, so both photographs scale as one and cannot
   drift out of register. Only opacity and transform animate, so the
   whole thing stays on the compositor. */
.hero {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 100vh;
  min-height: 100svh;
  overflow: hidden;
  background-color: var(--ink);
  isolation: isolate;

  /* Was 11s, which was the dissolve's cycle rather than the drift's. With
     nothing to keep time with, a full zoom in and back out every 11s reads
     as restless; at 22s you notice the frame is alive without watching it
     move. One loop is 22s, so it is 11s from rest out to full scale. */
  --hero-loop: 22s;
}

.hero__media {
  position: absolute;
  inset: 0;
  z-index: -2;
  overflow: hidden;
  transform-origin: 50% 52%;
  will-change: transform;
  animation: hero-drift var(--hero-loop) ease-in-out infinite;
}

.hero__layer {
  position: absolute;
  inset: 0;
  display: block;
}

.hero__layer img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* One still frame, breathing. ease-in-out rather than the site's --ease:
   --ease is a soft-close curve, which is right for a move that arrives and
   stops, but this one turns around and comes back forever — and --ease
   reaches its end fast, so the reversal would land as a visible tick.
   ease-in-out is slow at both ends, so the turn is where it is gentlest. */
@keyframes hero-drift {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.06); }
}

/* Left-weighted scrim: keeps the copy legible over a bright photo
   without turning the image into mud. */
.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    /* left-weighted scrim carrying the copy */
    linear-gradient(100deg,
      rgba(var(--scrim), 0.80) 0%,
      rgba(var(--scrim), 0.72) 30%,
      rgba(var(--scrim), 0.52) 55%,
      rgba(var(--scrim), 0.22) 78%,
      rgba(var(--scrim), 0.04) 100%),
    /* grounds the base of the frame */
    linear-gradient(to top,
      rgba(var(--scrim), 0.40) 0%,
      rgba(var(--scrim), 0) 45%),
    /* the source photo is very bright and very white — a light
       overall wash keeps it warm rather than glaring */
    linear-gradient(rgba(var(--scrim), 0.16), rgba(var(--scrim), 0.16));
}

.hero__inner {
  width: 100%;
  max-width: calc(var(--w-wide) + var(--gutter) * 2);
  margin-inline: auto;
  padding: calc(var(--header-total) + 3rem) var(--gutter) clamp(6rem, 14vh, 9rem);
}

/* Measured in rem, not ch — `ch` here would resolve against the body
   font, not the display font, and crush the headline to ~380px. */
.hero__content {
  max-width: min(64rem, 100%);
  color: var(--paper);
}

.hero__content .eyebrow {
  color: var(--brass-light);
  margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
}

/* Deliberately oversized. The eyebrow and sub-line stay small so the
   jump in scale between them does the talking. */
.hero__title {
  font-size: clamp(3.25rem, 9.6vw, 9rem);
  color: var(--paper);
  margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
  text-wrap: initial; /* the line break is authored, don't let balance move it */
}

/* Block so the headline always lands on two lines, whatever the
   viewport does — "Cabinetry" / "made simple." */
.hero__title-em {
  display: block;
  font-style: italic;
  font-weight: 400;
  color: var(--brass-light);
}

.hero__sub {
  max-width: 30rem;
  font-size: clamp(1.0625rem, 0.9vw + 0.8rem, 1.3125rem);
  font-weight: 400;
  line-height: 1.65;
  color: rgba(250, 247, 241, 0.82);
  margin-bottom: clamp(2.5rem, 5vw, 3.5rem);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}

/* Thin brass rule that draws down once the hero has settled */
.hero__scroll-hint {
  position: absolute;
  left: var(--gutter);
  bottom: 0;
  width: 1px;
  height: clamp(3rem, 7vh, 5rem);
  overflow: hidden;
}

.hero__scroll-line {
  display: block;
  width: 100%;
  height: 100%;
  background: linear-gradient(to bottom,
              rgba(211, 171, 126, 0) 0%,
              rgba(211, 171, 126, 0.9) 100%);
  transform: scaleY(0);
  transform-origin: 50% 0;
  transition: transform var(--dur-slow) var(--ease) 0.9s;
}

.hero.is-loaded .hero__scroll-line { transform: scaleY(1); }


/* ============================================================
   10. SHARED SECTION FURNITURE
   ============================================================ */
.section__head {
  max-width: 52rem;
  margin-bottom: clamp(3rem, 6vw, 5.5rem);
}
.section__head > * + * { margin-top: clamp(1.25rem, 2.5vw, 2rem); }

/* Small uppercase link with an arrow that eases outward on hover */
.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: 0.7em;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--brass-ink);
  padding-bottom: 0.35em;
  border-bottom: 1px solid rgba(166, 124, 82, 0.35);
  transition: color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.link-arrow span {
  transition: transform var(--dur) var(--ease);
}
.link-arrow:hover,
.link-arrow:focus-visible { border-color: var(--brass); }
.link-arrow:hover span,
.link-arrow:focus-visible span { transform: translateX(0.45em); }


/* ============================================================
   11. POSITIONING STATEMENT
   ============================================================ */
/* A deliberate pause. Narrow measure, centred, and considerably more
   air above and below than anything else on the page. The coastline
   mark sits under the quote as an anchor, so the space around it
   reads as composition rather than as a gap. */
.statement {
  position: relative;              /* positioning context for the palms */
  overflow: hidden;                /* they start off-edge; clip the slide-in */
  padding-block: var(--section-y-air);
  text-align: center;
}

/* Keep the words above the brand furniture */
.statement > .container { position: relative; z-index: 1; }

.statement__inner > .eyebrow {
  margin-bottom: clamp(1.75rem, 4vw, 2.75rem);
}

.statement__text {
  max-width: 100%;            /* the narrow container sets the measure */
  margin-inline: auto;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.75rem, 3.8vw, 3.125rem);
  line-height: 1.32;          /* a quote wants more air between lines */
  letter-spacing: var(--headline-tracking);
  text-wrap: balance;
  text-indent: -0.01em;       /* optical centring — nudge off the "B" */
}

/* --- Reveal ---
   Its own observer rather than the site-wide .reveal, so it can fire
   later: the shared one sweeps anything past 92% of the viewport,
   which set this section going while it was still below the fold and
   finished the motion before you ever looked at it.

   Hidden state and target are both declared here, so an element can
   only ever be mid-transition or finished — never popped in. */
.js .statement__item {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 1s var(--ease) var(--statement-delay, 0ms),
              transform 1s var(--ease) var(--statement-delay, 0ms);
  will-change: opacity, transform;
}

.js .statement.is-in .statement__item {
  opacity: 1;
  transform: translateY(0);
}

/* Explicit, not nth-child — the running order is eyebrow, quote,
   palms, then the wave anchoring last. */
.statement__item--eyebrow { --statement-delay: 0ms; }
.statement__item--quote   { --statement-delay: 160ms; }
.statement__item--mark    { --statement-delay: 950ms; }

/* --- The coastline mark ---
   Modest width, generous room either side. */
.statement__mark {
  width: clamp(9rem, 22vw, 15rem);
  margin: clamp(3rem, 7vw, 5.5rem) auto 0;
}

.statement__mark img {
  width: 100%;
  height: auto;
  aspect-ratio: 5.085;        /* intrinsic ratio of coastline.png */
}

/* Once it has arrived, the mark keeps drifting — the float lives on the
   inner <img> so it can't collide with the transform the reveal is
   transitioning on the <figure>. Starts after the reveal has landed. */
.js .statement.is-in .statement__mark img {
  animation: mark-float 4.5s ease-in-out calc(var(--statement-delay, 0ms) + 1s) infinite;
}

/* Five pixels, resting on water. */
@keyframes mark-float {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-5px); }
}

/* --- Palms ---
   A full-bleed overlay pinned behind the words, holding one palm at
   each edge. Flex centring means no magic offsets to keep in sync
   with the artwork's height. */
.statement__frame {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-inline: clamp(0.25rem, 4vw, 5rem);
  pointer-events: none;
}

.statement__palm {
  width: clamp(5rem, 11vw, 9rem);
  height: auto;
  aspect-ratio: 1;              /* trees.png trims to a square */
  opacity: 0;
  /* Mirroring uses `scale`, and the entrance animates `translate` —
     the individual transform properties, so the two never overwrite
     each other the way two `transform` declarations would. Note the
     translate is applied in the parent's space, so it is NOT flipped
     by the scale on the right-hand palm. */
}

.statement__palm--right { scale: -1 1; }

.js .statement.is-in .statement__palm--left {
  animation: palm-in-left 1.15s both 120ms;
}
.js .statement.is-in .statement__palm--right {
  animation: palm-in-right 1.15s both 120ms;
}

/* Glide in on the soft-close curve, overshoot by a few pixels, correct
   once, and stop. One overshoot and one correction — a drawer easing
   shut, not a spring. */
@keyframes palm-in-left {
  0%   { opacity: 0; translate: -72px 0; animation-timing-function: var(--ease); }
  62%  { opacity: 1; translate: 7px 0;   animation-timing-function: ease-in-out; }
  84%  {             translate: -2px 0;  animation-timing-function: ease-in-out; }
  100% { opacity: 1; translate: 0 0; }
}

@keyframes palm-in-right {
  0%   { opacity: 0; translate: 72px 0;  animation-timing-function: var(--ease); }
  62%  { opacity: 1; translate: -7px 0;  animation-timing-function: ease-in-out; }
  84%  {             translate: 2px 0;   animation-timing-function: ease-in-out; }
  100% { opacity: 1; translate: 0 0; }
}

/* Phones: smaller, and dropped back to a watermark so they frame the
   quote rather than crowd it. */
@media (max-width: 760px) {
  .statement__frame { padding-inline: 0; }

  .statement__palm { width: clamp(3.5rem, 17vw, 5.5rem); }

  /* Hang them partly past the edge and drop them well back, so at this
     width they read as ornament at the margins rather than fronds
     sitting under the words. The section clips the overhang. */
  .statement__palm--left  { margin-left: -1.4rem; }
  .statement__palm--right { margin-right: -1.4rem; }

  .js .statement.is-in .statement__palm--left,
  .js .statement.is-in .statement__palm--right { opacity: 0.3; }

  @keyframes palm-in-left {
    0%   { opacity: 0; translate: -48px 0; animation-timing-function: var(--ease); }
    62%  { opacity: 0.3; translate: 5px 0; animation-timing-function: ease-in-out; }
    84%  {             translate: -1.5px 0; animation-timing-function: ease-in-out; }
    100% { opacity: 0.3; translate: 0 0; }
  }
  @keyframes palm-in-right {
    0%   { opacity: 0; translate: 48px 0;  animation-timing-function: var(--ease); }
    62%  { opacity: 0.3; translate: -5px 0; animation-timing-function: ease-in-out; }
    84%  {             translate: 1.5px 0; animation-timing-function: ease-in-out; }
    100% { opacity: 0.3; translate: 0 0; }
  }
}


/* ============================================================
   12. ABOUT / THE PARTNERSHIP
   ============================================================ */
/* Asymmetric and generous: the photo takes just under half the wide
   container and anchors the left, text centred against it on the right. */
.about__grid {
  display: grid;
  grid-template-columns: 47% minmax(0, 1fr);
  gap: clamp(2.5rem, 5vw, 5.5rem);
  align-items: center;
}

/* --- The photograph ---
   Ken Burns settle inside a fixed frame: the <picture> is the frame and
   carries the fade, the <img> scales inside it. Keeping the scale off
   the frame means no layout shift and no overflow, and the warm tint
   below fades in with the photo rather than sitting on bare paper. */
.about__media picture {
  position: relative;
  display: block;
  overflow: hidden;
}

.about__media img {
  width: 100%;
  height: auto;
}

/* Warm treatment. The photo is shot cool and reads grey against the
   cream; this walks it into the palette without touching skin tones.
   Two very light passes rather than one heavy one:
     filter  — a whisper of sepia plus a little saturation
     overlay — a warm brass wash in soft-light, which lifts the warmth
               in the midtones and leaves highlights and shadows alone */
.about__media img {
  filter: sepia(0.10) saturate(1.06) brightness(1.01);
}

.about__media picture::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(160deg,
              rgba(211, 171, 126, 0.30) 0%,
              rgba(166, 124, 82, 0.18) 100%);
  mix-blend-mode: soft-light;
}

.about__caption {
  margin-top: 1.25rem;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
}

.about__body p { color: var(--muted); }
.about__body > * + * { margin-top: clamp(1.25rem, 2.4vw, 1.75rem); }
.about__body .h2 { margin-top: clamp(1.5rem, 3vw, 2.25rem); }

/* Their line, and the whole argument of the site. Give it room. */
.about__pull {
  margin-block: clamp(2rem, 4vw, 3rem) !important;
  padding-left: clamp(1.25rem, 2.5vw, 2rem);
  border-left: 1px solid var(--brass);
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(1.5rem, 2.8vw, 2.125rem);
  line-height: 1.25;
  letter-spacing: var(--headline-tracking);
  color: var(--brass-ink) !important;
}


/* --- Sequenced entrance ---
   Own observer, like the positioning strip, so the shared safety sweep
   can't fire it early. Order: the photo settles, the headline lands as
   three separate statements, the body follows, and the pull-quote
   arrives last on its own beat. */

/* Three beats, ~0.17s apart */
.about__head { display: block; }

.js .about__line {
  display: block;
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.9s var(--ease) var(--about-delay, 0ms),
              transform 0.9s var(--ease) var(--about-delay, 0ms);
}
.js .about.is-in .about__line { opacity: 1; transform: translateY(0); }

.about__line--1 { --about-delay: 260ms; }
.about__line--2 { --about-delay: 430ms; }
.about__line--3 { --about-delay: 600ms; }

/* Everything else in the column */
.js .about__seq {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.9s var(--ease) var(--about-delay, 0ms),
              transform 0.9s var(--ease) var(--about-delay, 0ms);
}
.js .about.is-in .about__seq { opacity: 1; transform: translateY(0); }

.about__seq--eyebrow { --about-delay: 120ms; }
.about__seq--body1   { --about-delay: 800ms; }
.about__seq--body2   { --about-delay: 890ms; }
.about__seq--body3   { --about-delay: 980ms; }
.about__seq--pull    { --about-delay: 1250ms; }   /* its own moment */
.about__seq--link    { --about-delay: 1400ms; }
.about__seq--caption { --about-delay: 700ms; }

/* The photo leads: 1.2s settle from 1.06, fading up as it lands */
.js .about__media picture {
  opacity: 0;
  transition: opacity 1.2s var(--ease);
}
.js .about.is-in .about__media picture { opacity: 1; }

.js .about__media img {
  transform: scale(1.06);
  transition: transform 1.2s var(--ease);
  will-change: transform;
}
.js .about.is-in .about__media img { transform: scale(1); }

@media (max-width: 900px) {
  .about__grid { grid-template-columns: 1fr; }
  .about__media { max-width: 30rem; }
}


/* ============================================================
   13. FEATURED TRANSFORMATION
   ------------------------------------------------------------
   An editorial case study, not a slider. The layout carries the
   argument: the after is large, bright and dominant; the before
   is a small muted inset overhanging its lower-left corner,
   reading as the faded past.

   The whole thing is budgeted to land inside roughly one screen
   on desktop, so you see the headline, the after, the before and
   the review together. That budget is why the head runs as two
   columns and the review lives beside the after rather than
   under it — stacked, those two alone cost ~370px.
   ============================================================ */
/* Extra room at the foot so the inset never runs to the section edge */
.feature {
  padding-block: clamp(2.25rem, 4vw, 3.25rem) clamp(3.5rem, 6vw, 5rem);
}

.feature__head {
  margin-bottom: clamp(0.75rem, 1.6vw, 1.35rem);
}

.feature__head .eyebrow { margin-bottom: clamp(0.9rem, 2vw, 1.4rem); }

/* Headline and intro read as one block, top left */
.feature__head-row .h2 { font-size: clamp(2.25rem, 4.4vw, 3.5rem); }

.feature__head-row .lede {
  max-width: 58ch;              /* two lines, not three — buys image height */
  margin-top: clamp(0.85rem, 1.8vw, 1.35rem);
}

/* --- The stage --- */
.feature__stage {
  display: grid;
  grid-template-columns: minmax(0, 0.38fr) minmax(0, 1fr);
  column-gap: clamp(1.25rem, 2.5vw, 2.5rem);
  align-items: end;
}

.feature__after,
.feature__before { margin: 0; position: relative; }

/* Left column: review above, faded before below, both bottom-aligned
   with the after so the composition sits on one baseline. */
.feature__aside {
  --inset-w: clamp(10rem, 24vw, 21rem);
  grid-column: 1;
  grid-row: 1;
  z-index: 2;                    /* the before overhangs the after */
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: flex-end;         /* review and inset share a column edge */
  gap: clamp(1rem, 2vw, 1.5rem);
  min-width: 0;
}

.feature__after { grid-column: 2; grid-row: 1; }

/* Capped so the hero image can never swallow the screen.

   A definite `height`, NOT `aspect-ratio` + `max-height`: that pairing
   preserves the ratio by shrinking the WIDTH too, so the picture came
   out 557px inside an 877px figure — a 320px dead strip that left the
   AFTER tag floating off the photo. With a fixed height the picture
   fills the column and object-fit does the cropping. */
.feature__after picture {
  position: relative;
  display: block;
  overflow: hidden;
  height: min(75vh, 42rem);
  background: var(--paper);
}

.feature__after img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The archival inset, overhanging into the after's lower-left corner.
   Sized absolutely rather than as a share of the column: at 100% of the
   column it came out ~52% of the after's width, which is not "noticeably
   smaller". This holds it near a third. */
.feature__before {
  width: var(--inset-w);
  margin-right: clamp(-5rem, -5.5vw, -2.5rem);    /* overhang the after */
  box-shadow: 0 24px 50px -30px rgba(21, 58, 71, 0.55);
  outline: clamp(0.4rem, 1vw, 0.7rem) solid var(--paper-alt);
}

.feature__before picture {
  display: block;
  overflow: hidden;              /* clips the bloom's scale */
  background: var(--paper);
}

.feature__before img { width: 100%; height: auto; }

/* Faded past. Applied always — it is the treatment, not the motion,
   so it survives prefers-reduced-motion. */
.feature__before img {
  filter: grayscale(0.3) saturate(0.7) brightness(0.95);
}

/* --- Tags --- */
.feature__tag {
  position: absolute;
  z-index: 2;
  padding: 0.5em 0.9em;
  font-family: var(--font-body);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}

.feature__tag--after {
  top: 0;
  right: 0;
  color: var(--paper);
  background: rgba(21, 58, 71, 0.82);
}

.feature__tag--before {
  top: 0;
  left: 0;
  color: var(--paper);
  background: rgba(21, 58, 71, 0.62);
}

/* --- Placeholder review ---
   Set exactly like the testimonials further down, so dropping the real
   words in is the only change needed. */
.feature__quote {
  width: var(--inset-w);
  margin: 0;
  padding-top: clamp(1rem, 2vw, 1.5rem);
  border-top: 1px solid var(--hairline);
}

/* .stars is clipped to nothing by default and only opened up by
   .reviews-section.is-in — so dropped in here it would stay invisible
   forever. This gives it the feature section's own trigger, and a delay
   matching the quote it sits above so the wipe still lands a beat behind
   its card, exactly as it does in the testimonials. */
.feature__quote .stars {
  --rev-delay: 1150ms;
  --star-h: 0.8rem;
  margin-bottom: clamp(0.75rem, 1.5vw, 1.1rem);
}
.js .feature.is-in .feature__quote .stars { clip-path: inset(0 0 0 0); }

.feature__quote-text {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.125rem, 1.6vw, 1.4375rem);
  line-height: 1.4;
  letter-spacing: var(--headline-tracking);
}

.feature__quote-by {
  margin-top: clamp(0.75rem, 1.5vw, 1.1rem);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--brass-ink);
}

/* --- Sequenced entrance ---
   Own observer. Reading order is deliberate: the copy, then the faded
   before, then the after blooming over the top of it. */
.js .feature__seq {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.9s var(--ease) var(--feature-delay, 0ms),
              transform 0.9s var(--ease) var(--feature-delay, 0ms);
}
.js .feature.is-in .feature__seq { opacity: 1; transform: translateY(0); }

.feature__seq--eyebrow { --feature-delay: 0ms; }
.feature__seq--head    { --feature-delay: 120ms; }
.feature__seq--intro   { --feature-delay: 260ms; }
.feature__seq--quote   { --feature-delay: 1150ms; }

/* The before arrives first, quietly */
.js .feature__before {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.9s var(--ease) 420ms,
              transform 0.9s var(--ease) 420ms;
}
.js .feature.is-in .feature__before { opacity: 1; transform: translateY(0); }

/* Then the after blooms: dimmed and flat at 1.0, brightening to full
   colour as it pushes in slightly. The frame clips the growth, so the
   scale reads as a bloom rather than a resize. */
.js .feature__after picture { opacity: 0; transition: opacity 1.2s var(--ease) 700ms; }
.js .feature.is-in .feature__after picture { opacity: 1; }

.js .feature__after img {
  transform: scale(1);
  filter: brightness(0.82) saturate(0.78);
  transition: transform 1.2s var(--ease) 700ms,
              filter 1.2s var(--ease) 700ms;
  will-change: transform, filter;
}
.js .feature.is-in .feature__after img {
  transform: scale(1.035);
  filter: brightness(1) saturate(1);
}

/* --- Responsive ---
   Below the desktop breakpoint the one-screen budget is dropped; it
   stacks naturally, keeping the overhang so the composition still
   reads as before-tucked-into-after. */
@media (max-width: 860px) {
  .feature { padding-block: var(--section-y); }

  .feature__head-row {
    grid-template-columns: 1fr;
    gap: clamp(1rem, 3vw, 1.5rem);
  }

  .feature__stage {
    grid-template-columns: 1fr;
    row-gap: 0;
  }

  .feature__after { grid-column: 1; grid-row: 1; }
  /* Unwind the desktop fixed-height/absolute-image pairing */
  .feature__after picture { height: auto; }
  .feature__after img { position: static; height: auto; }

  .feature__aside {
    grid-column: 1;
    grid-row: 2;
    align-items: flex-start;
    margin-top: clamp(-4rem, -9vw, -2rem);   /* keeps the overhang */
  }

  .feature__quote { width: 100%; }

  .feature__before {
    order: -1;                                /* image first, then review */
    width: 52%;
    margin-left: 0;
    margin-right: 0;
  }

  .feature__quote {
    width: 100%;
    margin-top: clamp(0.5rem, 2vw, 1rem);
  }
}

@media (max-width: 520px) {
  .feature__before { width: 62%; }
  .feature__tag { font-size: 0.5625rem; padding: 0.45em 0.7em; }
}
/* ============================================================
   14. SERVICES
   ------------------------------------------------------------
   An editorial index rather than a card grid — hairlines, room
   to breathe, name and description in two columns.
   ============================================================ */
/* The rules are pseudo-elements, not borders, so each one can draw
   itself in from the left as its row arrives. Same 1px hairline,
   same positions: one above the list, one under every row. */
.services {
  position: relative;
}

.services::before,
.service::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--hairline);
}

.services::before { top: 0; }
.service::after   { bottom: 0; }

.service {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.7fr);
  gap: clamp(0.75rem, 4vw, 4rem);
  padding-block: clamp(1.75rem, 3.5vw, 2.75rem);
  transition: background-color var(--dur) var(--ease);
}

.service__name {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.5rem, 2.6vw, 2.125rem);
  letter-spacing: var(--headline-tracking);
  line-height: 1.15;
  transition: color var(--dur) var(--ease);
}

.service__desc {
  max-width: 54ch;
  color: var(--muted);
}

.service:hover .service__name { color: var(--brass-ink); }


/* --- Sequenced entrance ---
   Head first, then the rows cascade down the list 0.1s apart, each
   with its own hairline drawing in from the left alongside it. */
.js .svc-seq {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.9s var(--ease) var(--svc-delay, 0ms),
              transform 0.9s var(--ease) var(--svc-delay, 0ms);
}
.js .services-section.is-in .svc-seq { opacity: 1; transform: translateY(0); }

.svc-seq--eyebrow { --svc-delay: 0ms; }
.svc-seq--head    { --svc-delay: 120ms; }
.svc-seq--intro   { --svc-delay: 240ms; }

.js .service {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.85s var(--ease) var(--svc-delay, 0ms),
              transform 0.85s var(--ease) var(--svc-delay, 0ms),
              background-color var(--dur) var(--ease);
}
.js .services-section.is-in .service { opacity: 1; transform: translateY(0); }

/* Each rule draws out from the left with its row */
.js .services::before,
.js .service::after {
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform 0.9s var(--ease) var(--svc-delay, 0ms);
}
.js .services-section.is-in .services::before,
.js .services-section.is-in .service::after { transform: scaleX(1); }

/* The cascade: 0.1s between rows. The top rule leads the first row in
   by a beat so the list opens rather than appearing mid-draw. */
.services::before          { --svc-delay: 360ms; }
.service:nth-child(1)      { --svc-delay: 420ms; }
.service:nth-child(2)      { --svc-delay: 520ms; }
.service:nth-child(3)      { --svc-delay: 620ms; }
.service:nth-child(4)      { --svc-delay: 720ms; }
.service:nth-child(5)      { --svc-delay: 820ms; }
.service:nth-child(6)      { --svc-delay: 920ms; }

@media (max-width: 700px) {
  .service { grid-template-columns: 1fr; gap: 0.75rem; }
}


/* ============================================================
   15. PROCESS
   ============================================================ */
/* The second deliberate pause — four steps laid right across the wide
   container with room between them. Light and calm by design. */
.process {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(2rem, 5vw, 5.5rem);
}

.step {
  padding-top: clamp(1.25rem, 2.5vw, 1.75rem);
  border-top: 1px solid var(--brass);
}

.step__num {
  font-family: var(--font-display);
  font-size: clamp(1.75rem, 3vw, 2.5rem);
  line-height: 1;
  letter-spacing: var(--headline-tracking);
  color: var(--brass);
}

.step__name {
  margin-top: clamp(1rem, 2vw, 1.5rem);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.25rem, 1.9vw, 1.625rem);
  letter-spacing: var(--headline-tracking);
}

.step__desc {
  margin-top: 0.85rem;
  font-size: 0.9375rem;
  color: var(--muted);
}

@media (max-width: 900px) {
  .process { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 520px) {
  .process { grid-template-columns: 1fr; }
}


/* ============================================================
   16. GALLERY
   ------------------------------------------------------------
   A disciplined wall. Every tile is the same 4:3 frame and every
   image covers it, so the grid reads as one curated set rather
   than six different shapes. The sources are a genuine mix —
   three are portrait, one is square — so the crop is the price
   of the discipline; --tile-pos on each tile shifts the window
   where a portrait would otherwise lose its subject.
   ============================================================ */
.gallery {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(0.75rem, 1.6vw, 1.5rem);
}

.gal-tile {
  position: relative;
  overflow: hidden;              /* the hover zoom stays inside the frame */
  aspect-ratio: 4 / 3;
  background: var(--paper-alt);
}

.gal-tile picture {
  display: block;
  width: 100%;
  height: 100%;
}

.gal-tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--tile-pos, 50% 50%);
  transition: transform 1.1s var(--ease);
}

.gal-tile:hover img,
.gal-tile:focus-within img { transform: scale(1.05); }

/* Caption bed. Always present so every tile is labelled without a hover.
   Measured against these photos: several are white cabinetry that runs
   very bright right at the bottom edge, and a lighter bed left captions
   at 2.8:1. This holds ~0.86 alpha through the caption band, which is
   what carries them past AA on every tile. */
.gal-tile__veil {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to top,
              rgba(var(--scrim), 0.94) 0%,
              rgba(var(--scrim), 0.82) 16%,
              rgba(var(--scrim), 0.34) 34%,
              rgba(var(--scrim), 0.06) 52%,
              rgba(var(--scrim), 0) 64%);
}

/* Hover emphasis rides on its own layer, so deepening it on hover never
   trades away the base legibility. */
.gal-tile::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to top,
              rgba(var(--scrim), 0.55) 0%,
              rgba(var(--scrim), 0.10) 45%,
              rgba(var(--scrim), 0) 70%);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}

.gal-tile:hover::after,
.gal-tile:focus-within::after { opacity: 1; }

/* z-index 2: ::after is generated after the children, so without this the
   hover wash would paint over the caption and dim the thing it exists to
   emphasise. */
.gal-tile__cap {
  position: absolute;
  z-index: 2;
  left: clamp(0.85rem, 1.6vw, 1.35rem);
  right: clamp(0.85rem, 1.6vw, 1.35rem);
  bottom: clamp(0.75rem, 1.4vw, 1.15rem);
  font-family: var(--font-body);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--paper);
  transform: translateY(2px);
  transition: transform var(--dur) var(--ease);
}

.gal-tile:hover .gal-tile__cap,
.gal-tile:focus-within .gal-tile__cap { transform: translateY(0); }

.gallery__more { margin-top: clamp(2.5rem, 5vw, 4rem); }


/* --- Sequenced entrance ---
   Own observer, like the rest of the page. Head first, then the
   tiles cascade 0.1s apart in reading order. */
.js .gal-seq,
.js .gal-tile {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.85s var(--ease) var(--gal-delay, 0ms),
              transform 0.85s var(--ease) var(--gal-delay, 0ms);
}

.js .gallery-section.is-in .gal-seq,
.js .gallery-section.is-in .gal-tile { opacity: 1; transform: translateY(0); }

.gal-seq--eyebrow { --gal-delay: 0ms; }
.gal-seq--head    { --gal-delay: 120ms; }

.gal-tile:nth-child(1) { --gal-delay: 300ms; }
.gal-tile:nth-child(2) { --gal-delay: 400ms; }
.gal-tile:nth-child(3) { --gal-delay: 500ms; }
.gal-tile:nth-child(4) { --gal-delay: 600ms; }
.gal-tile:nth-child(5) { --gal-delay: 700ms; }
.gal-tile:nth-child(6) { --gal-delay: 800ms; }

.gal-seq--more { --gal-delay: 950ms; }

/* The tile transform is the reveal's; the zoom belongs to the image
   inside, so the two never fight over the same property. */


/* --- Responsive: 3 → 2 → 1, tiles uniform at every size --- */
@media (max-width: 1000px) {
  .gallery { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 620px) {
  .gallery { grid-template-columns: 1fr; }
  .gal-tile { aspect-ratio: 3 / 2; }   /* a touch wider so six tiles
                                          do not become an endless column */
}
/* ============================================================
   16b. PEAK SECTIONS
   ------------------------------------------------------------
   The two dramatic breaks between the calm paper sections: a
   near-black statement over water, and a full-bleed photograph.
   Both sit outside the width system on purpose — they are the
   moments where the page stops being contained.
   ============================================================ */

/* --- Dark statement --- */
.feature-dark {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 80vh;
  min-height: 80svh;
  padding-block: var(--section-y);
  overflow: hidden;
  isolation: isolate;
  background: var(--ink-deep);   /* the floor: shows if no video ever loads */
  color: var(--paper);
}

/* --- Background water ---
   The footage is bright: light blue with white speculars and a near-white
   sky band. Measured, a 66% scrim alone leaves the brass eyebrow at about
   2:1 — unreadable. So the media is darkened here FIRST, and the scrim
   then sits in the 55–70% range on top of an already-dark image. Both
   passes together are what keeps the words the hero. */
.feature-dark__media {
  position: absolute;
  inset: 0;
  z-index: -2;
  background: url('../images/water-poster.jpg') center / cover no-repeat;
  /* 0.36, not 0.42: measured against the footage, 0.42 left the brass
     eyebrow at 4.38:1 over the brightest speculars — just under AA. */
  filter: brightness(0.36) saturate(0.85);
}

.feature-dark__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;                    /* fades up only once actually playing */
  transition: opacity 1.4s var(--ease);
}

.feature-dark.is-playing .feature-dark__video { opacity: 1; }

.feature-dark__scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: rgba(21, 58, 71, 0.66);
}

.feature-dark > .container { position: relative; z-index: 1; }

.feature-dark__inner { text-align: center; }

/* The default eyebrow brass is tuned for paper (5.0:1) and collapses on
   the deep ink. Reverse to the light brass here. */
.feature-dark .eyebrow { color: var(--brass-light); }

.feature-dark__statement {
  max-width: 18ch;           /* display font — ch is correct here */
  margin: clamp(2rem, 4vw, 3.25rem) auto 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.25rem, 6vw, 4.75rem);
  line-height: 1.12;
  letter-spacing: var(--headline-tracking);
  text-wrap: balance;
  color: var(--paper);
}

/* --- Full-bleed photograph --- */
.divider__media {
  position: relative;
  height: 90vh;
  height: 90svh;
  min-height: 26rem;
  margin: 0;
  overflow: hidden;
  background: var(--ink);
}

.divider__media picture { display: block; height: 100%; }

.divider__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The source is a true 16:9 (1672x941), so on a desktop-shaped frame
     there is almost nothing to crop and Y barely matters. On a portrait
     phone the crop is entirely horizontal, so the bias is pulled left of
     centre to hold the vanity and mirror in frame rather than landing on
     the shower wall. */
  object-position: 32% 50%;
  /* settles as the section arrives, like the hero */
  transform: scale(1.06);
  transition: transform 1.9s var(--ease);
}
.divider__media.is-visible .divider__img { transform: scale(1); }

/* Fade only — a 90vh block sliding up would open a gap beneath it */
.js .divider__media.reveal {
  --reveal-shift: 0px;
  transition-duration: var(--dur-drama);
}

.divider__scrim {
  position: absolute;
  inset: 0;
  /* Deepened for the wider bathroom shot: its lower left is lit marble
     and pale floor tiles, which put the brass eyebrow at 4.15:1. The
     dense band is held past the eyebrow's actual height (~20% up) so the
     upper two thirds of the photo stay clear. */
  background: linear-gradient(to top,
              rgba(var(--scrim), 0.94) 0%,
              rgba(var(--scrim), 0.90) 22%,
              rgba(var(--scrim), 0.52) 38%,
              rgba(var(--scrim), 0.12) 58%,
              rgba(var(--scrim), 0) 72%);
}

.divider__caption {
  position: absolute;
  left: var(--gutter);
  right: var(--gutter);
  bottom: clamp(2.5rem, 6vw, 4.5rem);
  color: var(--paper);
}

.divider__caption .eyebrow { color: var(--brass-light); }

.divider__line {
  margin-top: clamp(0.85rem, 2vw, 1.35rem);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.625rem, 3.4vw, 2.75rem);
  line-height: 1.15;
  letter-spacing: var(--headline-tracking);
}


/* ============================================================
   17. REVIEWS
   ------------------------------------------------------------
   The credibility section, so it earns some warmth: gold stars,
   each review sitting on its own soft card with a faded brass
   quote mark behind it, and a faint warm wash on the section so
   it is not flat cream.
   ============================================================ */

/* Faint warm wash, top-left to bottom-right, over the alt paper.
   Barely there — enough that the section is not one flat fill. */
.reviews-section {
  position: relative;
  background-image:
    radial-gradient(120% 80% at 12% 0%, rgba(166, 124, 82, 0.055) 0%, rgba(166, 124, 82, 0) 60%),
    radial-gradient(90% 70% at 100% 100%, rgba(31, 78, 95, 0.045) 0%, rgba(31, 78, 95, 0) 65%);
}

/* --- Stars ---
   One element, one background image: the five stars are laid out
   inside the SVG, so there is no five-span markup to repeat in
   five places. Sized off --star-h; the 5.9 factor is the SVG's
   own aspect ratio. */
.stars {
  --star-h: 0.85rem;
  display: block;
  width: calc(var(--star-h) * 5.9);
  height: var(--star-h);
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 94 16'%3E%3Cg fill='%23a67c52'%3E%3Cpath d='M8 0l2.472 5.007 5.528.803-4 3.899.944 5.505L8 12.614l-4.944 2.6.944-5.505-4-3.899 5.528-.803z'/%3E%3Cpath transform='translate(19.5)' d='M8 0l2.472 5.007 5.528.803-4 3.899.944 5.505L8 12.614l-4.944 2.6.944-5.505-4-3.899 5.528-.803z'/%3E%3Cpath transform='translate(39)' d='M8 0l2.472 5.007 5.528.803-4 3.899.944 5.505L8 12.614l-4.944 2.6.944-5.505-4-3.899 5.528-.803z'/%3E%3Cpath transform='translate(58.5)' d='M8 0l2.472 5.007 5.528.803-4 3.899.944 5.505L8 12.614l-4.944 2.6.944-5.505-4-3.899 5.528-.803z'/%3E%3Cpath transform='translate(78)' d='M8 0l2.472 5.007 5.528.803-4 3.899.944 5.505L8 12.614l-4.944 2.6.944-5.505-4-3.899 5.528-.803z'/%3E%3C/g%3E%3C/svg%3E")
              left center / contain no-repeat;
}

/* --- Pull quote --- */
.pullquote {
  margin: 0 auto clamp(3.5rem, 8vw, 6rem);
  text-align: center;
}

.pullquote .stars {
  --star-h: 1rem;
  margin: 0 auto clamp(1.5rem, 3vw, 2.25rem);
}

/* The measure lives here, not on the <figure>. `ch` resolves against the
   element's own font, and the figure inherits the body face — putting 18ch
   up there gives ~160px and stacks the quote one word per line. */
.pullquote__text {
  max-width: 18ch;
  margin-inline: auto;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.25rem, 6.5vw, 5rem);
  line-height: 1.1;
  letter-spacing: var(--headline-tracking);
  text-wrap: balance;
}

.pullquote__by {
  margin-top: clamp(1.5rem, 3vw, 2.25rem);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--brass-ink);
}

/* --- Cards --- */
.reviews {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1rem, 2.2vw, 1.75rem);
}

.review {
  position: relative;
  overflow: hidden;                /* keeps the oversized quote mark in */
  margin: 0;
  padding: clamp(1.75rem, 3.2vw, 2.75rem);
  border-radius: 3px;
  background: #fffefb;
  border: 1px solid rgba(166, 124, 82, 0.18);
  box-shadow: 0 18px 40px -32px rgba(21, 58, 71, 0.45);
}

/* Editorial detail: a big faded brass quote mark behind the text.
   aria-hidden by nature (generated content is not announced), and it
   sits at z-index 0 with the content above it. */
.review::before {
  content: '\201C';
  position: absolute;
  /* The glyph sits high in its em box, so a large negative offset puts
     the visible mark above the card's clipped edge. -0.06em lands it
     just inside the top-left, behind the stars and the quote. */
  top: -0.06em;
  left: 0.03em;
  z-index: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(6.5rem, 11vw, 9rem);
  line-height: 1;
  color: rgba(166, 124, 82, 0.13);
  pointer-events: none;
}

.review > * { position: relative; z-index: 1; }

.review .stars { margin-bottom: clamp(1rem, 2vw, 1.4rem); }

.review__quote {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.1875rem, 1.8vw, 1.5rem);
  line-height: 1.45;
  letter-spacing: var(--headline-tracking);
  /* Hang the opening quote into the margin so the text stays flush left */
  text-indent: -0.42em;
}

.review__by {
  margin-top: clamp(1.1rem, 2.2vw, 1.6rem);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--brass-ink);
}

/* --- Trust line ---
   A touch more presence than before, with brass dots between items
   instead of relying on gaps alone. */
.trust {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.6rem 0;
  margin-top: clamp(3rem, 6vw, 5rem);
  padding-top: clamp(2rem, 4vw, 3rem);
  border-top: 1px solid var(--hairline);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink);
  text-align: center;
}

.trust span { display: inline-flex; align-items: center; }

.trust span:not(:last-child)::after {
  content: '';
  width: 4px;
  height: 4px;
  margin-inline: clamp(0.9rem, 2.4vw, 1.6rem);
  border-radius: 50%;
  background: var(--brass);
}

/* --- Sequenced entrance ---
   Head, pull quote, then the cards cascade. The stars wipe in from
   the left as their card lands, so the rating reads as filling. */
.js .rev-seq,
.js .review {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.9s var(--ease) var(--rev-delay, 0ms),
              transform 0.9s var(--ease) var(--rev-delay, 0ms);
}

.js .reviews-section.is-in .rev-seq,
.js .reviews-section.is-in .review { opacity: 1; transform: translateY(0); }

.rev-seq--eyebrow { --rev-delay: 0ms; }
.rev-seq--head    { --rev-delay: 120ms; }
.rev-seq--pull    { --rev-delay: 260ms; }

.review:nth-child(1) { --rev-delay: 520ms; }
.review:nth-child(2) { --rev-delay: 620ms; }
.review:nth-child(3) { --rev-delay: 720ms; }
.review:nth-child(4) { --rev-delay: 820ms; }

.rev-seq--trust { --rev-delay: 980ms; }

/* Stars fill left to right, a beat behind their card */
.js .stars {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 0.8s var(--ease) calc(var(--rev-delay, 0ms) + 260ms);
}
.js .reviews-section.is-in .stars { clip-path: inset(0 0 0 0); }

/* The pull quote's stars inherit their delay from the figure */
.pullquote .stars { --rev-delay: 260ms; }

@media (max-width: 700px) {
  .reviews { grid-template-columns: 1fr; }
}
/* ============================================================
   18. QUOTE / BOOKING FORM
   ============================================================ */
.quote__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(3rem, 6vw, 6rem);
  align-items: start;
}

.quote__intro > * + * { margin-top: clamp(1.25rem, 2.5vw, 2rem); }

.quote__details {
  margin-top: clamp(2.5rem, 5vw, 3.5rem);
  display: grid;
  gap: 1.5rem;
}
.quote__details dt {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--brass-ink);
}
.quote__details dd {
  margin: 0.4rem 0 0;
  color: var(--muted);
}

.form {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.5rem, 3vw, 2.25rem);
  position: relative;
}

.field--full { grid-column: 1 / -1; }

.field label {
  display: block;
  margin-bottom: 0.7rem;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
}

.field input,
.field select,
.field textarea {
  width: 100%;
  padding: 0.65rem 0;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 1rem;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--hairline);
  border-radius: 0;
  transition: border-color var(--dur) var(--ease);
}

.field textarea { resize: vertical; line-height: 1.6; }

.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: none;
  border-bottom-color: var(--brass);
}

/* Custom select chevron — native arrows look nothing like the rest */
.field select {
  -webkit-appearance: none;
  appearance: none;
  padding-right: 1.75rem;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--brass) 50%),
    linear-gradient(135deg, var(--brass) 50%, transparent 50%);
  background-position: right 0.7rem top 1.15rem, right 0.35rem top 1.15rem;
  background-size: 0.35rem 0.35rem, 0.35rem 0.35rem;
  background-repeat: no-repeat;
  cursor: pointer;
}

/* honeypot — off-canvas rather than display:none, which bots skip */
/* The old off-screen honeypot and the JS status note are both gone —
   Netlify's own bot-field replaces the first, and nothing writes to the
   second now that the form submits natively. */

.form__submit {
  grid-column: 1 / -1;
  justify-self: start;
  margin-top: clamp(0.5rem, 1.5vw, 1rem);
}

@media (max-width: 900px) {
  .quote__grid { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
  .form { grid-template-columns: 1fr; }
}


/* ============================================================
   19. FOOTER
   ------------------------------------------------------------
   The page ends on the ink-blue. Everything above it is warm paper,
   so this reads as the floor rather than another section — the site
   closing on the brand colour instead of trailing off into cream.

   Palette inverts: cream body, brass labels, and the crest struck as
   a cream medallion (tools/prep-logo.py) so the navy artwork keeps
   its contrast instead of dissolving into the background.
   ============================================================ */
.site-footer {
  position: relative;
  padding-block: clamp(4.5rem, 9vw, 7.5rem);
  background: var(--ink-deep);
  color: var(--footer-text);
  font-size: 0.9375rem;

  /* Reversed-out equivalents of the page's warm neutrals. Named here
     rather than in :root — nothing else on the site reverses. */
  --footer-text:  rgba(245, 242, 236, 0.78);
  --footer-bright: #f5f2ec;
  --footer-rule:  rgba(245, 242, 236, 0.14);
}

/* A slow lift out of the dark, so the block has depth rather than
   reading as one flat rectangle. */
.site-footer::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(120% 90% at 18% 0%, rgba(31, 78, 95, 0.45), transparent 62%);
}

.site-footer > * { position: relative; }

/* The icon sprite: shapes only, never painted here. */
.site-footer__sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

/* Where we work and Hours are two short lines each; Get in touch carries
   a 28-character email and a 29-character handle. So the columns are not
   equal — the last one is given roughly double the width of the middle
   pair, which is what keeps those two links on one line each. */
.site-footer__grid {
  display: grid;
  grid-template-columns:
    minmax(0, 1fr) minmax(0, 0.72fr) minmax(0, 0.72fr) minmax(0, 1.36fr);
  gap: clamp(2.75rem, 5vw, 4.5rem);
  align-items: start;
}

/* --- The crest ---
   The white colourway sits straight on the ink-blue: no disc, so no rim to
   catch light and nothing to cast a shadow from. Ratio 1.181, and a little
   wider than the old medallion because there is no plate padding it out. */
.site-footer__badge {
  display: block;
  width: clamp(170px, 18vw, 240px);
  height: auto;
  aspect-ratio: 1.181;
}

/* --- Columns ---
   Scoped to .site-footer so it outranks the `.site-footer p` colour
   below; the label is itself a <p>. */
.site-footer .footer-label {
  margin-bottom: 1.15rem;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  /* --brass (#a67c52) is the brass for paper; on this ground it measures
     only 3.26:1, and these are 11px labels. --brass-light is the token the
     site already keeps for exactly this — same brass, lifted to 5.0:1. */
  color: var(--brass-light);
}

.site-footer p { color: var(--footer-text); }

.site-footer__links {
  display: grid;
  gap: 0.85rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-footer__links a {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  line-height: 1.4;
  /* The email and the Instagram handle are long unbroken tokens. Without
     these they refuse to shrink and punch straight out of the column. */
  min-width: 0;
}
.site-footer__links a span {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* Icons pick up the link colour, and the brass hover with it. */
.link-icon {
  flex: none;
  width: 1.0625rem;
  height: 1.0625rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.72;
  transition: opacity var(--dur-fast) var(--ease);
}

.site-footer a {
  color: var(--footer-bright);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
.site-footer a:hover,
.site-footer a:focus-visible { color: var(--brass-light); }
.site-footer a:hover .link-icon,
.site-footer a:focus-visible .link-icon { opacity: 1; }

.site-footer a:focus-visible {
  outline: 2px solid var(--brass-light);
  outline-offset: 4px;
  border-radius: 2px;
}

/* --- Licence / credit bar --- */
.site-footer__base {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 1rem 2rem;
  margin-top: clamp(3.5rem, 7vw, 5.5rem);
  padding-top: clamp(1.75rem, 3vw, 2.5rem);
  border-top: 1px solid var(--footer-rule);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
}
.site-footer__base p {
  margin: 0;
  /* Muted, but 12px still has to clear AA — 0.66 lands at 5.2:1. */
  color: rgba(245, 242, 236, 0.66);
}

/* Tablet — below this the four-across grid squeezes Get in touch under
   ~250px and the email starts breaking mid-word. So: crest on its own
   row, the two short columns paired, contact details full width. */
@media (max-width: 960px) {
  .site-footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .site-footer__brand,
  .site-footer__contact { grid-column: 1 / -1; }
}

/* Phone — one column, centred under the crest.

   Everything here is about length. Stacked into one column the desktop
   spacing ran the footer to 927px on a 390px phone — taller than the
   screen, so it scrolled on and on after the page had already finished.
   The measurements below are all reductions of that: half the padding,
   a crest at a modest ~100px instead of 164px, and the gaps between the
   three blocks cut to roughly a line of text each. */
@media (max-width: 600px) {
  .site-footer {
    padding-block: 3rem 2.5rem;
    font-size: 0.875rem;
  }

  .site-footer__grid {
    grid-template-columns: minmax(0, 1fr);
    gap: 1.5rem;
    justify-items: center;
    text-align: center;
  }

  .site-footer__badge { width: clamp(104px, 30vw, 132px); }

  /* The crest carries its own ring of whitespace, so it needs less help
     than the text blocks do. */
  .site-footer__brand { margin-bottom: 0.25rem; }

  .site-footer .footer-label { margin-bottom: 0.55rem; }

  .site-footer__links {
    gap: 0.6rem;
    margin-top: 0.15rem;
  }
  .site-footer__links a { justify-content: center; }

  .site-footer__base {
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 0.5rem;
    margin-top: 2.25rem;
    padding-top: 1.25rem;
  }
}


/* ============================================================
   20. THANK YOU PAGE
   ------------------------------------------------------------
   thank-you.html — where the quote form lands on success. Same
   tokens as everything else; the only new idea is that the page is
   short, so the footer is pinned to the bottom rather than left
   floating halfway up a tall screen.
   ============================================================ */
.page-thanks {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100svh;
}
.page-thanks .site-footer { margin-top: auto; }

.thanks-top {
  display: flex;
  justify-content: center;
  padding: clamp(2.5rem, 6vw, 4.5rem) var(--gutter) 0;
}

.thanks-top__logo {
  display: block;
  width: clamp(88px, 12vw, 132px);
  line-height: 0;
  transition: opacity var(--dur) var(--ease);
}
.thanks-top__logo:hover { opacity: 0.72; }
.thanks-top__logo img { width: 100%; height: auto; }

.thanks {
  padding-block: clamp(3rem, 8vw, 5.5rem) clamp(4.5rem, 10vw, 8rem);
}

.thanks__inner { text-align: center; }

.thanks__title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  /* A shade under the hero: this is a warm note, not a banner. */
  font-size: clamp(2.5rem, 7vw, 4.25rem);
  line-height: var(--headline-leading);
  letter-spacing: var(--headline-tracking);
  text-wrap: balance;
}

.thanks__lede {
  margin: clamp(1.25rem, 3vw, 1.75rem) auto 0;
  max-width: 32rem;
}

/* --- What happens next --- */
.thanks__next {
  margin-top: clamp(3.5rem, 8vw, 5.5rem);
  padding-top: clamp(2rem, 4vw, 3rem);
  border-top: 1px solid var(--hairline);
}

/* Counter rather than a real list marker: the numeral wants to be brass
   and set in the display face, which `list-style` cannot do. */
.thanks__steps {
  counter-reset: step;
  display: grid;
  gap: clamp(1.5rem, 3vw, 2rem);
  margin: clamp(1.75rem, 4vw, 2.5rem) auto 0;
  max-width: 30rem;
  padding: 0;
  list-style: none;
  text-align: left;
}

.thanks__steps li {
  counter-increment: step;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.9rem;
  align-items: baseline;
  color: var(--muted);
}

.thanks__steps li::before {
  content: counter(step);
  font-family: var(--font-display);
  font-size: 1.125rem;
  line-height: 1;
  color: var(--brass-ink);
  /* Holds the numerals in a column so the copy edges line up. */
  min-width: 1.1em;
}

/* --- Sign-off --- */
.thanks__signoff {
  margin: clamp(3.5rem, 8vw, 5rem) 0 0;
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 2.4vw, 1.625rem);
  line-height: 1.5;
  color: var(--ink);
}
.thanks__signoff em {
  font-style: italic;
  /* The names sit a touch larger than the line above them. */
  font-size: 1.12em;
}

/* --- The coastline, as a full stop --- */
.thanks__mark {
  width: clamp(7rem, 18vw, 11rem);
  margin: clamp(2rem, 4vw, 3rem) auto 0;
}
.thanks__mark img { width: 100%; height: auto; }

/* --- Onward --- */
.thanks__actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(1.5rem, 3vw, 2rem);
  margin-top: clamp(3rem, 7vw, 4.5rem);
}

@media (min-width: 601px) {
  .thanks__actions {
    flex-direction: row;
    justify-content: center;
    gap: clamp(2rem, 4vw, 3rem);
  }
}

@media (max-width: 600px) {
  /* Full-width tap target, same as the hero's CTA. */
  .thanks__actions .btn { width: 100%; padding-block: 1.25em; }
  .thanks__steps { text-align: left; }
}


/* ============================================================
   RESPONSIVE
   ------------------------------------------------------------
   Most traffic arrives from Facebook on a phone. The hero has to
   land there first: big headline, scrim that actually covers the
   text column, comfortable tap targets.
   ============================================================ */

/* Tablet & below — collapse the nav to a hamburger, but KEEP the quote
   button in the header. Most visitors arrive here from Facebook on a
   phone, and burying the one action they came for behind a menu costs
   more than the space it saves. It just shortens its label. */
@media (max-width: 900px) {
  .nav { display: none; }

  .nav-toggle { display: inline-flex; }

  /* --logo-w is not overridden here or on phones any more. Those overrides
     were widths for the old wordmark, which was 2.6x wider than it was
     tall; applied to a square crest they set its *height* too and it
     outgrew the bar. The base clamp already tracks the header down. */

  .site-header__cta {
    margin-left: auto;
    /* 1.5em block padding on an 11px label lands the button at 44px —
       the minimum comfortable tap target. */
    padding: 1.5em 1.2em;
    font-size: 0.6875rem;
    letter-spacing: 0.12em;
  }
  .site-header__cta .cta-long  { display: none; }
  .site-header__cta .cta-short { display: inline; }

  .nav-toggle { margin-left: 0.35rem; }

  /* --- Solid bar, always ---
     The transparent-over-hero treatment is a desktop luxury: there, the
     header clears the copy and only ever floats over the hero photo. On
     a phone the bar is 72px of a 844px screen and everything scrolls
     right under it, so headings and images ghost through the gradient.
     Below 900px it is simply an opaque cream bar from the first frame. */
  .site-header {
    background-color: var(--paper);
    color: var(--ink);
    box-shadow: var(--shadow-header);
  }

  /* The photo scrim and the reversed-out logo both exist only to carry a
     transparent bar over the hero. With a cream bar they invert the mark
     to white on white. */
  .site-header::before { opacity: 0; }
  .site-header:not(.is-scrolled):not(.is-nav-open) .logo img { filter: none; }

  .site-header__cta { border-color: var(--brass); }

  /* The nav overlay is the same cream and sits *under* the header, so the
     shadow would rule a hard line across an otherwise seamless panel. */
  .site-header.is-nav-open {
    background-color: var(--paper);
    box-shadow: none;
  }
}

/* Phone */
@media (max-width: 600px) {
  :root {
    --header-h: 72px;
    --reveal-shift: 24px;
  }

  .site-header__inner { gap: 0.5rem; }

  .hero {
    align-items: flex-end;
  }

  .hero__inner {
    padding-bottom: clamp(4rem, 12vh, 6rem);
  }

  .hero__content { max-width: none; }

  /* Vertical scrim on phones: the copy sits low, so the coverage has to
     come from the bottom rather than the left. Weighted heavier and with
     a smoother ramp than it was — the pantry dissolves in near-white, and
     the old curve thinned out right where the sub-line sits. Every line
     of copy now sits over at least 0.70, which clears AA on white. */
  .hero__scrim {
    background:
      linear-gradient(to top,
        rgba(var(--scrim), 0.92) 0%,
        rgba(var(--scrim), 0.86) 28%,
        rgba(var(--scrim), 0.70) 52%,
        rgba(var(--scrim), 0.38) 76%,
        /* not 0.12: this photo runs bright white right to the top of the
           frame, and too thin a top stop leaves the upper cabinets glaring
           above the copy rather than settling behind it. */
        rgba(var(--scrim), 0.20) 100%),
      linear-gradient(100deg,
        rgba(var(--scrim), 0.30) 0%,
        rgba(var(--scrim), 0.05) 70%);
  }

  .hero__title {
    /* Stay large and legible — the headline is the whole message. */
    font-size: clamp(3rem, 15vw, 4.75rem);
  }

  .hero__sub { max-width: 22rem; }

  .hero__actions .btn {
    width: 100%;
    padding-block: 1.25em;
  }

  .hero__scroll-hint { display: none; }

  /* --- Trust line ---
     Wrapping a flex row left each item's separator dangling at the end of
     its line, so the list read as three broken-off fragments. On a phone
     it becomes a proper stack and the dots go entirely — the spacing is
     doing that job now. */
  .trust {
    flex-direction: column;
    gap: 0.85rem;
  }
  .trust span:not(:last-child)::after { display: none; }
}

/* --- Hero focal point in a portrait window ---
   A 16:9 photo in a portrait frame keeps only a narrow vertical slice of
   itself — about a quarter of its width on a phone. `center` is not a
   neutral choice there, it is just wherever the middle happens to fall.

   Pulled left onto the run of cabinetry: upper doors across the top, the
   splashback, then the benchtop and drawer fronts carrying the bottom of
   the frame. The right of the photograph is the corner return, and framing
   on that put the oven and cooktop front and centre — appliances rather
   than the joinery this is meant to be selling.

   25% rather than hard left: at 0% the crop runs off the cabinetry into
   the bare wall and the framed print beside it. Past ~30% the black
   dishwasher pushes into the bottom corner.

   Keyed to aspect rather than width because that is the actual cause —
   this catches portrait tablets, which crop nearly as hard, and leaves
   every landscape window on the full frame. */
@media (max-aspect-ratio: 4 / 5) {
  .hero__layer img { object-position: 25% center; }
}

/* Short landscape phones — don't let the hero overflow */
@media (max-height: 520px) and (orientation: landscape) {
  .hero { min-height: 620px; }
  .hero__title { font-size: clamp(2.25rem, 6vw, 3rem); }
}


/* ============================================================
   11. REDUCED MOTION
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
  }

  .js .reveal {
    opacity: 1;
    transform: none;
  }

  /* No drift — the photograph, held still. `animation: none` matters here:
     the blanket rule above only forces animation-duration to 0.01ms, which
     would run the loop instantly and park the frame on a keyframe rather
     than stopping it. */
  .hero__media {
    animation: none;
    transform: none;
  }

  .hero__scroll-line { transform: scaleY(1); }

  /* Positioning strip: shown, still. The compound selectors have to
     match the specificity of the rules that set the motion, or those
     simply outrank `animation: none` / `transform: none` and keep
     running for someone who asked for neither. */
  .js .statement__item,
  .js .statement.is-in .statement__item {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .js .statement.is-in .statement__mark img {
    animation: none;
    transform: none;
  }
  /* Palms: present and still. `translate: none` matters as well as
     `animation: none` — the keyframes drive the individual translate
     property, which `transform: none` would not reset. */
  .statement__palm,
  .js .statement.is-in .statement__palm--left,
  .js .statement.is-in .statement__palm--right {
    opacity: 1;
    animation: none;
    translate: none;
  }
  @media (max-width: 760px) {
    .statement__palm,
    .js .statement.is-in .statement__palm--left,
    .js .statement.is-in .statement__palm--right { opacity: 0.3; }
  }

  /* About: everything present and still. The warm treatment on the
     photo is deliberately NOT reset here — it is colour, not motion. */
  .js .about__line,
  .js .about.is-in .about__line,
  .js .about__seq,
  .js .about.is-in .about__seq {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .js .about__media picture,
  .js .about.is-in .about__media picture { opacity: 1; }
  .js .about__media img,
  .js .about.is-in .about__media img { transform: none; }

  /* Featured transformation: final state, no bloom. The before keeps
     its muted filter — that is the treatment, not the motion. */
  .js .feature__seq,
  .js .feature.is-in .feature__seq,
  .js .feature__before,
  .js .feature.is-in .feature__before {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .js .feature__after picture,
  .js .feature.is-in .feature__after picture { opacity: 1; transition: none; }
  /* `transition: none` explicitly, so the end state is reached outright
     rather than relying on the blanket 0.01ms duration above. */
  .js .feature__after img,
  .js .feature.is-in .feature__after img {
    transform: none;
    filter: none;
    transition: none;
  }

  /* Services: shown, still, hairlines already drawn. */
  .js .svc-seq,
  .js .services-section.is-in .svc-seq,
  .js .service,
  .js .services-section.is-in .service {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .js .services::before,
  .js .service::after,
  .js .services-section.is-in .services::before,
  .js .services-section.is-in .service::after {
    transform: none;
    transition: none;
  }

  /* Dark section: static poster only, no video. main.js also never
     loads it under reduced motion, so this is belt and braces. */
  .feature-dark__video { display: none; }

  /* Gallery: tiles shown, no cascade, no hover zoom. */
  .js .gal-seq,
  .js .gal-tile,
  .js .gallery-section.is-in .gal-seq,
  .js .gallery-section.is-in .gal-tile {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .gal-tile img,
  .gal-tile:hover img,
  .gal-tile:focus-within img { transform: none; transition: none; }

  /* Reviews: cards shown, stars already filled, no cascade. */
  .js .rev-seq,
  .js .review,
  .js .reviews-section.is-in .rev-seq,
  .js .reviews-section.is-in .review {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .js .stars,
  .js .reviews-section.is-in .stars {
    clip-path: none;
    transition: none;
  }

  .divider__img { transform: none; }
}
