/* =============================================================================
   base.css â€” the fluid foundation. Copy into every site as css/base.css.
   Nothing here is width-locked: type, spacing and layout scale continuously
   from ~360px phones to ultrawide. This file is why sites are dynamic off the bat.
   ============================================================================= */

/* â”€â”€ Brand faces (files supplied in /fonts) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   ASSUMED ROLES: Sunshine = display (headings), Montserrat = body + UI. Say the word
   if that is the wrong way round. TODO convert both to .woff2 before launch: it is
   roughly a third of the payload of .ttf for identical rendering. */
@font-face {
  font-family: "Sunshine";
  src: url("../fonts/Sunshine-Regular.ttf") format("truetype");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Sunshine";
  src: url("../fonts/sunshine-Italic.ttf") format("truetype");
  font-weight: 400; font-style: italic; font-display: swap;
}
@font-face {
  font-family: "Montserrat";
  src: url("../fonts/Montserrat-VariableFont_wght.ttf") format("truetype-variations");
  font-weight: 100 900; font-style: normal; font-display: swap;   /* variable: one file, every weight */
}

:root {
  /* COLOURS â€” the brand palette from the supplied swatch row.
     --sage is sampled straight out of the logo's wave mark, so it is exact. The other
     four are read off the swatch screenshot and may be a shade out: confirm the hexes. */
  --brand:      #2A5045;   /* dark forest green, primary */
  --brand-dark: #1D382F;   /* derived: hovers, footer */
  /* CLIENT-SPECIFIED, 2026-08-24. Was #91AFA0, which was sampled exactly out of the wave
     mark in logo.png — so the logo art is still the OLD lighter sage and no longer matches
     this token. The logo needs re-exporting in #507663, or the lighter green has to be
     accepted as a separate brand element. Flagged, not resolved.
     Luminance dropped .392 -> .156, which INVERTS several contrast decisions: sage on white
     goes 2.37:1 -> 5.11:1 (a large gain, and most of the site is light), but sage on --brand
     goes 3.79:1 -> 1.76:1 and on a dark scrim 8.84:1 -> 4.11:1. Every place sage sat on a
     dark background was re-pointed at a light colour instead; search --focus-ring and the
     .menu__nav / .sec-bleed / .ty__panel eyebrow rules for those. Do not put this colour
     back on a dark ground without re-measuring. */
  --sage:       #507663;
  --accent:     var(--sage);
  --muted:      #6B7A72;   /* derived: secondary text */
  --surface:    #F4F4F3;   /* off-white section background */
  --mist:       #E1DFDF;   /* CONFIRMED by the client. Was #E0DFDF by eye, 1/255 out. */
  --ink:        #000000;   /* body text and the logo wordmark */
  --white:      #ffffff;

  /* TYPE â€” fluid scale. Never set a size that only looks right at one width. */
  --font:         "Montserrat", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;   /* body */
  --font-display: "Sunshine", Georgia, serif;                                               /* headings */
  --font-ui:      "Montserrat", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;   /* eyebrows, buttons, nav, forms */
  --t-header: clamp(48px, 7vw,   88px);
  --t-hero:   clamp(30px, 2.9vw, 54px);   /* hero heading: sized so the longest line is ~22vw, as drawn */
  --t-h1:     clamp(40px, 5.4vw, 70px);
  --t-h2:     clamp(30px, 3.6vw, 46px);
  --t-h3:     clamp(24px, 2.6vw, 31px);
  --t-lg:   22px;                         /* only .legal h3. Moved with --t-body so it stays
                                             a step above it rather than 2px from it. */
  --t-body: 18px;                         /* was 16px. Only `body` reads this, so it sets the
                                             floor everything inherits: legal pages, footer
                                             meta, list items, anything with no rule of its own.
                                             The real prose blocks each set their own size —
                                             see --t-copy below and the notes on .heritage__cols,
                                             .art__body, .art__standfirst and .ty__note. */
  /* Shared body-copy step, +2px on the 13/15px it was. Four rules had this exact clamp
     written out separately (.prose p, .welcome__copy p, .live__ask, .post__excerpt), which is
     why it is a token now — the next size change is one line, not four.
     The vw term moved .92 -> 1.06 to KEEP THE FLUID BAND WHERE IT WAS. .92vw reached 13px at
     1413px and 15px at 1630px, so the copy was pinned to its min below 1413px and its max
     above 1630px; 1.06vw hits 15px at 1415px and 17px at 1604px, the same window. Adding 2px
     to the ends without moving the vw term would have shifted that window instead. */
  --t-copy: clamp(15px, 1.06vw, 17px);
  --t-small: 11px;
  --t-cta:   12px;

  /* SPACING â€” gutters and section rhythm breathe with the viewport. */
  --wrap: 1240px;                             /* max content width; page itself is fluid */
  --pad:  clamp(20px, 5vw, 64px);             /* fluid side padding */
  --section-y: clamp(56px, 8vw, 128px);       /* vertical section rhythm */
  --measure: 68ch;                            /* readable line-length cap */

  --nav-h: 68px;
  --radius: 10px;
  --ease: cubic-bezier(.22,.61,.36,1);

  /* HERO BANNER geometry. These three drive the whole overlap, so the interlock can be
     dialled without touching markup:
       --hero-inset  side margin left/right of the image band
       --hero-band   white gap between the masthead and the top of the image
       --hero-lift   how far the heading rides up alongside the logo (0 on mobile)

     How they interact, measuring down the page:
       heading line 1 top = masthead-padding + logo-height - lift
       image top          = masthead-padding + logo-height + band
       "living" overhang  = 2 line-heights - lift - band
     That last one is the one to watch: NEGATIVE means "living" pokes out above the
     image, 0 means it sits exactly on the top edge. All three values are tuned so it
     stays at roughly 0 from 760px up to 1920px, because they scale on the same vw. */
  --hero-inset: clamp(10px, 1.7vw, 28px);
  /* 0: the heading sits entirely INSIDE the image now, so there is no white gap above it to
     reserve. It used to hold lines 1-2 of the title above the photograph. */
  --hero-band:  0px;
  /* How far the heading is inset from the image's top-right corner, now that it lives on the
     photograph rather than half above it. */
  --hero-copy-in: clamp(22px, 3.4vw, 58px);
  /* 0 since the masthead became permanently opaque. This used to lift the heading UP into
     the bar, which worked while the bar was transparent and the heading sat on white. On
     the dark glass bar it overlapped by up to 60px and sage measured 2.31:1 against it —
     failing even for large text. Restore a value only if the bar goes transparent again. */
  --hero-lift:  0px;

  /* Distance from the viewport's right edge to the .wrap content edge, i.e. exactly where
     the Menu toggle's right edge falls. Anything that has to line up with the masthead
     aligns to this instead of guessing a percentage. */
  --edge: max(var(--pad), calc((100vw - var(--wrap)) / 2 + var(--pad)));

  /* FULL-PAGE MENU. The panel's right edge is anchored to --edge and overhangs the text
     line by --menu-strip-overhang. Its width is clamped rather than a bare 30%: the right
     edge stops travelling once --wrap caps out, so an uncapped percentage would keep
     growing leftward and the panel would read far too wide on a large monitor. */
  --menu-strip-w:        clamp(300px, 30%, 470px);
  --menu-strip-overhang: clamp(18px, 2vw, 34px);

}

/* â”€â”€ Minimal reset â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; -webkit-tap-highlight-color: transparent; scroll-behavior: smooth; }
body {
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: 1.6;
  color: var(--ink);
  background: var(--white);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;                         /* body never gets a horizontal scrollbar */
  overflow-wrap: break-word;                  /* long words wrap instead of overflowing (else clipped by overflow-x) */
}
img, picture, svg, video { display: block; max-width: 100%; height: auto; }
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; }
/* --focus-ring, not --accent directly: the new sage is 5.11:1 on white but 1.76:1 on the
   --brand menu panel, well under the 3:1 that WCAG 1.4.11 wants for a non-text indicator.
   The dark surfaces that hold focusable elements re-point it at white below. */
:focus-visible { outline: 2px solid var(--focus-ring, var(--accent)); outline-offset: 2px; }
/* The menu overlay (nav links on the --brand panel), the hero (kicker CTA and the ambient
   toggle, both over the photograph) and the thank-you banner (its --light CTA). */
.menu, .hero, .ty { --focus-ring: var(--white); }

/* â”€â”€ Typography scale â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
h1, h2, h3, h4 { font-family: var(--font-display); }
h1 { font-size: var(--t-h1); line-height: 1.05; font-weight: 400; }
h2 { font-size: var(--t-h2); line-height: 1.12; font-weight: 400; }
h3 { font-size: var(--t-h3); line-height: 1.2;  font-weight: 400; }
p  { max-width: var(--measure); }
/* Unbreakable tokens (URLs, emails, long single words) break instead of running off narrow screens. */
a, h1, h2, h3, h4, h5, h6, td, li { overflow-wrap: anywhere; }
/* 14px, NOT --t-cta (12px), which it used to share. Deliberately its own value: --t-cta is
   also the size of .btn, the masthead's "Menu" label and the form's consent line, and none of
   those were meant to grow with the section eyebrows. Fixed rather than a clamp, matching
   --t-cta's own convention — the small UI sizes on this site do not scale with the viewport.
   .18em of tracking rides along at the new size, so the letter-spaced look holds without
   retuning. 12px -> 14px -> 16px over two passes. The section titles now share this exact
   treatment at larger sizes (see the ALL-CAPS block near .sec-title), so a change here is a
   change to the whole title system, not just the small labels. */
/* ── ONE LABEL STYLE: .eyebrow and every section title are the same thing ──────────
   They are literally one declaration, so they cannot drift. Change the size here and both
   the small labels and the section titles move together — that is the point.

   This replaced a scale of display-serif titles running 17px to 40px. The section titles are
   now the SAME 16px as the eyebrows, which is a deliberate flattening, not an oversight:
   hierarchy comes from position and the .wipe sweep rather than from size. Two consequences
   worth knowing before "fixing" anything here:
     - Where an eyebrow sits directly above one of these titles (the homepage Location block,
       /lifestyle's split sections) the two now read as two identical label lines.
     - Body copy is --t-copy, 15-17px, so a title is no longer larger than the prose under it.

   No line-height: these inherit body's 1.6, exactly as .eyebrow always did. The titles used
   to set 1.18-1.45 for display sizes and those are gone with the sizes.

   The display serif is now reserved for the long statement headings — .welcome__title,
   .amen__title, .club__title, .hero__title — which stay sage Sunshine. */
.eyebrow,
.heritage__title,
.live__title,
.loc__title,
.contact__title,
.forest__title,
.sec-title {
  font-family: var(--font-ui); font-size: 16px; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase; color: var(--ink);
}

/* â”€â”€ Layout helpers â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--pad); }
.section { padding-block: var(--section-y); }
.section--surface { background: var(--surface); }
.measure { max-width: var(--measure); }
.stack > * + * { margin-top: 1rem; }
.center { text-align: center; margin-inline: auto; }

/* Auto-fit grid that never overflows: cells wrap when they can't hold min width. */
.grid { display: grid; gap: clamp(16px, 3vw, 32px); }
.grid--auto { grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); }

/* Wide content (tables/code/diagrams) scrolls inside its own box, never the page. */
.scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* â”€â”€ Buttons â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: .5em; padding: .9em 1.6em; border: 1px solid transparent; border-radius: var(--radius);
  font-family: var(--font-ui);
  font-size: var(--t-cta); letter-spacing: .08em; text-transform: uppercase; font-weight: 700;
  transition: background .2s var(--ease), color .2s var(--ease), transform .2s var(--ease);
}
.btn:active { transform: translateY(1px); }
.btn-fill    { background: var(--brand); color: var(--white); }
.btn-fill:hover  { background: var(--brand-dark); }
.btn-outline { background: transparent; color: var(--brand); border-color: currentColor; }
.btn-outline:hover { background: var(--brand); color: var(--white); }

/* â”€â”€ Masthead: logo left, MENU right, in flow on a white band â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Deliberately NOT a stacking context: the hero heading lifts up into this band with a
   negative margin, and needs to paint over it (it comes later in the DOM). Adding
   position/z-index here would bury the first heading line behind the masthead. */
.masthead__bar {
  /* align-items: center in BOTH states. The oldest version seated "Menu" high against a
     tall logo with lopsided padding; the logo is small enough in either state now that
     centring the two on each other is right at rest and while stuck. Padding does not
     change on scroll either — the height change comes from --logo-w alone. */
  display: flex; align-items: center; justify-content: space-between; gap: clamp(16px, 2.4vw, 40px);
  padding-block: clamp(8px, 1vw, 14px);
}
/* The sticky bar now covers the top of the page, so in-page anchors have to stop clear of
   it or the section heading lands underneath. Sized to the stuck bar's height. */
section[id], [id="main"] { scroll-margin-top: clamp(76px, 8vw, 112px); }
/* FIXED, with a constant-height spacer holding its place in flow. Historically it was
   sticky and shrank on scroll, which shortened the document, made the browser scroll-anchor
   to compensate, and oscillated. It has a single state now and is out of flow, so neither
   problem can recur.
   z-index sits above the page but below the menu overlay (60), which .menu-active raises it
   past so the logo and toggle stay on top while the menu is open. */
.masthead {
  position: fixed; top: 0; left: 0; right: 0; z-index: 50;
  /* RESTING state: opaque white, dark logo, ink text, logo at full size. .is-stuck below
     swaps all four once the page scrolls. Opaque rather than transparent because the hero
     heading no longer lifts into the bar — it sits inside the photograph — so the bar has
     nothing to get out of the way of.
     No backdrop-filter at rest: the bar is opaque, so blurring behind it costs a compositor
     layer and changes nothing. The stuck bar is translucent and does use one.
     The shadow is deliberately lighter than the stuck bar's: on white it only has to
     separate the bar from the page, not carry a colour change as well. */
  --logo-w: clamp(56px, 5.4vw, 83px);
  background: var(--white);
  box-shadow: 0 1px 14px rgba(0, 0, 0, .07);
  transition: background .3s var(--ease), backdrop-filter .3s var(--ease),
              box-shadow .3s var(--ease);
}
/* Sized to the RESTING bar, which is the taller of the two: padding both sides plus the logo
   at its 181/228 aspect. It never changes, which is the whole point — the bar is out of flow,
   so .is-stuck shrinking it moves no layout at all and there is nothing for the browser's
   scroll anchoring to compensate for. Sizing this to the STUCK height instead would reintroduce
   the old oscillation. */
.masthead__spacer { height: calc(2 * clamp(8px, 1vw, 14px) + clamp(56px, 5.4vw, 83px) * .79386); }
.menu-active .masthead { z-index: 70; }

/* ── SCROLLED state: dark glass strip, logo 15% smaller, everything reversed out ─────
   Toggled by .is-stuck from main.js on a two-threshold scroll check. Dark because the logo
   reverses to white here, so the bar has to carry it.
   The bar shrinks purely by shrinking --logo-w; padding-block is identical in both states.
   Bar height is (2 * padding) + max(--logo-w * .79386, 44), the 44 being .masthead__menu's
   min-height tap target. That max() is why the shrink is viewport-dependent:
       >= 1260px   93.9 -> 84.0px  (-10.5%)  burger is display:none, the logo alone sets it
       ~1180px     74.2 -> 67.6px  ( -8.9%)  burger starts absorbing part of the shrink
       <= 1040px   60.5 -> 60.0px  ( -0.8%)  resting logo is already at the 44px floor, so
                                             the logo still shrinks but the bar cannot
   Deliberate: the tap target outranks the effect. To make the bar shrink on phones too,
   give .is-stuck a tighter padding-block here — that is the only knob it needs. */
.masthead.is-stuck {
  /* 85% of the resting clamp, term by term. Written out rather than calc()'d off a single
     variable so the three breakpoints stay legible: 56->47.6, 5.4->4.59, 83->70.55. */
  --logo-w: clamp(47.6px, 4.59vw, 70.55px);
  /* --brand at 82%. Below about 0.80 the bar composites too pale over white page content and
     white text drops under 4.5:1 (0.72 measured at 4.22:1). Still reads as glass. */
  --masthead-glass: rgba(42, 80, 69, .82);
  background: var(--masthead-glass);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  box-shadow: 0 2px 18px rgba(0, 0, 0, .16);
}
/* Reverse everything out on the dark bar. The light logo is the second stacked <img> that
   already exists for the menu overlay, so this is the same cross-fade, not a new asset. */
.masthead.is-stuck .masthead__logo img + img { opacity: 1; }
.masthead.is-stuck .masthead__menu { color: var(--surface); }
.masthead.is-stuck .masthead__menu:hover { color: var(--white); }

/* At rest the DARK logo is the default. The light one is the second stacked <img>, shown by
   .is-stuck above and by the menu overlay below. */
/* While the menu is open the bar sits on the overlay, so the glass would read as a seam —
   and so would a shadow under a bar that has no background, hence box-shadow too. Equal
   specificity with .masthead.is-stuck above, and this comes later, so it wins in both states. */
.menu-open .masthead {
  background: none; -webkit-backdrop-filter: none; backdrop-filter: none; box-shadow: none;
}

.masthead__logo { display: block; flex: none; position: relative; }
/* Width comes from --logo-w so the stuck state can shrink it without redeclaring the clamp.
   Shrinking it shortens the bar but moves NOTHING else: the bar is fixed and .masthead__spacer
   holds a constant height in flow, so the hero and every section below stay put. It used to
   pull the hero up with it, back when the bar was in flow. */
.masthead__logo img { width: var(--logo-w); height: auto; display: block; transition: width .3s var(--ease); }
/* The light logo sits exactly on top of the dark one and cross-fades in. */
.masthead__logo img + img { position: absolute; top: 0; left: 0; opacity: 0; transition: opacity .35s var(--ease); }
.menu-open .masthead__logo img + img { opacity: 1; }

.masthead__menu {
  /* Symmetrical padding: the bar centres its children in both states, so the old lopsided
     padding that seated "Menu" high against a tall logo would push it off centre.
     min-height keeps the 44px tap target. Ink at rest on the white bar; .is-stuck above
     reverses it to --surface on the dark one. */
  display: inline-flex; align-items: center; gap: .75em;
  background: none; border: 0; padding: 0; min-height: 44px; color: var(--ink);
  font-family: var(--font-ui); font-size: var(--t-cta); font-weight: 500;
  letter-spacing: .22em; text-transform: uppercase;
  transition: color .35s var(--ease);
}
.masthead__menu:hover { color: var(--brand); }
.menu-open .masthead__menu { color: var(--surface); }
/* White, NOT --brand: .menu__strip behind this is --brand, so a brand hover disappears into
   the panel. It also has to match .masthead.is-stuck's hover, which is 3 classes against this
   rule's 2 and wins whenever the menu is opened from a scrolled position. */
.menu-open .masthead__menu:hover { color: var(--white); }

/* Two 1px lines, 5px apart: total 7px, so the centres sit at 0.5 and 6.5 and the midpoint
   is 3.5. Moving each by 3px lands them both on that midpoint, where they rotate into an X. */
/* ── Desktop inline nav: the links in the bar instead of behind the burger ─────────
   Two bands (client call, 2026-09-08: the burger arrived far too early — iPad landscape
   and small desktops must still show the menu):

     1024-1339px  COMPACT: same nav, 11.5px type and tighter gaps, and the bar breaks out
                  of .wrap to align with the HERO IMAGE instead — logo on its left edge,
                  the Contact Us chip on its right edge (padding-inline: --hero-inset).
                  Fits nine labels down to 1024 (iPad landscape) with ~75px spare.
     1340px+      ROOMY: 12.5px type inside the .wrap column (was 13px for eight labels;
                  see the note on the gap/size below).

   The original seven labels were MEASURED with GD: 755px of glyphs + tracking at
   13px/0.035em, and the nav as a whole (labels + 6 gaps + the CTA chip's padding and
   margin) needed 968px beside the logo, which made 1260px the breakpoint with 66px spare.
   "Gallery" (added 2026-09-07) is ~50px of glyphs + one more 30px gap, which lands the
   same margin at 1340px — hence the compact band below it. Re-measure whenever a label is
   added or lengthened, or the size/tracking/gap changes. There is no ellipsis and nothing
   scrolls: an overflow simply wraps the bar. */
.masthead__nav { display: none; }
@media (min-width: 1024px) {
  .masthead__nav {
    display: flex; align-items: center; flex-wrap: nowrap;
    gap: clamp(15px, 1.35vw, 19px);
    /* No text-transform: the labels carry their own capitalisation. Was 13px/.035em/30px
       gaps for eight labels; "Documents" (2026-09-12) made nine, which no longer fits the
       1240px .wrap at those metrics, so the whole roomy band tightened one notch:
       12.5px, .02em, 16px gaps — ~15px spare at the 1340px breakpoint. */
    font-family: var(--font-ui); font-size: 12.5px; font-weight: 500;
    letter-spacing: .02em;
  }
  .masthead__nav a {
    color: var(--ink); white-space: nowrap;
    transition: color .25s var(--ease);
  }
  .masthead__nav a:hover,
  .masthead__nav a[aria-current="page"] { color: var(--brand); }
  /* The last item is the call to action, so it reads as a button rather than a link. Scoped
     to :last-child rather than a class, because nav.php builds every item from one loop —
     the CTA is defined by its POSITION in $links, which is where it already lives.
     aria-current never applies to it (it is an in-page anchor, not a route), so the
     current-page colour above cannot fight it. */
  .masthead__nav a:last-child {
    margin-left: clamp(4px, .6vw, 12px);
    padding: .72em 1.25em;
    background: var(--brand); color: var(--surface);
    border-radius: 2px;
    transition: background .25s var(--ease), color .25s var(--ease);
  }
  .masthead__nav a:last-child:hover { background: var(--ink); color: var(--white); }

  /* ── The same links on the scrolled dark bar ──────────────────────────────────
     Colours here are measured against the composited glass, not picked. The bar is
     --brand at 82%, so over white page content it composites to about #50706F, luminance
     .142. Against that:
       --surface #F4F4F3   5.07 : 1   resting links, clears 4.5:1 at 13px
       --white   #ffffff   5.48 : 1   hover and current page
       --sage    #507663   1.07 : 1   FAILS badly — do not use sage for text on this bar.
                                      It was 2.31:1 as the old #91AFA0 and still failed. */
  .masthead.is-stuck .masthead__nav a { color: var(--surface); }
  .masthead.is-stuck .masthead__nav a:hover,
  .masthead.is-stuck .masthead__nav a[aria-current="page"] { color: var(--white); }
  /* The CTA inverts: brand-on-white at rest, white-on-brand would vanish into the glass, so
     it becomes a white chip with brand text (9.0:1). Hover fills with sage and the label goes
     WHITE — ink on the new darker sage is only 4.11:1, under the 4.5:1 bar at 13px, where
     white is 5.11:1. (It was ink while sage was the old light #91AFA0.) */
  .masthead.is-stuck .masthead__nav a:last-child {
    background: var(--white); color: var(--brand);
  }
  .masthead.is-stuck .masthead__nav a:last-child:hover {
    background: var(--sage); color: var(--white);
  }
  /* The burger is a mobile control only from here up. */
  .masthead__menu { display: none; }
}

/* COMPACT band: iPad landscape and small desktops. The bar leaves the .wrap column and
   aligns with the hero image instead — same --hero-inset the hero media uses, so the logo
   sits on the image's left edge and the Contact Us chip on its right edge. Type drops to
   12px and the gaps tighten so all eight labels + the chip clear 1024px beside the logo. */
@media (min-width: 1024px) and (max-width: 1339px) {
  .masthead__bar { max-width: none; padding-inline: var(--hero-inset); }
  /* Was 12px/.02em with 16px max gaps; retightened 2026-09-12 when "Documents" made nine
     labels, keeping ~75px spare at the 1024px floor. */
  .masthead__nav { font-size: 11.5px; letter-spacing: 0.01em; gap: clamp(11px, 1.2vw, 17px); }
  .masthead__nav a:last-child { margin-left: 4px; padding: 0.7em 1.05em; }
}
/* While the overlay is open the bar sits on top of it, so the inline links would
   collide with the overlay list. They are only visible at desktop, where the overlay is
   unreachable anyway (no burger), but this keeps it true if that ever changes. */
.menu-open .masthead__nav { visibility: hidden; }

.masthead__burger { display: grid; gap: 5px; width: 22px; }
.masthead__burger span { display: block; height: 1px; background: currentColor; transition: transform .3s var(--ease); }
.menu-open .masthead__burger span:nth-child(1) { transform: translateY(3px)  rotate(45deg); }
.menu-open .masthead__burger span:nth-child(2) { transform: translateY(-3px) rotate(-45deg); }

/* â”€â”€ Hero banner â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   One overlapping grid cell: the copy layer sits on top of the media layer. The media
   is pushed down by --hero-band, so heading lines 1 and 2 land in the white gap and the
   italic third line straddles the top edge of the image, as drawn. */
.hero { background: var(--white); }
.hero__inner { display: grid; grid-template-columns: 100%; }

/* Constrained to the same .wrap box as the masthead, so the heading's right edge lines
   up with MENU on every screen. (justify-self:end would track the viewport instead and
   drift right of MENU on anything wider than --wrap.) */
.hero__copy {
  grid-area: 1 / 1; z-index: 2; align-self: start;
  width: 100%; max-width: var(--wrap); margin-inline: auto;
  padding-inline: var(--pad); text-align: right;
  /* Inside the image, not above it. --hero-inset is NOT added here: the media insets only
     on the inline axis, so the photograph starts at the grid top and this is the whole
     distance from it. */
  padding-top: var(--hero-copy-in);
  pointer-events: none;                      /* the text never blocks a click on the image */
}
/* ...but the CTA inside it must still be clickable. Without this the kicker link is dead,
   because the parent's pointer-events:none is inherited by everything in it. */
.hero__kicker, .hero__kicker a { pointer-events: auto; }
.hero__title {
  /* White, not sage: it sits on the photograph now, under the corner scrim. */
  font-family: var(--font-display); font-weight: 400; color: var(--white);
  font-size: var(--t-hero); line-height: 1.22; letter-spacing: .005em;
}
.hero__title em { font-style: italic; }
/* font-style stays normal here: .cta italicises only the verb. */
.hero__kicker {
  font-family: var(--font-display); color: var(--white);
  font-size: calc(var(--t-hero) * .38); margin-top: .5em; max-width: none;
}
/* The hero CTA carries a rule under it. It uses .cta--bare, which exists to drop that rule
   and inherit the surrounding font-size — the size inheritance is still wanted here (the
   kicker sets it), so rather than swapping the class the rule is simply put back. Two classes
   deep so it out-specifies .cta--bare's border-bottom: 0.

   WHITE, not the --sage that .cta sets. .hero__kicker already declares white, but that is
   only inherited — a colour set directly on the <a> beats it, so the link was rendering sage
   while the title above it was white. It has to be overridden here, not just on the parent.
   The scrim's composited luminance under this corner is .256, which puts sage at 1.44:1 —
   effectively invisible. White is 3.43:1 (see the --hero-scrim note above; still short of
   4.5:1 for text this size, which is what raising the scrim to .94 would buy).

   The rule under it starts at 55% white so the underline reads as secondary to the label,
   and goes solid on hover — that IS the hover, since the text cannot get lighter than white
   and every darker option on this scrim fails contrast. */
.hero__kicker .cta {
  color: var(--white);
  border-bottom: 1px solid rgba(255, 255, 255, .55); padding-bottom: .18em;
}

/* Overrides .cta:hover (--brand), which lands later in the file, so this is deliberately two
   classes deep to win on specificity. On this scrim (luminance .256) --brand measures
   2.63:1 and --sage 1.44:1, so neither is available as a hover colour. */
.hero__kicker .cta:hover { color: var(--white); border-bottom-color: var(--white); }

.hero__media { grid-area: 1 / 1; margin-top: var(--hero-band); padding-inline: var(--hero-inset); }
/* Positioning context for the audio toggle. Separate from .hero__carousel so the toggle's
   invisible hit area isn't clipped by that element's overflow:hidden. */
.hero__frame { position: relative; }
.hero__carousel {
  position: relative; aspect-ratio: 1920 / 797; overflow: hidden; background: var(--mist);
}
/* Scrim under the heading. The italic third line and the CTA sit over the top-right of the
   image, and on a pale frame (the sunset slide, the pool deck) sage-on-cream loses contrast.
   Runs from the top-right corner towards the bottom-left and is gone by --hero-scrim-in, so
   it darkens only the corner the type occupies and leaves the rest of the photograph alone.
   z-index 1 puts it over the slides (which have none) but under .hero__audio at 2. */
.hero__carousel::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to bottom left, rgba(0, 0, 0, var(--hero-scrim)), transparent var(--hero-scrim-in));
  /* Darker and reaching further across than the 0.7 / 48% it was: the whole heading is on
     the photograph now, not just its last line, so the corner has more type to carry.

     0.86 / 70% is set by MEASUREMENT, not taste. White type was sampled over the heading's
     actual box (x 67-90%, y 8-40% of the image) on all three slides:
       0.80 / 62%  ->  2.67 : 1  the title itself fails the 3:1 large-text bar
       0.86 / 70%  ->  3.43 : 1  title clears 3:1 on every slide
       0.94 / 78%  ->  4.62 : 1  the smaller kicker clears 4.5:1 too
     0.86 is the lightest corner that carries the title. The kicker (20px, so NOT large text
     by WCAG) still wants 0.94 — raise both knobs together if that matters more than the
     photograph showing through. */
  --hero-scrim: 0.86;
  --hero-scrim-in: 70%;
}
/* Homepage: no copy over the image any more (tagline and standalone CTA both removed at the
   client's request), so the corner scrim that existed to carry that type goes with them.
   The inner pages keep their kickers and keep the scrim. */
.hero--bare .hero__carousel::after { display: none; }

/* â”€â”€ Ambient sound toggle: small glossy disc, top-left inside the image â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
.hero__audio {
  position: absolute; top: 10px; left: 10px; z-index: 2;
  display: grid; place-items: center;
  width: calc(var(--audio-icon) + var(--audio-pad) * 2);
  height: calc(var(--audio-icon) + var(--audio-pad) * 2);
  padding: 0; border: 0; border-radius: 50%;
  color: var(--ink);
  background: rgba(255, 255, 255, .6);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .12);
  transition: background .2s var(--ease), transform .2s var(--ease);
  /* 3x the first pass (10px icon / 3px pad). The pad is scaled with the icon on purpose:
     the glyph is square, so a circle that only just contains a 10px square would let a
     30px square's corners poke outside it. Drop --audio-pad alone for a tighter disc.
     At 48px the control clears the 24px minimum target size on its own, so the
     transparent hit-area pseudo-element the small version needed is gone. */
  --audio-icon: 30px;
  --audio-pad: 9px;
}
.hero__audio:hover { background: rgba(255, 255, 255, .82); transform: scale(1.08); }
.hero__audio svg {
  width: var(--audio-icon); height: var(--audio-icon); display: block;
  /* Back to the source weight: at 30px a 1.5 stroke on a 24px viewBox renders ~1.9px,
     which reads cleanly. The 2.2 bump was only needed at the old 10px size. */
  stroke: currentColor; stroke-width: 1.5; fill: none;
  stroke-linecap: round; stroke-linejoin: round;
}
.hero__audio [data-icon="off"] { display: none; }
.hero__audio[aria-pressed="false"] [data-icon="on"]  { display: none; }
.hero__audio[aria-pressed="false"] [data-icon="off"] { display: block; }
.hero__slide {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; transition: opacity 1.2s var(--ease);
}
.hero__slide.is-active { opacity: 1; }

/* Carousel pagination. Built by main.js only when the carousel actually runs, so there is
   no no-JS/single-slide styling to worry about. The visible dot is a ::before inside a
   larger invisible button, so the dots stay small and neat while the hit target still
   clears the 24px WCAG minimum. z-index 2: over the slides and the corner scrim (z1),
   level with the audio disc, which lives in the opposite corner. */
.hero__dots {
  position: absolute; left: 0; right: 0; bottom: 10px; z-index: 2;
  display: flex; justify-content: center; gap: 2px;
  pointer-events: none;                       /* the row spans the image; only dots are live */
}
.hero__dot {
  pointer-events: auto;
  display: grid; place-items: center;
  width: 24px; height: 24px; padding: 0; border: 0; background: none; cursor: pointer;
}
.hero__dot::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: rgba(255, 255, 255, .45);
  box-shadow: 0 0 3px rgba(0, 0, 0, .35);     /* keeps it legible on the pale sand slide */
  transition: background .3s var(--ease), transform .3s var(--ease);
}
.hero__dot:hover::before { background: rgba(255, 255, 255, .75); }
.hero__dot.is-active::before { background: var(--white); transform: scale(1.3); }

/* Mobile: stop the overlap. Copy above, image below, nothing to misread. */
@media (max-width: 700px) {
  :root { --hero-lift: 0px; --hero-band: 0px; }
  .hero__inner { grid-template-columns: 100%; }
  .hero__copy { grid-area: auto; padding-bottom: clamp(20px, 5vw, 32px); }
  /* Up here the copy is its own row on the white ground, not over the photograph, so the
     white-on-scrim colours above would vanish. Ink, with the same 55%-to-solid rule. */
  .hero__kicker, .hero__kicker .cta { color: var(--ink); }
  .hero__kicker .cta { border-bottom-color: rgba(0, 0, 0, .45); }
  .hero__kicker .cta:hover { color: var(--ink); border-bottom-color: var(--ink); }
  .hero__media { grid-area: auto; }
  .hero__carousel { aspect-ratio: 4 / 3; }   /* the 2.4:1 crop is a letterbox sliver on a phone */
  /* Half size: 30+2x9 = 48px disc becomes 15+2x4.5 = 24px. Both vars are halved rather than
     just the icon, because the pad is scaled WITH the glyph — a circle that only just
     contained a 15px square would let its corners poke out.
     24px is exactly the WCAG 2.2 minimum target size, so this is the floor: any smaller and
     the control is too small to hit reliably on a touch screen. */
  .hero__audio { --audio-icon: 15px; --audio-pad: 4.5px; }
}

/* â”€â”€ Full-page menu â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
.menu {
  position: fixed; inset: 0; z-index: 60; overflow: hidden;
  background: var(--brand-dark);              /* covers the gap before the photo decodes */
  opacity: 0; visibility: hidden;             /* visibility:hidden also blocks focus */
  transition: opacity .35s var(--ease), visibility .35s;
}
.menu.is-open { opacity: 1; visibility: visible; }

/* Reproduces the box object-fit:cover would give a --menu-ar image at this viewport, so
   anything positioned inside in % is positioned against the PHOTO, not the window.
     wider viewport than the photo  -> cover is width-driven,  box = 100vw x 100vw/ar
     taller viewport than the photo -> cover is height-driven, box = 100dvh*ar x 100dvh
   max() picks the right one on each axis without a media query or a line of JS. */
.menu__stage {
  --menu-ar: 1.779425;                        /* 1920 / 1079 */
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width:  max(100vw, calc(100vh * var(--menu-ar)));
  height: max(100vh, calc(100vw / var(--menu-ar)));
  width:  max(100vw, calc(100dvh * var(--menu-ar)));
  height: max(100dvh, calc(100vw / var(--menu-ar)));
}

.menu__media { position: absolute; inset: 0; z-index: 0; }
.menu__bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* Every layer that swaps to its evening counterpart on hover carries this one class, so the
   photo and the tree cut-out are guaranteed to fade on identical timing. Add the class to
   any future layer and it joins the same cross-fade. :has() keeps it in CSS; :focus-within
   gives keyboard users the same thing as they tab through the items. */
.menu__layer--hover { opacity: 0; transition: opacity .9s var(--ease); }
.menu:has([data-menu-nav]:hover) .menu__layer--hover,
.menu:has([data-menu-nav]:focus-within) .menu__layer--hover { opacity: 1; }

/* Green panel. Positioned against the VIEWPORT (it is a layout element, not part of the
   photo), so it stays put regardless of how the photo crops. Wipes down from the top. */
.menu__strip {
  position: absolute; top: 0; bottom: 0; z-index: 1;
  right: calc(var(--edge) - var(--menu-strip-overhang)); width: var(--menu-strip-w);
  background: var(--brand);
  transform: scaleY(0); transform-origin: top;
  transition: transform .75s var(--ease) .15s;
}
.menu.is-open .menu__strip { transform: scaleY(1); }

/* Cut-out branches: same stage as the photo, pinned to its top-right corner. The width is
   the crop's share of the source (1141/1920 = 59.4271%); height:auto then lands on
   790/1079 = 73.2159% exactly. Re-derive both if the cut-out is ever re-exported at a
   different size, which has already happened once. */
.menu__trees { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
.menu__tree { position: absolute; top: 0; right: 0; width: 59.4271%; height: auto; }

/* Right-aligned to --edge, so the items land on exactly the same line as the Menu
   toggle's right edge in the masthead above. */
.menu__nav {
  position: absolute; z-index: 3; right: var(--edge); bottom: 12%;
  display: grid; gap: clamp(.5rem, .9vw, .85rem); justify-items: end; text-align: right;
}
.menu__nav a {
  font-family: var(--font-ui); color: var(--surface);
  font-size: clamp(13px, 1.15vw, 17px); font-weight: 400;
  letter-spacing: .1em; text-transform: uppercase;
  opacity: 0; transform: translateY(10px);
  transition: opacity .5s var(--ease), transform .5s var(--ease), color .25s var(--ease);
}
/* Staggered so the items arrive after the panel has wiped past them. The third value in
   the list is the colour transition, which must stay instant for hover to feel responsive.

   The delay is computed from --i, which nav.php stamps on each link as its index. This
   used to be five hand-written :nth-child rules, which meant the menu silently broke the
   moment it grew past five items: item six matched none of them, got no delay, and flew
   in on its own while the rest were still waiting. Adding a link now needs no CSS. */
.menu__nav { --menu-lead: .55s; --menu-step: .08s; }
.menu.is-open .menu__nav a {
  opacity: 1; transform: none;
  --menu-in: calc(var(--menu-lead) + var(--i, 0) * var(--menu-step));
  transition-delay: var(--menu-in), var(--menu-in), 0s;
}
/* White, not sage: .menu__strip behind these is --brand, where the new sage measures 1.76:1.
   Resting is --surface (above), so this is the same surface -> white step the masthead uses. */
.menu__nav a:hover, .menu__nav a[aria-current="page"] { color: var(--white); }

/* Phones: the panel and the right-hand alignment stop making sense at this width, so the
   panel becomes a full-width scrim and the items centre over it. */
@media (max-width: 700px) {
  .menu__strip { right: 0; width: 100%; opacity: .92; }
  .menu__nav { right: 0; left: 0; bottom: 14%; justify-items: center; text-align: center; }
  .menu__nav a { font-size: 15px; }
}

/* â”€â”€ Contact â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Background is the beige the client supplied as #E1DFDF, which is --mist.
   Padding is 60px tighter than the standard --section-y rhythm, top and bottom, with a
   floor so it does not go negative on a small screen. */
.contact {
  background: var(--contact-bg); --contact-bg: var(--mist);
  padding-block: max(16px, calc(var(--section-y) - 30px)) max(12px, calc(var(--section-y) - 70px));
}
/* The negative margin is the italic D's own left side bearing, measured at 0.05em across
   the size range. It pulls the glyph's ink out to the box edge so the title lines up with
   the left end of the input underlines below, which is the form's real visual left edge.
   Aligning to the input's placeholder text instead would need a shift the other way. */
.contact__title {
  --wipe-fill: var(--ink);
}
/* font-style comes from the all-caps block above: roman. */

/* Underline fields in two columns: name/phone left, email/message right. */
.cform {
  display: grid; grid-template-columns: 1fr 1fr;
  column-gap: clamp(28px, 6vw, 76px); row-gap: clamp(22px, 3vw, 44px);
  margin-top: clamp(20px, 3.7vw, 60px);   /* 10px tighter under the title */
}
.cform__msg { grid-column: 1 / -1; font-family: var(--font-ui); font-size: 13px; margin: 0; }
.cform__msg:empty { display: none; }
.cform__msg.is-error { color: #6d1610; }
/* Source order is name, email, phone, message: that is the sensible tab order AND the
   linear reading order, so the drawn two-column layout is placed explicitly rather than
   achieved by reordering the markup. */
.cform__field:nth-of-type(1) { grid-column: 1; grid-row: 2; }
.cform__field:nth-of-type(2) { grid-column: 2; grid-row: 2; }
.cform__field:nth-of-type(3) { grid-column: 1; grid-row: 3; }
.cform__field:nth-of-type(4) { grid-column: 2; grid-row: 3; }

.cform input[type="text"], .cform input[type="email"], .cform input[type="tel"], .cform textarea {
  width: 100%; background: none; border: 0; border-bottom: 1px solid rgba(0, 0, 0, .42);
  /* Was 0 0 .7em, i.e. text jammed against the left edge and sitting on the underline.
     The side inset also gives the browser's autofill highlight somewhere to breathe —
     it fills the padding box, so with no padding it painted right up to the glyphs. */
  --cform-pad-x: clamp(10px, .9vw, 16px);
  padding: .55em var(--cform-pad-x) .75em;
  color: var(--ink);
  font-family: var(--font-ui); font-size: 14px;
  letter-spacing: .12em; text-transform: uppercase;
  transition: border-color .25s var(--ease), box-shadow .25s var(--ease);
}
/* iOS Safari zooms the whole page when a field under 16px takes focus, which throws the
   layout about. 14px is fine on a pointer device; touch devices keep 16px to avoid it. */
@media (pointer: coarse) {
  .cform input[type="text"], .cform input[type="email"], .cform input[type="tel"], .cform textarea { font-size: 16px; }
}
.cform input::placeholder, .cform textarea::placeholder { color: rgba(0, 0, 0, .7); opacity: 1; }
/* No focus ring, as asked. Focus is still clearly shown, because removing the indicator
   outright would strand keyboard users: the underline goes black and doubles in weight.
   box-shadow rather than border-width so nothing shifts by a pixel when it lands. */
.cform input:focus,
.cform input:focus-visible,
.cform textarea:focus,
.cform textarea:focus-visible {
  outline: none;
  border-bottom-color: var(--ink);
  box-shadow: 0 1px 0 0 var(--ink);
}

.cform__foot {
  grid-column: 2; grid-row: 4;
  display: flex; align-items: center; justify-content: space-between; gap: clamp(14px, 2vw, 30px);
}
.cform__consent {
  display: inline-flex; align-items: center; gap: .9em; cursor: pointer;
  font-family: var(--font-ui); font-size: var(--t-cta); letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink);
}
.cform__consent a { text-decoration: underline; }
.cform__consent input { flex: none; width: 20px; height: 20px; accent-color: var(--brand); }
/* Black, not the .cta sage: sage on this beige is far too weak for the one control that
   actually submits the form. */
.cform__submit { background: none; border: 0; cursor: pointer; padding-bottom: .18em; color: var(--ink); }
.cform__submit:hover { color: var(--brand); }
.cform__hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

@media (max-width: 700px) {
  .cform { grid-template-columns: 1fr; }
  .cform__field:nth-of-type(1), .cform__field:nth-of-type(2),
  .cform__field:nth-of-type(3), .cform__field:nth-of-type(4),
  .cform__foot { grid-column: 1; grid-row: auto; }
  .cform__foot { flex-direction: column; align-items: flex-start; gap: 1.4rem; }
}

/* â•â•â• Shared page components â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
   Built from patterns already on the homepage so the inner pages stay in the same
   language rather than inventing a second one:
     (inner pages reuse .hero itself for their banner, so there is no separate page-hero)
     .sec-*  five section shapes (show / duo / calm / tri / bleed), own block below
     .fgrid  titled text blocks â€” the .heritage__cols pattern, for copy with no photo
     .posts  post cards â€” used by the homepage strip AND the /blog listing

   .feat, an alternating image/copy split, used to live here and drove /lifestyle and
   /forest. Both moved to .sec-* and nothing referenced it, so it was removed rather
   than left as dead weight.
   â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• */

/* â”€â”€ Titled text blocks, for copy with no photograph â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
.fgrid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
  gap: clamp(26px, 3.6vw, 56px);
  margin-top: clamp(30px, 4.4vw, 64px);
}
/* Ruled off at the top so five blocks of plain text read as a structured list rather than
   five loose paragraphs. --mist on --surface: visible, but not a line that competes. */
.fgrid__item {
  display: grid; gap: .55rem; align-content: start;
  border-top: 1px solid var(--mist);
  padding-top: clamp(14px, 1.6vw, 22px);
}
/* The icon sits inside the h2, so the heading is the flex row. */
.fgrid__head {
  display: flex; align-items: center; gap: clamp(8px, .8vw, 12px);
  margin: 0;
}
.fgrid__icon {
  flex: none; width: clamp(20px, 1.6vw, 26px); height: clamp(20px, 1.6vw, 26px);
  /* --muted is exactly #6b7a72, so this uses the token rather than hard-coding the hex. */
  color: var(--muted);
}
.fgrid__title { font-family: var(--font-display); font-size: clamp(17px, 1.4vw, 23px); line-height: 1.25; color: var(--ink); }

/* ══ Lifestyle page: the seven facilities ═══════════════════════════════════════════
   Seven facilities, five shapes. These used to be seven .feat splits alternating
   image-left / image-right; over that many repeats the alternation stops reading as
   rhythm and starts reading as a list, so the page now has a shape instead:

     .sec-split   copy one side, a square photograph the other   Ray's Kitchen / Wildlife
     .sec-duo     tall image, a square at the foot of the copy    Pool & Gym / Forest Boardwalk
     .sec-calm    centred and symmetrical on a mist band          Spa / Birdlife
     .sec-tri     three portrait cards on one line                Tennis / Trails / Play
     .sec-bleed   full-bleed closer, copy over the photograph     Beach Clubhouse

   A sixth, .sec-show — a wide opener with a frosted panel notched over its corner — was
   removed once /lifestyle and /forest had both moved off it and nothing referenced it.

   No new visual language: the inset square is the .welcome pattern, .sec-split is the
   homepage's .loc, and the bleed is the hero's scrim on a shorter frame. Only .sec-bleed
   escapes the wrap; the rest live inside it like every other section.

   Image ratios here are contractual — the exports in images/sec-*.webp and images/fo-*.webp
   are cut to match (1:1, 3:3.48, 3:2, 3:3.2, 2:1). Changing a ratio means re-cutting the
   crop, not just the aspect-ratio value, or the subject gets cropped by object-fit instead.
   ════════════════════════════════════════════════════════════════════════════════════ */
/* font-family and font-weight come from the eyebrow-treatment block below. */
.sec-title { /* size, family, weight, case, tracking and colour: shared label rule */ }

/* ── SECTION TITLES: THE EYEBROW TREATMENT AT TITLE SIZES ──────────────────────────
   These are the same four properties .eyebrow sets — Montserrat, 600, uppercase, .18em —
   just at each title's own size. The two are now ONE system: retracking or reweighting
   .eyebrow should be done here too, or they drift apart.

   That means these titles are NO LONGER Sunshine. The display serif is now reserved for the
   long statement headings (.welcome__title, .amen__title, .club__title, .hero__title), which
   stay sage Sunshine — so the page reads serif-sage for statements, tracked-sans-ink for
   section labels. Do not "restore" var(--font-display) here without moving the whole family.

   text-transform rather than rewritten copy, so the DOM text stays mixed case for screen
   readers, and .wipe's background-clip: text still sweeps the rendered glyphs.

   MEASURED, not assumed: Montserrat at .18em is about 1.4x wider than Sunshine at .02em, so
   the old fits do not carry over. Simulating the real greedy wrap at 900/1440/1920px, every
   title holds either one line or a clean two, at the sizes already set:
     one line   heritage (ESTATE HERITAGE), live, contact, sec-split, sec-calm, sec-bleed, forest
     two lines  loc (PERFECTLY / POSITIONED), sec-duo (JOGGING, FOREST / TRAILS & DAMS),
                sec-tri (FLOODLIT TENNIS / COURTS)
   Nothing runs to three. Re-simulate if a label, size, gap or column ratio changes.

   DELIBERATELY EXCLUDED, do not add them without re-measuring:
     .hero__title                       the h1 hero statements, 3 lines with an italic word
     .welcome__title .amen__title
     .club__title                       full sentences set as display type — caps are unreadable
                                        at that length. NOTE .club__title is ALSO used for the
                                        short "Stories from Dunkirk" on the homepage; it would
                                        need its own class to be capsed.
     .art__title .post__title .ty__title    editorial and blog titles

   All of this now lives in the ONE LABEL STYLE rule up at .eyebrow. */
/* Roman, not italic. The individual em rules below drop their font-style too, so nothing in
   the file contradicts this; this line is what covers .heritage__title, which never had an
   em rule of its own and was relying on the browser default. */
.heritage__title em,
.live__title em,
.loc__title em,
.contact__title em,
.forest__title em,
.sec-title em { font-style: normal; }
.sec-media { margin: 0; overflow: hidden; }
.sec-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Clearance under a section eyebrow. Declared after .sec-media's margin:0 so it wins.
   .sec-calm is absent on purpose — neither page puts an eyebrow there, its photograph leads. */
.sec-tri { margin-top: clamp(24px, 3.4vw, 52px); }

/* ── Split: copy left, a square photograph right ──────────────────────────────────
   Modelled directly on the homepage's .loc section, which is the layout this was asked to
   match: a .92fr / 1fr split, a percentage gutter that opens up on wide screens, and a 1:1
   image so the photograph reads as a panel rather than a banner. Kept as its own shape
   rather than reusing .loc's classes, since those are named for the Location section.

   The image ratio is contractual — images/sec-rays-*.webp is cut 1:1 to match. */
.sec-split {
  display: grid; align-items: stretch;
  grid-template-columns: minmax(0, .92fr) minmax(0, 1fr);
  column-gap: clamp(28px, 9%, 90px);
}
.sec-split__left {
  display: flex; flex-direction: column; justify-content: flex-start;
  gap: clamp(14px, 2.4vw, 34px);
}
.sec-split__title {
  --wipe-fill: var(--ink);
}
/* Both lines flush left, unlike .loc__title, which indents its italic line to right-align
   the pair. Measured in Sunshine at the clamp cap: "Ray's Kitchen" italic is 307px against
   "Clubhouse &" at 297px, so the italic line is the LONGER one and any indent would push it
   past the column. Restore an indent only if the wording changes to make line 1 longer. */
.sec-split__title em { display: block; }
.sec-split__media { margin: 0; overflow: hidden; aspect-ratio: 1; }
/* --32: one frame relaxed to the photograph's own 3:2. Used only by /lifestyle's Ray's
   Kitchen "dinner" shot, whose 600x400 master cannot survive the square crop — at 3:2 the
   cover fit crops nothing, so the image shows whole with the least possible upscaling. */
.sec-split__media--32 { aspect-ratio: 3 / 2; align-self: center; }
.sec-split__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Mirrored: photograph left, copy right. Only the order flips — the .92fr column stays the
   COPY's, so the image is the slightly wider of the two, which is what /forest wants. */
.sec-split--flip .sec-split__media { order: -1; }
.sec-split--flip { grid-template-columns: minmax(0, 1fr) minmax(0, .92fr); }
@media (max-width: 860px) {
  .sec-split, .sec-split--flip { grid-template-columns: 1fr; row-gap: clamp(24px, 5vw, 40px); }
  .sec-split__media { order: -1; }             /* photograph leads on a phone either way */
}

/* ── Duo: portrait image with a small square inset hung into the column gap ──────── */
.sec-duo {
  display: grid; align-items: stretch;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  /* +50px on both ends of the previous clamp(44px, 6.4vw, 108px): the copy was crowding
     the photograph beside it. */
  column-gap: clamp(94px, 6.4vw, 158px);
}
.sec-duo__stack { position: relative; }
/* Mirrored: photograph right, copy left. order on the stack rather than reversing the grid
   template, so the DOM keeps the photograph first — it is the more important of the two and
   should still lead when the columns collapse on a phone. */
.sec-duo--flip .sec-duo__stack { order: 2; }
/* The copy column. The small square is OPTIONAL: /lifestyle's Pool & Gym has one and it is
   pushed to the foot from here, /forest's Boardwalk has none and the copy just sits at the
   top. Nothing to change either way. */
.sec-duo__aside { display: flex; flex-direction: column; }
/* 3:3.48, not 3:4 — 13% off the height at the same width. images/sec-pool-*.webp is cut to
   match, with the horizontal framing left alone and the 13% taken off the top and bottom,
   so the photograph reads identically and object-fit has nothing left to trim. */
.sec-duo__main { aspect-ratio: 3 / 3.48; }
/* The small square sits at the foot of the RIGHT column, right-aligned to the copy above it
   and bottom-aligned with the main photograph opposite.

   Both alignments fall out of the layout rather than being set: margin-left:auto pushes it
   to the column's right edge, and margin-top:auto pushes it to the foot of a column that
   .sec-duo stretches to the row height. The row height is the main image's height, so the
   two bottoms land on the same line — PROVIDED the copy plus this square is shorter than the
   main image. It is at every width down to the 860px breakpoint (verified), but make the
   square much larger and that stops being true.

   No outline any more: it used to overhang the next section and needed separating from it.
   It sits inside its own section now, so the ring was just a frame that did not match. */
.sec-duo__sub {
  /* +5% on clamp(148px, 19.8vw, 288px), which was itself +20% on clamp(123px, 16.5vw, 240px).
     /lifestyle's Pool & Gym is the ONLY consumer of this square — /forest's Boardwalk has no
     sub and /beach-clubhouse dropped its one — so this value is effectively single-use.
     Checked against the note above about the copy plus this square staying shorter than the
     photograph: it clears from 960px up, and in the 860-960px band the aside is already the
     taller of the two by ~20px BEFORE this change. The 5% adds ~9px to that, so it is a
     pre-existing squeeze slightly deepened, not a new one — nothing bottom-aligns there, the
     row simply takes the taller column. */
  --sec-sub-w: clamp(155px, 20.8vw, 302px);
  width: var(--sec-sub-w); aspect-ratio: 1 / 1;
  margin-left: auto;
  margin-top: auto;
}
.sec-duo__title { margin-bottom: .55em; }

/* ── Calm: centred and symmetrical, on its own band. The one quiet beat on the page ── */
.sec-calm { background: var(--mist); }
.sec-calm__inner { max-width: 920px; margin-inline: auto; text-align: center; }
/* 80% of the text column, i.e. 20% smaller than the first pass, and centred inside it.
   The measure stays on .sec-calm__inner so narrowing the photograph does not narrow the copy. */
.sec-calm__media { aspect-ratio: 3 / 2; width: 80%; margin-inline: auto; margin-bottom: clamp(20px, 3vw, 40px); }
.sec-calm__title { margin-bottom: .55em; }
/* p carries a global max-width: var(--measure); centring needs the box centred too. */
.sec-calm .prose p { max-width: 60ch; margin-inline: auto; }

/* ── Trio: three portraits stepping down the page ────────────────────────────────── */
/* Three cards on ONE line. They used to step down the page by --sec-step each; the cascade
   is gone and the tops are level. */
.sec-tri {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  /* +20px on both ends of the previous clamp(18px, 2.6vw, 44px). */
  column-gap: clamp(38px, 4vw, 64px); align-items: start;
}
/* 3:3.2, not 3:4 — 20% off the height at the same width. The exports are cut to match, so
   changing this back means re-cutting them, not just editing the ratio. */
.sec-tri__media { aspect-ratio: 3 / 3.2; margin-bottom: clamp(13px, 1.5vw, 20px); }
.sec-tri__title { margin-bottom: .45em; }

/* ── Closer: full-bleed photograph, parallax + slow zoom, with the copy over it ───── */
.sec-bleed {
  position: relative; display: grid; isolation: isolate;
  min-height: clamp(400px, 48vw, 620px);
  background: var(--brand);            /* never flashes white before the image paints */
}
/* Parallax by TRANSFORM, not position:fixed. The .plx bands elsewhere on the site pin the
   image to the viewport and clip it, which is right for a band you scroll past. Here the
   image also has to zoom, and a scaled position:fixed child inside a clip-path ancestor is
   exactly the combination browsers disagree about — so this one translates and scales a
   normally-positioned image instead. Same visual result, one compositor layer, no fixed
   positioning, so iOS behaves.

   --pz is scroll progress across this section, 0 to 1, written by main.js. It defaults to
   .5 here so a visitor with JS off or reduced motion gets the mid state: no offset, and
   the zoom parked halfway. */
.sec-bleed__media {
  grid-area: 1 / 1; position: relative; overflow: hidden;
  --pz: .5;
  --sec-travel: clamp(24px, 4.5vw, 64px);   /* how far the image slides, each way from centre */
  --sec-zoom:   .12;                        /* total scale gained across the crossing */
}
.sec-bleed__media img {
  /* Oversized by --sec-travel top and bottom so the slide can never expose an edge; the
     zoom only ever adds coverage on top of that, so scale needs no slack of its own. */
  position: absolute; left: 0; width: 100%;
  top: calc(-1 * var(--sec-travel));
  height: calc(100% + var(--sec-travel) * 2);
  object-fit: cover;
  transform:
    translate3d(0, calc((.5 - var(--pz)) * var(--sec-travel) * 2), 0)
    scale(calc(1 + var(--sec-zoom) * var(--pz)));
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  /* Hold it still and unscaled rather than parked mid-zoom. */
  .sec-bleed__media { --sec-travel: 0px; --sec-zoom: 0; }
}
.sec-bleed::after {
  /* Left-weighted, unlike the hero's top-right scrim, because the copy sits left here. */
  content: ""; grid-area: 1 / 1; z-index: 1; pointer-events: none;
  background: linear-gradient(to right, rgba(0, 0, 0, var(--sec-scrim)), transparent var(--sec-scrim-in));
  --sec-scrim: .68; --sec-scrim-in: 66%;
}
.sec-bleed__panel {
  grid-area: 1 / 1; z-index: 2; align-self: center;
  width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--pad);
}
.sec-bleed__inner { max-width: 46ch; }
/* Keeps its own colour: white on a dark photograph, so it cannot take the shared ink. */
.sec-bleed__title { color: var(--white); margin-bottom: .55em; }
/* --surface, not sage: this sits on a dark photograph, where the new sage is 4.11:1 against
   pure black and worse against the real scrim. The eyebrow stays distinct from the prose
   beside it through .eyebrow's own uppercase, 600 weight and .18em tracking, not colour. */
.sec-bleed .eyebrow { color: var(--surface); display: block; margin-bottom: .9em; }
.sec-bleed .prose p { color: var(--surface); }

@media (max-width: 980px) {
  .sec-tri { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: clamp(24px, 4vw, 40px); }
}
@media (max-width: 860px) {
  .sec-duo { grid-template-columns: 1fr; row-gap: clamp(52px, 9vw, 76px); }
  /* Stacked: there is no tall column opposite to bottom-align against, so the square just
     sits in the flow. margin-left:auto still right-aligns it, which reads fine. */
  .sec-duo__sub { margin-top: 0; }
  .sec-bleed__inner { max-width: none; }
  .sec-bleed::after { --sec-scrim: .7; --sec-scrim-in: 100%; }   /* copy spans the full width now */
}
@media (max-width: 620px) {
  .sec-tri { grid-template-columns: 1fr; }
}

/* ══ Blog article ═══════════════════════════════════════════════════════════════════
   These classes shipped with blog-article.php but WITHOUT any rules, so the page rendered
   with no spacing at all and the title fell through to the global h1 (--t-h1, up to 70px).
   This is that component, written properly.

   Centred head, left-aligned body: the standard editorial shape, and it stops a short title
   and a long paragraph fighting over the same alignment. Every vertical value is a clamp so
   the rhythm holds from a phone to a wide monitor. ══════════════════════════════════════ */
/* Every other page opens on a hero, so its own image gives the masthead something to sit
   against. This one opens on type, directly under the bar — hence a little more top room than
   the standard --section-y. */
.art { padding-top: calc(var(--section-y) + clamp(10px, 2.4vw, 40px)); }
.art__head { max-width: 760px; margin-inline: auto; text-align: center; }
/* 12px off the previous cap. It was inheriting --t-h1's clamp(40px, 5.4vw, 70px) — the
   homepage hero's scale, far too loud for a single article. The floor and the vw term are
   scaled by the same 0.83 so it still interpolates smoothly instead of sitting on a stop. */
.art__title { font-size: clamp(33px, 4.5vw, 58px); line-height: 1.2; }
.art__standfirst {
  margin: clamp(14px, 1.8vw, 24px) auto 0;
  max-width: 56ch;
  /* +2px on the 14/17px it was; vw 1.06 -> 1.21 holds the fluid band. */
  font-family: var(--font); font-size: clamp(16px, 1.21vw, 19px);
  line-height: 1.6; color: var(--muted);
}

/* Deliberately NO aspect-ratio: the three posts' images are 1.78, 1.50 and 2.41, so any fixed
   box would blind-crop two of the three. Each keeps its own shape, and the width/height
   attributes on the img reserve the right space so nothing shifts as it loads. */
.art__media { margin: clamp(30px, 4.6vw, 64px) auto 0; max-width: 920px; }
.art__media img { width: 100%; height: auto; display: block; }

.art__body { max-width: 68ch; margin: clamp(30px, 4.6vw, 60px) auto 0; }
/* Larger and looser than --t-copy: this is long-form reading, not a caption beside a
   photograph. Overridden here rather than in .prose, so nothing else on the site moves.
   +2px on the 15/17px it was; vw 1.06 -> 1.2 holds the fluid band. */
.art__body p { font-size: clamp(17px, 1.2vw, 19px); line-height: 1.75; }
.art__body p + p { margin-top: 1.15em; }
.art__body h2 {
  font-family: var(--font-display); font-weight: 400; color: var(--ink);
  font-size: clamp(19px, 1.65vw, 27px); line-height: 1.25;
  margin-top: clamp(30px, 3.6vw, 50px); margin-bottom: clamp(8px, 1vw, 14px);
}
.art__body > :first-child { margin-top: 0; }

/* ══ Drift + zoom on CONTAINED photographs ══════════════════════════════════════════
   The treatment trialled on the homepage's welcome image, rolled out to every framed
   photograph. Driven by the same --pz main.js writes, so there is ONE mechanism site-wide.

   THE SIZING IS THE WHOLE POINT. These frames run from a 288px square to a 1064px panel, so
   a travel in PIXELS is useless: ±95px is 1.66x magnification on the small one and 1.18x on
   the large one. Everything is therefore a percentage, derived from a single number:

     --fx-shift  is the travel as a share of the IMG's own height, which is what a
                 translateY percentage resolves against.
     img height  = 100 / (100 - 2S) of the frame  = 138.89%
     img top     = -100S / (100 - 2S) of the frame = -19.44%

   At S = 14 that is 1.389x magnification AT EVERY FRAME SIZE, and the travel scales with the
   image (±56px on the 288px square, ±207px on the 1064px panel). The 489px welcome frame
   lands on ±95px, which is exactly the value it was signed off at.

   The translate uses the whole slack and the scale (never below 1) adds margin on top, so no
   edge is ever exposed at any size. Zoom is INVERTED — 1.06 entering, 1.0 leaving — so
   scrolling DOWN zooms OUT and scrolling UP zooms IN.

   NOT applied to: .rail__card (dragged horizontally, a second transform would fight it),
   .live__media (inside the pinned block, which owns its motion), the hero carousels (their
   own opt-in) and the .pxz/.plx bands (already covered). .art__media is handled separately
   below — its image is height:auto, so it has no frame height to oversize against.
   ═══════════════════════════════════════════════════════════════════════════════════ */
.sec-media,
.post__media,
.welcome__media,
.heritage__media,
.loc__media {
  --pz: .5;              /* main.js overwrites this; .5 is the neutral resting value */
  --fx-shift:   14%;     /* travel, as a share of the img's own height */
  --fx-top:  -19.44%;    /* = -100S/(100-2S) of the frame height */
  --fx-tall: 138.89%;    /* =  100 /(100-2S) of the frame height */
  --fx-zoom-in:  1.06;   /* scale as it enters at the bottom */
  --fx-zoom-out: 1;      /* scale as it leaves at the top */
  position: relative;
  overflow: hidden;
}
.sec-media > img,
.post__media img,
.welcome__media img,
.heritage__media img,
.loc__media img {
  position: absolute; left: 0; width: 100%;
  top: var(--fx-top); height: var(--fx-tall);
  object-fit: cover;
  transform:
    translate3d(0, calc((.5 - var(--pz)) * var(--fx-shift) * 2), 0)
    scale(calc(var(--fx-zoom-in) + (var(--fx-zoom-out) - var(--fx-zoom-in)) * var(--pz)));
  will-change: transform;
}

/* ── Heritage: the ONE frame that zooms IN on the way down ─────────────────────────
   The two --fx-zoom-* names are POSITIONS, not directions: -in is the scale when the frame
   enters at the bottom (--pz 0), -out is the scale when it leaves at the top (--pz 1). Every
   other frame runs 1.06 -> 1, so scrolling down shrinks it. This one is swapped, so scrolling
   down grows it 1 -> 1.06.

   Safe by symmetry, not by luck. The img is 138.89% of the frame with a -19.44% top, so the
   translate lands the top edge exactly flush at --pz 0 and the bottom edge exactly flush at
   --pz 1; whichever end carries the >1 scale is the protected one. Swapping just moves that
   protection from the top end to the bottom end — the geometry is a mirror image, so no edge
   is exposed either way (checked: at --pz 1 the scaled img spans -0.43F to 1.04F of the frame).

   To make the whole site zoom in on scroll instead, swap the pair in the block above and
   delete this rule rather than adding more overrides. */
.heritage__media { --fx-zoom-in: 1; --fx-zoom-out: 1.06; }

/* The article's image is height:auto — its frame takes its height FROM the image, so there is
   no frame height to oversize against and the percentages above would be circular. Scale
   only, which needs no slack because it never drops below 1. */
.art__media { --pz: .5; --fx-zoom-in: 1.06; --fx-zoom-out: 1; overflow: hidden; }
.art__media img {
  transform: scale(calc(var(--fx-zoom-in) + (var(--fx-zoom-out) - var(--fx-zoom-in)) * var(--pz)));
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .sec-media, .post__media, .welcome__media, .heritage__media, .loc__media {
    /* BOTH zoom ends, not just --fx-zoom-in. That was enough while every frame kept
       --fx-zoom-out: 1, but .heritage__media now holds 1.06 there and would have carried on
       animating 1 -> 1.06 under reduced motion. Resetting the pair keeps this correct for any
       frame that flips direction later. */
    --fx-shift: 0%; --fx-top: 0%; --fx-tall: 100%; --fx-zoom-in: 1; --fx-zoom-out: 1;
  }
  .art__media { --fx-zoom-in: 1; --fx-zoom-out: 1; }
}
/* ══ Beach Clubhouse page (/beach-clubhouse) ════════════════════════════════════════
   Two small components. Everything else on that page is the homepage's own vocabulary:
   .hero, .club, .pxz, .sec-calm, .sec-media, .contact.
   ═══════════════════════════════════════════════════════════════════════════════════ */

/* The homepage's link into the page. Sits under the supporting column, not centred, so it
   reads as the end of that column rather than as a section-level control. */
.club__more { margin-top: clamp(18px, 2.4vw, 30px); max-width: none; }

/* Nine facility labels. A LIST, not .fgrid: those are titled paragraph blocks and these are
   bare labels. Hairlines rather than bullets, matching .amen__item's resting treatment, which
   is the closest thing on the site to a facilities list. */
.retreat {
  list-style: none; padding: 0;
  margin: clamp(20px, 3vw, 34px) auto 0;
  max-width: 640px; text-align: left;
  display: grid; column-gap: clamp(24px, 4vw, 56px);
}
.retreat__item {
  font-family: var(--font-display);
  font-size: clamp(14px, 1.05vw, 18px); line-height: 1.35; color: var(--ink);
  padding: clamp(10px, 1.3vw, 14px) 0;
  border-bottom: 1px solid rgba(0, 0, 0, .12);
}

/* ── Inside a .sec-duo__aside ──────────────────────────────────────────────────────
   ONE column. It was two for a while, purely to clear the square inset .sec-duo bottom-aligns
   with the photograph opposite — nine rows plus a square overflowed at every width. The square
   was dropped, so the constraint went with it, and one column is the better read: measured
   against the real column widths it runs 442-539px against a 415-665px photograph, where two
   columns came to 329-451px and left a void of up to 336px under a stubby list.
   Only 860-950px has the list marginally taller than the photograph (442 vs 415). Nothing
   breaks there — with no square to bottom-align, the row simply takes the taller of the two.

   max-width is dropped because the aside column is already the constraint, and the auto
   margins with it, or the list would centre itself inside its own column. */
.retreat--duo { max-width: none; margin-inline: 0; grid-template-columns: 1fr; }
.retreat--duo .retreat__item:last-child { border-bottom: 0; }

/* ══ Bird-species dialog (/forest) ══════════════════════════════════════════════════
   A native <dialog>. THE GOTCHA: the UA sheet carries `dialog:not([open]) { display: none }`,
   so declaring `display: flex` unscoped would make the modal visible while closed. Every
   display here is scoped to [open].

   The dialog element itself is the full-viewport click target — transparent, padded, with the
   panel centred inside it. That is what makes "click outside to close" work: a click landing
   anywhere but the panel has the dialog as its target, which is the test main.js uses.
   ═══════════════════════════════════════════════════════════════════════════════════ */
.birds {
  border: 0; padding: clamp(14px, 3vw, 44px); margin: 0;
  width: 100%; max-width: 100%; height: 100%; max-height: 100%;
  background: none; color: var(--ink);
  overflow: hidden;              /* the PANEL scrolls, not the dialog */
}
.birds[open] { display: flex; }
.birds::backdrop { background: rgba(0, 0, 0, .55); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); }

/* Entry animation, keyed off [open] so it runs on every showModal() rather than once per page.
   `both` with only a from-frame means the to-state IS the rule's normal computed state, so
   there is nothing to keep in sync between the keyframes and the layout above.
   The panel rises and settles while the backdrop fades, and it is slightly the slower of the
   two so the darkening reads as leading the panel in rather than arriving with it. */
@keyframes birds-panel-in {
  from { opacity: 0; transform: translateY(16px) scale(.985); }
}
@keyframes birds-backdrop-in {
  from { opacity: 0; }
}
.birds[open]::backdrop    { animation: birds-backdrop-in .28s var(--ease) both; }
.birds[open] .birds__panel { animation: birds-panel-in .42s var(--ease) both; }

.birds__panel {
  background: var(--white);
  width: 100%; max-width: 860px; max-height: 100%;   /* 680 -> +15% -> +10% */
  margin: auto;                  /* centres in both axes inside the flex dialog */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* NO padding on the scroll container: the children carry it via --birds-pad. A padded
     scroller would need the sticky header pulled up by a negative margin, and `top` for a
     sticky element is measured from the padding box — so it would scroll the pad distance
     out of view before sticking. Paying for it with one variable is simpler than fighting it. */
  --birds-pad: clamp(22px, 3.4vw, 40px);
}
/* Sticky so the close button stays reachable through a 17-row scroll. The background must be
   opaque or the rows show through as they pass under it. */
.birds__head {
  position: sticky; top: 0;
  display: flex; align-items: center; justify-content: space-between; gap: 1.5em;
  padding: var(--birds-pad) var(--birds-pad) clamp(12px, 1.6vw, 18px);
  background: var(--white);
  border-bottom: 1px solid var(--mist);
  z-index: 1;
}
/* The shared label style, same as .eyebrow and every section title. */
.birds__title {
  font-family: var(--font-ui); font-size: 16px; font-weight: 600;
  letter-spacing: .18em; text-transform: uppercase; color: var(--ink);
}
.birds__close {
  flex: none; display: grid; place-items: center;
  width: 44px; height: 44px;                       /* the tap target IS the button */
  background: none; border: 1px solid var(--mist); border-radius: 50%;
  color: rgba(0, 0, 0, .42);
  transition: color .3s var(--ease), border-color .3s var(--ease);
}
.birds__close:hover { color: var(--ink); border-color: var(--sage); }
.birds__close svg { width: 18px; }

.birds__note {
  font-size: var(--t-copy); line-height: 1.65; color: var(--muted);
  margin: clamp(16px, 2.2vw, 24px) 0 0; padding-inline: var(--birds-pad); max-width: none;
}
.birds__list {
  list-style: none; margin: clamp(6px, 1vw, 12px) 0 0;
  padding: 0 var(--birds-pad) var(--birds-pad);
}
.birds__row {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: clamp(14px, 2.2vw, 24px); align-items: start;
  padding: clamp(18px, 2.4vw, 26px) 0;
  border-bottom: 1px solid var(--mist);
}
.birds__row:last-child { border-bottom: 0; padding-bottom: 0; }
/* Circular, as drawn. Fixed rather than fluid: these are thumbnails in a list, and letting
   them scale would drag the whole row's rhythm around with the viewport. */
/* Two shadows, both with a POSITIVE y so the weight sits under the circle rather than around
   it: a tight contact shadow plus a second offset further down. overflow: hidden does not clip
   these — a box-shadow is painted outside the border box — and both follow the 50% radius.

   CRISP, not soft: the blur radii are short relative to the offsets (2px blur on a 1px drop,
   9px on a 4px drop) and the opacity carries the weight instead. Widening the blur is what
   made the first pass read as a haze rather than a shadow. */
.birds__img {
  margin: 0;
  width: 160px;
  height: 160px;          /* 84px + 20% */
  border-radius: 50%;
  overflow: hidden;
  flex: none;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .16), 0 4px 9px -2px rgba(0, 0, 0, .30);
}
.birds__img img { width: 100%; height: 100%; object-fit: cover; }
.birds__name {
  font-family: var(--font-ui); font-weight: 600; font-size: 17px;
  line-height: 1.3; color: var(--ink); letter-spacing: 0;
}
/* Sentence case and untracked on purpose: these are NAMES, not labels, so they deliberately
   sit outside the uppercase label system the section titles use. */
.birds__blurb {
  font-size: var(--t-copy); line-height: 1.6; color: var(--muted);
  margin: .5em 0 0; max-width: none;
}
/* No height: native audio controls have their own intrinsic height (54px in Chrome) and
   forcing a smaller one clips the scrub bar. Width is safe to set, height is not. */
.birds__audio { width: 100%; margin-top: clamp(10px, 1.4vw, 14px); }

/* The trigger. .cta is written for links, so a <button> needs the UA border and background
   knocked off — .cta's own border-bottom then shows through as the rule under the text. */
.cta--btn { background: none; border-width: 0 0 1px; cursor: pointer; }
.birds__cta { margin-top: clamp(22px, 3vw, 36px); max-width: none; }
.sec-calm .birds__cta { text-align: center; }

@media (max-width: 560px) {
  /* The thumbnail and the copy stop sharing a row: at this width the blurb column gets too
     narrow to read beside an 84px circle. */
  .birds__row { grid-template-columns: 1fr; }
  .birds__img { width: 77px; height: 77px; }        /* 64px + 20% */
  .birds__panel { --birds-pad: clamp(16px, 4vw, 22px); }
}
@media (prefers-reduced-motion: reduce) {
  .birds::backdrop { -webkit-backdrop-filter: none; backdrop-filter: none; }
  /* `none`, not a 0s duration: with `both` a cancelled animation would otherwise leave the
     panel holding its from-frame, i.e. invisible and offset. */
  .birds[open]::backdrop,
  .birds[open] .birds__panel { animation: none; }
}

/* ══ Ordered reveals within a section ═══════════════════════════════════════════════
   Headline and larger copy arrive first, body copy after. main.js numbers every .fade-up
   inside each <section> in DOM order and writes it as --seq; the delay is computed from that.
   The order is therefore the markup order, which already runs eyebrow, heading, paragraphs —
   nothing has to be hand-tagged.

   --seq is its own property rather than reusing --i, which the menu's entrance stagger owns.
   This rule sits after .fade-up.d2/.d3 so it wins on source order at equal specificity, and
   the manual d2/d3 classes still in the markup are simply overridden. ═══════════════════ */
[data-seq] { --seq-step: .16s; }
.reveal-ready [data-seq] .fade-up { transition-delay: calc(var(--seq-step) * var(--seq, 0)); }

/* â”€â”€ Post cards â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
.posts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  gap: clamp(22px, 3vw, 46px); margin-top: clamp(30px, 4.4vw, 64px);
}
.post { display: grid; gap: .85rem; align-content: start; }
.post__media { margin: 0; overflow: hidden; aspect-ratio: 3 / 2; }
.post__media img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .6s var(--ease); }
.post:hover .post__media img { transform: scale(1.04); }
.post__title { font-family: var(--font-display); font-size: clamp(17px, 1.4vw, 24px); line-height: 1.25; color: var(--ink); }
.post__excerpt { font-size: var(--t-copy); line-height: 1.65; color: var(--muted); }

/* â”€â”€ Footer â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Light, centred logo, one meta row, then the tiling wave band. */
.footer { background: var(--surface); color: var(--ink); padding-top: clamp(48px, 8vw, 120px); }
/* A second 20% off, so 0.8 x 0.8 = 64% of the original clamp(190px, 19vw, 300px). All
   three terms scale together, so it shrinks by the same fifth at every viewport width
   rather than only at the extremes. */
.footer__logo { display: block; margin-inline: auto; width: clamp(122px, 12.16vw, 192px); }
.footer__logo img { width: 100%; height: auto; }
/* Phone and email, centred on one line with a hairline divider between them. */
.footer__contact {
  list-style: none; padding: 0;
  margin: clamp(26px, 4vw, 52px) 0 0;
  display: flex; justify-content: center; align-items: center; flex-wrap: wrap;
  gap: clamp(14px, 3vw, 42px);
  font-family: var(--font-display); font-size: clamp(14px, 1.1vw, 18px); color: var(--ink);
}
/* The divider is drawn on the item rather than as an element, so it disappears by itself
   when the two wrap onto separate lines on a narrow screen. */
.footer__contact li + li { position: relative; }
.footer__contact li + li::before {
  content: ""; position: absolute; left: calc(-1 * clamp(7px, 1.5vw, 21px));
  top: 50%; transform: translateY(-50%);
  width: 1px; height: 1.35em; background: var(--mist);
}
.footer__contact a { display: inline-flex; align-items: center; gap: 7px; }   /* the 7px */
.footer__contact a:hover { color: var(--brand); }
.footer__ico {
  flex: none; width: 1.05em; height: 1.05em; color: var(--brand);
  /* Optical, not geometric: the glyphs read heavy at the bottom against the cap height. */
  transform: translateY(-.5px);
}
@media (max-width: 560px) {
  /* Stacked, so the divider rule would sit in the wrong axis — it is dropped with the row. */
  .footer__contact { flex-direction: column; gap: .7em; }
  .footer__contact li + li::before { display: none; }
}

.footer__meta {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  gap: clamp(12px, 3vw, 40px);
  margin-top: clamp(26px, 4vw, 52px);
  padding-top: clamp(20px, 3vw, 38px);
  padding-bottom: clamp(20px, 3vw, 40px);
  /* Hairline over the copyright / privacy row. --mist, not a black alpha: it has to read
     as a rule on the footer's --surface without darkening it. */
  border-top: 1px solid var(--mist);
  /* 1px off both ends of clamp(11px, .85vw, 14px). vw .85 -> .77 keeps the fluid band where
     it was: .85vw reached 11px at 1294px, and .77vw reaches 10px at 1299px. */
  font-family: var(--font-display); font-size: clamp(10px, .77vw, 13px); color: var(--ink);
}
.footer__meta p { margin: 0; max-width: none; }
.footer__meta a:hover { text-decoration: underline; }
.footer__copy { font-style: italic; }
.footer__by { text-align: center; }
.footer__legal { text-align: right; display: flex; gap: .5em; justify-content: flex-end; flex-wrap: wrap; }

/* Decorative band: the client's wave-pattern.svg, one 405x83 strip repeated on x only.
   No background colour of its own â€” the strip's own #DEDBD7 reads tone-on-tone against the
   footer's --surface. background-size: auto 100% scales the strip to the band height and
   lets its width follow, so the tile keeps its 4.88:1 proportions at any band height. */
.footer__wave {
  position: relative; overflow: hidden;
  height: var(--wave-h);
  --wave-h: clamp(48px, 5.8vw, 83px);
  /* The rendered width of ONE tile. background-size: auto 100% scales the 405x83 strip to
     the band height, so its width follows at the source ratio — this reproduces that number
     so the animation can travel exactly one tile. Hard-coding a px width instead would drift
     out of step with the band the moment --wave-h changed. */
  --wave-tile: calc(var(--wave-h) * 405 / 83);
  --wave-secs: 22s;                  /* time to travel ONE tile, so speed is height-independent */
}
/* Animated on a pseudo-element with transform, NOT by animating background-position: a
   transform is composited, where background-position repaints the strip every frame.

   Seamless because the travel is exactly one tile and the pattern is periodic with that
   same period — at the end of each cycle the pixels are identical to the start, so the
   restart is invisible. The element is one tile WIDER than the band for the same reason:
   sliding left by a tile would otherwise uncover a gap at the right edge. */
.footer__wave::before {
  content: "";
  position: absolute; top: 0; bottom: 0; left: 0;
  width: calc(100% + var(--wave-tile));
  background-image: url("../images/wave-pattern.svg");
  background-repeat: repeat-x; background-position: left bottom;
  background-size: auto 100%;
  animation: wave-drift var(--wave-secs) linear infinite;
  will-change: transform;
}
@keyframes wave-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(calc(-1 * var(--wave-tile)), 0, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .footer__wave::before { animation: none; }
}

@media (max-width: 700px) {
  .footer__meta { grid-template-columns: 1fr; justify-items: center; text-align: center; row-gap: .6rem; }
  .footer__by, .footer__legal { text-align: center; justify-content: center; }
}

/* â”€â”€ Forms â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
.form { display: grid; gap: 1rem; max-width: 560px; font-family: var(--font-ui); }
.form__row { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr)); }
.form label { display: grid; gap: .4rem; font-size: 14px; }
.form input, .form select, .form textarea {
  font: inherit; font-size: 16px; padding: .8em 1em; border: 1px solid #cfcfcf; border-radius: var(--radius); background: var(--white); width: 100%;  /* font-size >=16px so iOS Safari doesn't zoom on focus */
}
.form input:focus, .form select:focus, .form textarea:focus { border-color: var(--accent); outline: none; }
.form__consent { display: flex; gap: .6rem; align-items: flex-start; font-size: 13px; }
.form__hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }  /* honeypot */
.form__msg { font-size: 14px; }
.form__msg.is-error { color: #b0261a; }

/* ══ Thank-you page ═══════════════════════════════════════════════════════════════
   A full-bleed photograph with the message low over it, rather than the centred text on
   white it used to be. The copy sits at the FOOT on purpose: the sun is the brightest
   thing in the frame (luminance 0.93) and a scrim heavy enough to carry white type across
   it would have flattened the sunset. The bottom band peaks at 0.375, so a bottom-weighted
   gradient reaching ~52% where the text lands gives white better than 5:1 while the sky
   above stays untouched. ═══════════════════════════════════════════════════════════════ */
.ty {
  position: relative; display: grid; isolation: isolate;
  min-height: clamp(480px, 76vh, 880px);
  background: var(--brand);          /* never flashes white before the image paints */
  /* The same white gap every hero on the site leaves between the masthead and the top of
     its image, so this page sits in the identical rhythm. margin, not padding: padding
     would show --brand in the gap instead of the page's white. */
  margin-top: var(--hero-band);
}
.ty__bg { grid-area: 1 / 1; width: 100%; height: 100%; object-fit: cover; }
/* Top-weighted, because the copy sits at the top. It was bottom-weighted when the copy was
   at the foot; the scrim has to follow the type, not the picture. The sky the text now
   crosses peaks at 0.487 luminance, which needs ~37% to carry white at AA — hence .58
   at the very top easing out by 72%, leaving the sun and the horizon clear. */
.ty::after {
  content: ""; grid-area: 1 / 1; z-index: 1; pointer-events: none;
  background: linear-gradient(to bottom,
    rgba(0, 0, 0, .60) 0%,
    rgba(0, 0, 0, .48) 26%,
    rgba(0, 0, 0, .22) 50%,
    transparent 72%);
}
.ty__panel {
  grid-area: 1 / 1; z-index: 2; align-self: start;
  width: 100%; max-width: var(--wrap); margin-inline: auto;
  /* ~70px down from the top of the banner, easing off on small screens. */
  padding: clamp(52px, 5.5vw, 70px) var(--pad) 0;
  text-align: center;
}
/* --surface for the same reason as .sec-bleed .eyebrow: dark banner behind it. */
.ty__panel .eyebrow { display: block; color: var(--surface); margin-bottom: .9em; }
.ty__title {
  font-family: var(--font-display); font-weight: 400; color: var(--white);
  font-size: clamp(30px, 4.2vw, 62px); line-height: 1.08;
}
.ty__title em { font-style: italic; }
.ty__note {
  margin: clamp(.8rem, 1.6vw, 1.3rem) auto clamp(1.4rem, 2.4vw, 2rem);
  max-width: 44ch; color: var(--surface);
  /* +2px on the 14/17px it was; vw 1.0 -> 1.14 holds the fluid band. */
  font-family: var(--font); font-size: clamp(16px, 1.14vw, 19px); line-height: 1.6;
}

/* Body offset so fixed nav never covers content on sub-pages. */
.has-static-nav { padding-top: var(--nav-h); }

/* â”€â”€ Legal pages (privacy / terms) â€” long-form readable typography â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
/* These pages carry class="wrap legal" — BOTH classes on one element, so any max-width
   here fights .wrap's. .legal must therefore declare NO max-width at all, and let .wrap's
   var(--wrap) govern; that is what puts the left edge under the logo and the right edge
   under the Menu toggle, since the masthead bar is the same .wrap box.
   It has been wrong in both directions: --measure (68ch) made it a narrow centred column,
   and `none` let it run the full viewport width. Neither is .wrap. Do not re-add one. */
.legal h1 { font-size: var(--t-h2); }
.legal h2 { font-size: var(--t-h3); margin-top: 2.2rem; }
.legal h3 { font-size: var(--t-lg); margin-top: 1.6rem; }
.legal p, .legal ul, .legal ol { margin-top: 1rem; }
.legal ul, .legal ol { padding-left: 1.4rem; }
.legal li { margin-top: .4rem; }
.legal a { color: var(--brand); text-decoration: underline; }

/* â”€â”€ Image-layout repertoire â€” frames, captions, galleries (immersive, not stiff) â”€â”€ */
.media-frame { position: relative; overflow: hidden; border-radius: var(--radius); }
.media-frame img { width: 100%; height: 100%; object-fit: cover; }
.media-cap { position: absolute; inset: auto 0 0 0; padding: 1rem 1.2rem; color: var(--white);
  background: linear-gradient(0deg, rgba(0,0,0,.6), transparent); font-weight: 600; pointer-events: none; }
.gallery { display: grid; gap: clamp(12px, 2vw, 24px); grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); }
.gallery-frame { position: relative; overflow: hidden; border-radius: var(--radius); aspect-ratio: 4 / 3; }
.gallery-frame img { width: 100%; height: 100%; object-fit: cover; }

/* â”€â”€ CTA: the site-wide link style â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   House rule from the design: the verb is italic, the subject is roman.
   "<em>Discover</em> Dunkirk" â€” never italicise the whole phrase. */
.cta {
  display: inline-block; font-family: var(--font-display); font-style: normal;
  font-size: clamp(15px, 1.15vw, 19px); color: var(--sage);
  padding-bottom: .18em; border-bottom: 1px solid currentColor;
  transition: color .25s var(--ease), border-color .25s var(--ease);
}
.cta em { font-style: italic; }
.cta:hover { color: var(--brand); }
/* No rule under it (the hero uses this) and inherits its surrounding size. */
.cta--bare { border-bottom: 0; padding-bottom: 0; font-size: inherit; }
.cta--light { color: var(--surface); }
.cta--light:hover { color: var(--white); }

/* â”€â”€ Heading wipe â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   The line starts as 20%-black and the sage sweeps across it from the left. One copy
   of the text, no duplicate DOM: a 200%-wide gradient (sage | 20% black) is clipped to
   the glyphs and slid from right to left, so the sage edge travels left to right.
   Gated on .reveal-ready so a JS-less visitor gets plain readable text instead of
   transparent glyphs, and main.js adds .in (immediately, under reduced motion). */
.wipe {
  /* The knobs. --wipe-ghost is the resting state: keep it faint, or the sweep has nothing
     to travel against and reads as no animation at all. Override --wipe-fill per section
     when the heading is not sage. */
  /* --wipe-fill is deliberately NOT declared here. It used to be, and any section that
     overrode it EARLIER in this file silently lost: same specificity, and .wipe came later,
     so .contact__title's ink was overwritten back to sage. The gradient below carries the
     sage default as a var() fallback instead, so a per-section override always wins no
     matter where it sits in the file. */
  --wipe-ghost:   rgba(0, 0, 0, .06);
  /* Settled at 5s, after 1.5 -> 4.5 -> 9 -> 6.5 -> 5. The sweep is the animation you actually watch on scroll, so it
     is the duration that was slowed, not the stagger — with a sweep this long the lines
     already read as sequential and tripling the stagger too would push a six-line heading
     past 8 seconds. Raise --wipe-stagger if the cascade wants to be more pronounced. */
  --wipe-dur:     5s;
  --wipe-stagger: .24s;
}
.reveal-ready .wipe__line {
  background-image: linear-gradient(90deg, var(--wipe-fill, var(--sage)) 0 48%, var(--wipe-ghost) 52% 100%);
  background-size: 200% 100%; background-position: 100% 0; background-repeat: no-repeat;
  -webkit-background-clip: text; background-clip: text;
  color: transparent; -webkit-text-fill-color: transparent;
  transition: background-position var(--wipe-dur) var(--ease);
}
.reveal-ready .wipe.in .wipe__line { background-position: 0 0; }
.wipe__line { display: block; }
.reveal-ready .wipe.in .wipe__line:nth-child(2) { transition-delay: var(--wipe-stagger); }
.reveal-ready .wipe.in .wipe__line:nth-child(3) { transition-delay: calc(var(--wipe-stagger) * 2); }
.reveal-ready .wipe.in .wipe__line:nth-child(4) { transition-delay: calc(var(--wipe-stagger) * 3); }
.reveal-ready .wipe.in .wipe__line:nth-child(5) { transition-delay: calc(var(--wipe-stagger) * 4); }
.reveal-ready .wipe.in .wipe__line:nth-child(6) { transition-delay: calc(var(--wipe-stagger) * 5); }

/* â”€â”€ Fixed-background parallax band â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   The image is position:fixed (pinned to the viewport) and clip-path confines it to
   this window, so the page scrolls past a stationary image. Same approach as The
   Bower: iOS-safe, unlike background-attachment: fixed, which Safari ignores.
   z-index 0 so the section above can overlap it (see .welcome__media--sub). */
.plx {
  position: relative; z-index: 0;
  height: var(--plx-h); clip-path: inset(0);
  background: var(--brand);          /* never flashes white before the image paints */
  --plx-h: 88vh;
  /* Focal point for the cover crop. A tall viewport crops these bands hard on the
     horizontal, so a band whose subject sits off-centre overrides this per instance. */
  --plx-pos: 50% 50%;
}
@supports (height: 88svh) { .plx { --plx-h: 88svh; } }
.plx > img {
  position: fixed; top: 0; left: 0; width: 100%; height: 100vh;
  object-fit: cover; object-position: var(--plx-pos);
}
@supports (height: 100lvh) { .plx > img { height: 100lvh; } }

/* ── .pxz — the other parallax band: it drifts AND zooms ─────────────────────────────
   Same job as .plx, different mechanism, and they are deliberately both kept.

     .plx  pins the image to the viewport and clips it, so the photograph sits dead still
           while the page slides past. Right when you want stillness.
     .pxz  translates and scales a normally-positioned image, so the photograph creeps
           upward and grows very slightly as you pass it.

   .plx cannot do the zoom: scaling a position:fixed child inside a clip-path ancestor is
   the one combination browsers disagree about. .pxz costs a composited layer instead of a
   fixed one, uses no fixed positioning at all (so iOS behaves), and shares the --pz
   contract with main.js and .sec-bleed__media.

   --pz is scroll progress across the band, 0 to 1, written by [data-parallax-zoom] in
   main.js. It defaults to .5 so JS-off gets the neutral mid state: no offset, no zoom.
   --pxz-pos is the object-fit focal point, and matters more here than on .plx: this band
   is ~2:1 on a desktop but PORTRAIT on a phone, so the crop swings a long way. ───────── */
.pxz {
  position: relative; z-index: 0; overflow: hidden;
  height: var(--pxz-h);
  background: var(--brand);            /* never flashes white before the image paints */
  --pxz-h: 88vh;
  --pz: .5;
  --pxz-travel: clamp(28px, 5vw, 80px);   /* slide, each way from centre */
  --pxz-zoom: .14;                        /* total scale gained across the crossing */
  --pxz-pos: 50% 50%;
  /* Zoom progress, and therefore its DIRECTION. Defaults to --pz, so the image starts at
     scale 1 and grows as you scroll. Set it to calc(1 - var(--pz)) on an instance to
     invert: start zoomed in and open out. /location's band does that.
     Both directions are edge-safe — inverting moves the moment of least coverage from the
     top edge at pz=0 to the bottom edge at pz=1, and neither is on screen. */
  --pxz-zoom-t: var(--pz);
}
@supports (height: 88svh) { .pxz { --pxz-h: 88svh; } }

/* Half height on a phone. At 88vh a band filled the whole screen with nothing but
   photograph, which reads as the page having stalled rather than as a moment. 44vh still
   gives a full-bleed band but lets the sections either side of it stay in view.
   Both components, since both ran at 88vh. The svh variants are set too: on iOS Safari
   vh includes the retracting toolbars, so a vh-only rule jumps as they hide. */
@media (max-width: 700px) {
  .plx { --plx-h: 44vh; }
  .pxz { --pxz-h: 44vh; }
}
@supports (height: 44svh) {
  @media (max-width: 700px) {
    .plx { --plx-h: 44svh; }
    .pxz { --pxz-h: 44svh; }
  }
}
.pxz > img {
  /* Oversized by --pxz-travel top and bottom so the slide can never expose an edge; the
     zoom only ever adds coverage on top of that, so it needs no slack of its own. */
  position: absolute; left: 0; width: 100%;
  top: calc(-1 * var(--pxz-travel));
  height: calc(100% + var(--pxz-travel) * 2);
  object-fit: cover; object-position: var(--pxz-pos);
  transform:
    translate3d(0, calc((.5 - var(--pz)) * var(--pxz-travel) * 2), 0)
    scale(calc(1 + var(--pxz-zoom) * var(--pxz-zoom-t)));
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .pxz { --pxz-travel: 0px; --pxz-zoom: 0; }
}

/* ── The same drift + zoom on an inner page's hero photograph ──────────────────────
   Opt-in per instance via data-parallax-zoom on the carousel, so the HOMEPAGE hero is
   untouched — it cross-fades between two slides and does not want a second motion on top.

   .hero__carousel already clips (overflow: hidden) and .hero__slide is inset:0 absolute, so
   the slide only needs oversizing top and bottom the way .pxz's img does. bottom:auto is
   explicit: with top, bottom AND height all set, bottom would be the one ignored, and
   relying on that is the sort of thing that quietly changes.

   Smaller numbers than a full band: this frame is only ~2.4:1 and the heading crosses its
   top edge, so a big slide would read as the picture sliding out from under the type.

   Note the hero is on screen at load, so --pz starts near .5 rather than 0 — the photograph
   arrives already very slightly scaled and keeps opening. That is intended. */
.hero__carousel[data-parallax-zoom] {
  --pz: .5;
  --pxz-travel: clamp(14px, 2.4vw, 38px);
  --pxz-zoom: .09;
  --pxz-zoom-t: var(--pz);
}
.hero__carousel[data-parallax-zoom] .hero__slide {
  top: calc(-1 * var(--pxz-travel));
  bottom: auto;
  height: calc(100% + var(--pxz-travel) * 2);
  transform:
    translate3d(0, calc((.5 - var(--pz)) * var(--pxz-travel) * 2), 0)
    scale(calc(1 + var(--pxz-zoom) * var(--pxz-zoom-t)));
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .hero__carousel[data-parallax-zoom] { --pxz-travel: 0px; --pxz-zoom: 0; }
}

/* â”€â”€ Beach clubhouse â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Two columns: the sage statement left, supporting copy right, both top-aligned.
   Column ratio and gap are measured off the design (533 : 166 : 459 of a 1158px content
   box). The heading reuses the Welcome section's scale rather than adding a new step:
   measuring two separate lines of the design both gave ~27.2px at 1440, which is what
   clamp(19px, 1.9vw, 29px) already produces. */
.club { background: var(--surface); }
.club__note {
  font-family: var(--font-display); font-size: clamp(13px, 1vw, 17px);
  color: var(--ink); margin-top: clamp(10px, 1.4vw, 22px);
}
.club__grid {
  display: grid; align-items: start;
  grid-template-columns: minmax(0, 1.16fr) minmax(0, 1fr);
  column-gap: clamp(30px, 14.3%, 166px);
  margin-top: clamp(20px, 2.4vw, 38px);
}
.club__title { font-size: clamp(19px, 1.9vw, 29px); line-height: 1.45; }

/* Shared body-copy block. NOTE the Welcome section still carries its own 1.85 line-height
   from before this was measured; unify the two if the tighter setting is preferred. */
.prose p { font-size: var(--t-copy); line-height: 1.65; color: var(--ink); }
.prose p + p { margin-top: 1em; }

@media (max-width: 860px) {
  .club__grid { grid-template-columns: 1fr; row-gap: clamp(22px, 4vw, 34px); }
}

/* â”€â”€ Living at Dunkirk: pinned, with a scroll-driven handover between two slides â”€â”€
   Structured so the JS-less state is the useful one: by default both slides simply stack
   down the page, no pin, everything readable. main.js adds .is-run, which gives the
   section its runway height, makes the inner block sticky, and stacks the slides so they
   can cross-fade. Reduced motion never gets .is-run, so it keeps the static version
   rather than a pinned section that cannot animate. */
.live { position: relative; background: var(--surface); }
.live.is-run { height: var(--live-run); --live-run: 240vh; }
@supports (height: 240svh) { .live.is-run { --live-run: 240svh; } }
/* "Living at Dunkirk" is deliberately OUTSIDE the sticky block and scrolls away normally.
   Putting it inside meant the pin engaged the instant the section's top touched the viewport,
   which caught it too early. The pin's own top is the trigger, so that title clears first. */
.live__head { padding-top: clamp(48px, 8vw, 130px); padding-bottom: clamp(12px, 1.6vw, 24px); }
.live.is-run .live__pin {
  position: sticky;
  top: 0;
  min-height: 100vh;
  /* flex-start, not center. Centred, the slide sat mid-screen and it was the IMAGE that
     appeared to lock; the eye had nothing at the top edge to read the pin against. Now the
     slide's own label row is what parks, --live-pin-top from the top of the viewport.
     That value clears the stuck masthead (~90px at desktop) and leaves 10px above the label. */
  display: flex;
  align-items: flex-start;
  --live-pin-top: clamp(0px, 0vw, 20px);
  padding-top: var(--live-pin-top);
}
@supports (height: 100svh) { .live.is-run .live__pin { min-height: 100svh; } }

.live__title { --wipe-fill: var(--ink); }
/* font-style comes from the all-caps block: roman. */

.live__slides { position: relative; width: 100%; }
.live__slide + .live__slide { margin-top: clamp(30px, 5vw, 70px); }   /* static fallback */
.live.is-run .live__slide { opacity: 0; transition: opacity .7s var(--ease); }
.live.is-run .live__slide:first-child { position: relative; }
.live.is-run .live__slide:not(:first-child) { position: absolute; inset: 0; margin-top: 0; }
.live.is-run .live__slide[data-active] { opacity: 1; }

.live__row { display: flex; align-items: baseline; justify-content: space-between; gap: clamp(16px, 3vw, 48px); }
.live__note {
  font-family: var(--font-display); font-size: clamp(12px, .96vw, 17px);
  color: var(--ink); max-width: none; text-align: right;
}
/* The image runs wider than the text column, as drawn: it escapes the wrap's padding by
   --live-bleed on each side. */
.live__media {
  margin: clamp(18px, 2.6vw, 40px) calc(-1 * var(--live-bleed)) 0;
  overflow: hidden; aspect-ratio: 2.45;
  --live-bleed: clamp(0px, 3.2vw, 60px);
}
.live__media img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Line of copy and the CTA under the slides. --live-foot-gap is the 15px below the image;
   .live__media carries no bottom margin, so this margin-top is the whole distance.
   Left-aligned to sit under the slide's label, which is the left-hand end of .live__row. */
.live__foot {
  --live-foot-gap: 15px;
  margin-top: var(--live-foot-gap);
  padding-bottom: clamp(20px, 3vw, 46px);
}
.live__ask {
  font-family: var(--font); font-size: var(--t-copy);
  line-height: 1.65; color: var(--ink); max-width: none; margin: 0;
}
.live__foot .cta { display: inline-block; margin-top: clamp(14px, 1.8vw, 26px); }

@media (max-width: 700px) {
  .live__row { flex-direction: column; gap: .5rem; }
  .live__note { text-align: left; }
  .live__media { aspect-ratio: 16 / 10; }      /* 2.45 is a sliver on a phone */
}

/* â”€â”€ Location â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Heading top-left, copy and CTA pushed to the bottom so they sit level with the foot of
   the square image (space-between, same trick as the Welcome column). Heading is ink, so
   --wipe-fill is overridden as in .heritage__title; "in Salt Rock" is the italic second
   line, indented 4.77em, which is the offset measured off the design. */
.loc { background: var(--surface); }
.loc__grid {
  display: grid; align-items: stretch;
  grid-template-columns: minmax(0, .92fr) minmax(0, 1fr);
  column-gap: clamp(28px, 9%, 90px);
}
/* flex-start, not space-between. space-between spread the leftover height of the (taller)
   image column across the two gaps around the distances, which is what made them so airy;
   no gap value could win against it. The trade-off is that the copy no longer bottom-aligns
   with the foot of the image. Gap is 30% of what it was. */
.loc__left { display: flex; flex-direction: column; justify-content: flex-start; gap: clamp(11px, 2.4vw, 39px); }
/* Clamp max cut 40px -> 36px. "PERFECTLY POSITIONED" is 14.83em with the caps tracking, and
   the .92fr column holds 551px from 1600px up, so 40px needed 593px and wrapped — which would
   have broken the two-line stagger below it. 36px needs 534px. Caps read larger than mixed
   case at the same size (cap-height vs x-height), so this is not a loss of presence. */
.loc__title {
  --wipe-fill: var(--ink);
}
/* FLUSH LEFT, indent dropped. It was 4.77em and then 6.40em, each time computed to right-align
   this line under line 1 — but line 1 is "PERFECTLY / POSITIONED" over two lines in this
   treatment at every width (one line would need 16.8px, under the 23px clamp minimum), so
   there is no single line above to align to. Now matches .sec-split__title em, which is
   flush left by the same logic. */
.loc__title em { display: block; }
/* Distances, sitting in the gap between the title and the copy. As a third flex child of
   .loc__left, space-between drops it into the middle of the column, which is the gap the
   design already leaves there. */
.loc__stats { list-style: none; padding: 0; display: grid; gap: clamp(14px, 1.9vw, 28px); }
/* One line per item: icon, place, then the distance last. Everything is centred against the
   row rather than sitting on a shared baseline, since the icon leads and both pieces of text
   are now the same size. */
.loc__stats li {
  display: flex; align-items: center; gap: .6em; flex-wrap: wrap;
  font-family: var(--font-display); font-size: clamp(14px, 1.1vw, 18px);
  line-height: 1.3; color: var(--muted);
}
/* Place, colon and distance are one text run, so the literal space in the markup is what
   separates them: an ordinary word space, not a flex gap. The row's gap now only applies
   between the icon and this run. */
.loc__text { color: inherit; }
.loc__colon { margin-left: .05em; }
/* Sized on its own clamp rather than in em: it leads the row, so it has no text span to
   inherit from. The three source viewBoxes differ, so the default preserveAspectRatio
   centres each glyph inside the same square box. transform, not margin: with the row
   centre-aligned a negative margin-top would be halved by the centring and push it down. */
.loc__icon {
  flex: none; margin-right: .2em; transform: translateY(-1px);
  width: clamp(20px, 1.7vw, 28px); height: clamp(20px, 1.7vw, 28px);
}

.loc__copy { display: grid; }
/* The CTA is its own block now, sitting AFTER the distances rather than inside the copy.
   Right-aligned, as it was when .loc__copy justified its children to the end. */
.loc__cta { margin: 0; text-align: right; }
.loc__media { margin: 0; overflow: hidden; aspect-ratio: 1; }
.loc__media img { width: 100%; height: 100%; object-fit: cover; display: block; }

@media (max-width: 860px) {
  .loc__grid { grid-template-columns: 1fr; row-gap: clamp(24px, 5vw, 40px); }
  .loc__media { order: -1; }
  .loc__left { gap: clamp(22px, 4vw, 36px); }
  /* The 2.2em phone indent went with the desktop one: it was a reduced version of the
     right-align offset, and there is no line above to align to any more. */
}

/* â”€â”€ Beach club rail: cards drift leftward, draggable by mouse or finger â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Before JS runs the rail is a native horizontal scroller, so it still works with JS off.
   main.js adds .is-live, which hands control to the transform and turns native scrolling
   off. touch-action: pan-y means a vertical swipe still scrolls the page while a
   horizontal drag belongs to the rail. */
.rail {
  background: var(--white); overflow-x: auto; overflow-y: hidden; touch-action: pan-y;
  --rail-h: clamp(179px, 20.3vw, 325px);
}
.rail::-webkit-scrollbar { height: 0; }
.rail.is-live { overflow: hidden; cursor: grab; user-select: none; -webkit-user-select: none; }
.rail.is-drag { cursor: grabbing; }
.rail__track { display: flex; gap: var(--rail-gap); width: max-content; will-change: transform; --rail-gap: 3px; }
/* Uniform height, variable width: the rail reads as one clean band while the cards vary.
   Driving off height (with aspect-ratio deriving the width) is what makes mixed widths
   possible at all â€” a width-driven card would need a different ratio per card to stay
   level. --rail-h is 35% down from the first pass, which turned the 4:5 portrait into a
   1.23 landscape; --wide cards are then 25% longer again. */
.rail__card {
  position: relative; flex: none; overflow: hidden;
  height: var(--rail-h); aspect-ratio: var(--rail-ratio);
  --rail-ratio: 1.23;
}
.rail__card--wide { --rail-ratio: 1.54; }
.rail__card img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The captions and their scrim were removed — the rail is photographs only now, so the alt
   text on each image is the only description a card carries. */

/* â”€â”€ Forest band: parallax photo with a green panel that wipes down over it â”€â”€â”€â”€â”€â”€
   Reuses .plx for the pinned image. The panel is revealed with clip-path rather than
   scaleY, because scaling the panel would squash the text inside it: clip-path moves the
   reveal edge and leaves children untouched. main.js adds .is-revealed at 50% visibility. */
.forest__strip {
  /* --forest-strip-shift moves the whole panel left of where --edge would put it. The copy is
     laid out within the panel, so it travels with it and keeps its position.
     Tapered below 1100px: this is a composition choice against a photograph, so it scales
     with the frame rather than staying a fixed offset on small screens. */
  --forest-strip-shift: 175px;
  position: absolute;
  inset: 0 calc(var(--edge) + var(--forest-strip-shift)) 0 auto;
  z-index: 1;
  width: var(--forest-strip-w);
  /* Two gradients over the brand colour — one down from the top edge, one up from the
     bottom — so the panel does not read as a flat rectangle against the photograph.
     Layered on the background rather than added as pseudo-elements, so the clip-path
     reveal still wipes the whole panel as one thing. --strip-fade is how far each
     reaches in; the last layer in the shorthand is the base colour. */
  --strip-fade: 22%;
  background:
    linear-gradient(to bottom, rgba(0, 0, 0, .4), transparent var(--strip-fade)),
    linear-gradient(to top,    rgba(0, 0, 0, .4), transparent var(--strip-fade)),
    var(--brand);
  color: var(--surface);
  display: grid; align-content: start; justify-items: center;
  padding-top: var(--forest-copy-top);
  clip-path: inset(0 0 100% 0);
  transition: clip-path 1.1s var(--ease);
  --forest-strip-w:   clamp(300px, 30.7%, 460px);
  --forest-copy-top:  clamp(44px, 15vh, 160px);
}
@media (max-width: 1100px) { .forest__strip { --forest-strip-shift: 13vw; } }
@supports (height: 15svh) { .forest__strip { --forest-copy-top: clamp(44px, 15svh, 160px); } }
.forest__strip.is-revealed { clip-path: inset(0 0 0 0); }

/* This band is .pxz for its FRAME only — clipped, cover-cropped, full-bleed — with the drift
   and zoom switched off. Both are zeroed rather than the section being reverted to .plx,
   because .plx pins its image to the viewport and cannot carry a second stacked layer.

   The motion had to go: the cut-out is aligned to the photograph, so it moves with it, and
   once it moves it travels across the panel and over the copy inside it. With both knobs at
   zero the img's transform resolves to identity and its box is exactly the frame, so the two
   layers sit still and stay registered. data-parallax-zoom is off the markup too, so main.js
   does not track it at all. */
.pxz.forest { --pxz-travel: 0px; --pxz-zoom: 0; }

/* The cut-out foreground. It needs no sizing of its own: .pxz > img already gives it the
   same box, object-fit and object-position as the background, and the two files share one
   intrinsic size, so they land on top of each other exactly.
   z-index 2 is the only addition — it lifts the plant above the panel's z-index 1. */
/* z-index 2 lifts the plant above the panel (z-index 1) so it reads as standing in front of
   it. pointer-events: none is what makes that safe: this is a FULL-FRAME image spanning the
   whole band, and a transparent pixel in an <img> still captures clicks — so without it the
   cut-out sat over the panel and swallowed every click on the "Explore The Forest" CTA. It is
   purely decorative, so it should never be a hit target anyway. */
.forest__cut { z-index: 2; pointer-events: none; }
@media (max-width: 700px) {
  /* Below this the panel stops being a panel and becomes a full-width scrim over the whole
     band, so there is no edge for the plant to stand clear of — it would just sit brightly
     on top of the copy. Hidden rather than fought with. */
  .forest__cut { display: none; }
}

/* Right-aligned block, centred in the panel. Line 1 is pushed left of line 2's right edge
   by 1.53em, which is the stagger measured off the design. */
.forest__copy { text-align: right; }
.forest__title {
  /* No font-family/font-weight here: they come from the eyebrow-treatment block. This rule
     sits AFTER that block at equal specificity, so declaring them here silently overrode it
     and left this one title in Sunshine while every sibling moved to Montserrat. */
  color: var(--surface);   /* light: this one sits on the dark green panel */
}
.forest__title .forest__l1 { display: block; margin-right: 1.53em; }
.forest__title em { display: block; }
.forest__cta { margin-top: 1.1em; }

@media (max-width: 700px) {
  /* The panel stops being a panel: it becomes a full-width scrim so the text stays legible. */
  .forest__strip { inset: 0; width: auto; justify-items: center; padding-inline: var(--pad); }
  .forest__copy { text-align: center; }
  .forest__title .forest__l1 { margin-right: 0; }
}

/* â”€â”€ Welcome section â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
.welcome { position: relative; z-index: 1; background: var(--white); }
/* Two lines, and they must STAY two lines. Measured in Sunshine: the longer line
   ("Dunkirk is a 20-year-old legacy coastal estate in Salt Rock,") is 33.79em wide, and
   the wrap gives at most 1112px of content, so anything above ~32px forces a wrap and the
   heading reads as four lines. 29px keeps ~6% of headroom at every width down to 760px.
   No max-width: the wrap already constrains it, and a ch-based cap was what broke it.
   Re-measure this if the copy changes:
     php -r '$b=imagettfbbox(100,0,"fonts/Sunshine-Regular.ttf",$line); echo ($b[2]-$b[0])/100;' */
.welcome__title { font-size: clamp(19px, 1.9vw, 29px); line-height: 1.32; }
/* Two columns, ONE row. The left column is a flex column that stretches to the row height
   and pushes its image to the bottom with space-between, so the offset below the main
   image is measured off the main image itself rather than off the copy's height (which
   varies with wrapping and can't be expressed in CSS). */
.welcome__grid {
  display: grid; align-items: stretch;
  /* Equal columns with a wide gutter. Derived rather than guessed: the photograph had to
     come down 10% in width WITHOUT the copy column growing to absorb it, so the freed width
     goes to the gutter instead. At 1440 that is right 544px -> 489px (-10.0%), left 489px
     (unchanged), gutter 79px -> 134px. Hence 1fr/1fr and 9.3vw. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: clamp(40px, 9.3vw, 150px);
  margin-top: clamp(34px, 4.5vw, 72px);
}
.welcome__left {
  display: flex; flex-direction: column; justify-content: space-between;
  gap: clamp(28px, 4vw, 64px);
}
.welcome__copy { display: grid; gap: 1.1rem; justify-items: end; text-align: left; }
.welcome__copy p { font-size: var(--t-copy); line-height: 1.85; color: var(--ink); }
.welcome__media { margin: 0; overflow: hidden; }
.welcome__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* align-self:start so aspect-ratio sets the height instead of the stretched row. */
.welcome__media--main { align-self: start; aspect-ratio: 1; }

/* The welcome image was the trial for the treatment above. It now takes the shared rule
   like every other frame — at its 489px height the percentages resolve to ±95px, the
   exact travel it was signed off at, so nothing about it changed. */
/* Portrait now, and it DOES cross onto the parallax band below. space-between has already
   pushed it to the foot of the left column, so the negative bottom margin only has to clear
   the section's own padding-bottom before the overlap begins — hence both terms.
   .welcome carries z-index 1 and .plx z-index 0, which is what lets it paint over. */
.welcome__media--sub {
  /* +50px on the previous clamp(20px, 3vw, 56px), with the vw term re-fitted so it still
     interpolates instead of sitting on the floor: 6.5vw passes through 94px at 1440. */
  --welcome-overlap: clamp(70px, 6.5vw, 106px);
  aspect-ratio: 3 / 4; flex: none;
  width: min(100%, 260px); margin-left: clamp(8px, 3vw, 52px);
  margin-bottom: calc(-1 * (var(--section-y) + var(--welcome-overlap)));
}

/* Plain --section-y now. The +20px here used to pair with the old -20px nudge on the sub
   image, which stopped just short of the parallax band. That image crosses the boundary
   outright now, and .welcome__media--sub's negative margin is measured against THIS value,
   so the two have to stay in step — change one and the overlap changes with it. */
.welcome { padding-bottom: var(--section-y); }

/* â”€â”€ Estate heritage â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
   Image left, heading + two copy columns right. Heading is ink here rather than sage,
   so --wipe-fill is overridden; "heritage" is the italic half via <em>. */
.heritage { background: var(--mist); padding-block: calc(var(--section-y) * 1.15); }
.heritage__grid {
  display: grid; align-items: center;
  grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr);
  column-gap: clamp(28px, 4vw, 72px);
}
.heritage__media { margin: 0; overflow: hidden; aspect-ratio: 215 / 244; }
.heritage__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.heritage__title {
  --wipe-fill: var(--ink);
}
.heritage__cols {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(18px, 2.2vw, 38px);
  margin-top: clamp(22px, 3vw, 46px);
}
/* Deliberately a step under --t-copy: two narrow columns beside a portrait photograph.
   +2px on the 12/14px it was; vw .86 -> 1.0 keeps the fluid band (12px at 1395px -> 14px at
   1400px, same place). */
.heritage__cols p { font-size: clamp(14px, 1vw, 16px); line-height: 1.85; color: var(--ink); max-width: none; }

@media (max-width: 860px) {
  .heritage__grid { grid-template-columns: 1fr; row-gap: clamp(24px, 5vw, 40px); }
  .heritage__cols { grid-template-columns: 1fr; }
}

/* â”€â”€ Lifestyle & amenities â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
.amen { background: var(--surface); }

/* Scroll-grow frame. Same approach as The Bower: the frame escapes the section's side padding
   symmetrically (not 100vw, which would go off-centre by the scrollbar width) so it can grow
   out to --amen-frame-edge from each page edge, and main.js scales it from --amen-grow-start
   to 1 as it rises. Static at full size with JS off / reduced motion.

   This was .amen__video holding a looping timelapse; it is a still now and the name went with
   it. --amen-grow-start keeps ITS name because js/main.js reads that exact custom property. */
.amen__frame {
  margin-inline: calc(var(--amen-frame-edge) - var(--pad));
  transform-origin: 50% 50%; will-change: transform;
  --amen-frame-edge:  clamp(12px, 3vw, 50px);
  --amen-grow-start:  .55;
}
.amen__frame img {
  display: block; width: 100%; aspect-ratio: 1134 / 507; object-fit: cover;
  background: var(--brand-dark);        /* never a white box before the image paints */
}

.amen__title {
  font-size: clamp(18px, 1.72vw, 29px); line-height: 1.5;
  margin-top: clamp(18px, 2.4vw, 38px);
}

/* Accordion + image. Deliberately roomier than the design: each row gets real vertical
   padding and a hairline, so it reads as a list you can operate rather than a caption. */
.amen__grid {
  display: grid; align-items: start;
  grid-template-columns: minmax(0, .78fr) minmax(0, 1.22fr);
  column-gap: clamp(28px, 5vw, 90px);
  margin-top: clamp(44px, 7vw, 110px);
}
/* The left column stretches to the full row height — which the taller photograph sets — so
   the arrows can be pushed to its BOTTOM rather than sitting straight under the last row.
   align-self overrides .amen__grid's align-items: start for this one column. If the list ever
   grows taller than the image the stretch simply does nothing and the arrows follow the rows. */
.amen__side { align-self: stretch; display: flex; flex-direction: column; }
.amen__list { display: grid; }

/* ── Carousel arrows, bottom left ─────────────────────────────────────────────────
   display:none until main.js adds data-amen-live to the wrapper. With JS off the tabs still
   work as plain buttons but these two would do nothing, so they are not rendered visible at
   all — the same contract .rail uses. */
.amen__nav { display: none; margin-top: auto; padding-top: clamp(28px, 4vw, 56px); gap: 12px; }
[data-amen-live] .amen__nav { display: flex; }
.amen__arrow {
  /* --amen-arrow is the ONE knob: the disc and the chevron inside it both come off it, so
     resizing is a single number. Was 44px (the bare tap-target minimum) with a 16px chevron;
     56px reads as a deliberate control rather than the smallest allowable one. The .36 ratio
     is carried over from 16/44, which is what kept the chevron from crowding the ring. */
  --amen-arrow: 56px;
  display: grid; place-items: center;
  width: var(--amen-arrow); height: var(--amen-arrow);
  background: none; border: 1px solid var(--mist); border-radius: 50%;
  /* Resting tone is the rows' own rgba(0,0,0,.42), so the controls read as part of the list
     rather than as a separate widget bolted under it. */
  color: rgba(0, 0, 0, .42);
  transition: color .3s var(--ease), border-color .3s var(--ease);
}
.amen__arrow:hover { color: var(--ink); border-color: var(--sage); }
/* Height comes from the global svg reset (height: auto) and the square 24x24 viewBox. */
.amen__arrow svg { width: calc(var(--amen-arrow) * .36); }
/* No separators. --amen-row-pad is the single knob for row spacing: it is the padding on
   EACH side, so the gap between two rows is twice it. Now half what it was — the gap went
   from 20px to 10px at the clamp cap. */
.amen__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.2em;
  width: 100%;
  background: none;
  border: 0;
  padding: var(--amen-row-pad) 0;
  text-align: left;
  cursor: pointer;
  font-family: var(--font-display);
  font-size: clamp(14px, 1.05vw, 18px);
  line-height: 1.3;
  color: rgba(0, 0, 0, .42);                       /* the "lighter black" resting state */
  transition: color .3s var(--ease);
  --amen-row-pad: clamp(4px, .52vw, 5px);
}
.amen__item:hover { color: rgba(0, 0, 0, .72); }
.amen__item[aria-selected="true"] { color: var(--ink); }

.amen__media { position: relative; aspect-ratio: 3 / 2; overflow: hidden; }
.amen__media img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; transition: opacity .6s var(--ease);
}
.amen__media img[data-active] { opacity: 1; }

@media (max-width: 860px) {
  .amen__grid { grid-template-columns: 1fr; row-gap: clamp(26px, 5vw, 40px); }
  .amen__media { order: -1; }                      /* image leads on a phone */
  /* Nothing taller sits beside the column here, so margin-top:auto has no space to claim and
     would collapse the arrows onto the last row. A fixed gap instead. */
  .amen__nav { margin-top: 0; padding-top: clamp(22px, 5vw, 32px); }
}

/* Counter-drift on scroll: main.js writes --drift (px) per element from its position in
   the viewport. Defaults to 0, so with JS off or under reduced motion this is translateY(0)
   and nothing is left half-transformed. */
[data-drift] { --drift: 0; transform: translateY(calc(var(--drift) * 1px)); will-change: transform; }

@media (max-width: 860px) {
  .welcome__grid { grid-template-columns: 1fr; row-gap: clamp(24px, 5vw, 40px); }
  .welcome__media--main { order: -1; }           /* big image leads on a phone */
  .welcome__left { gap: clamp(24px, 5vw, 40px); }
  .welcome__copy { justify-items: start; }
  .welcome__media--sub { margin-left: 0; margin-bottom: 0; width: min(100%, 280px); }
  .welcome__title { max-width: none; }
}

/* â”€â”€ Skip link: visible only on keyboard focus â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ */
.skip-link { position: absolute; left: -9999px; top: 0; z-index: 100; }
.skip-link:focus { left: 1rem; top: 1rem; background: var(--white); color: var(--ink);
  padding: .7em 1.2em; border-radius: var(--radius); box-shadow: 0 6px 24px rgba(0,0,0,.2); }
/* â”€â”€ Reduced motion: every animation must have a no-motion path (spec Â§1.5). â”€â”€ */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
}

/* ── Gallery (/gallery) ────────────────────────────────────────────────────────────
   Uniform 3:2 cells: every photograph in the bank is landscape and near 3:2 already, so
   object-fit trims at most a few percent and nothing important is ever cropped away.
   The grid is 3-up, stepping to 2 and 1 with the same breakpoints the sizes attribute
   in gallery.php advertises — change one and change the other. */
.gal-intro { padding-bottom: 0; }
.gal__title {
  /* The statement-serif treatment (h1 already brings --font-display / weight 400);
     default sage wipe fill, like the other display headings. */
  font-size: clamp(26px, 3vw, 44px); line-height: 1.2;
  margin-top: clamp(14px, 1.8vw, 28px);
}
.gal__lede { max-width: 62ch; margin-top: clamp(14px, 1.8vw, 24px); padding-bottom: 60px; }

.gal { padding-top: clamp(30px, 4.4vw, 64px); }
.gal__grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(7px, calc(1.8vw - 5px), 19px);
}
@media (max-width: 1100px) { .gal__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 700px)  { .gal__grid { grid-template-columns: 1fr; } }
.gal__item { margin: 0; aspect-ratio: 3 / 2; overflow: hidden; }
.gal__link { display: block; width: 100%; height: 100%; cursor: zoom-in; }
.gal__item img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform .5s var(--ease);
}
.gal__link:hover img, .gal__link:focus-visible img { transform: scale(1.06); }

/* Infinite-scroll sentinel: hidden until main.js confirms there is more to reveal. */
.gal__sentinel { display: grid; place-items: center; margin-top: clamp(24px, 3.4vw, 40px); color: var(--brand); }
.gal__sentinel[hidden] { display: none; }      /* same [hidden]-vs-display:grid clash as .gbox */
.gal__spinner { animation: gal-spin 1s linear infinite; }
@keyframes gal-spin { to { transform: rotate(360deg); } }

/* ── Gallery lightbox ─────────────────────────────────────────────────────────────
   z-index 80: over everything including the menu overlay (60). The buttons are 44px
   circles (comfortably past the 24px WCAG floor); prev/next pin to the viewport sides so
   they never overlap a portrait-ish caption. */
html.gbox-open { overflow: hidden; }
.gbox {
  position: fixed; inset: 0; z-index: 80;
  display: grid; place-items: center;
  padding: clamp(16px, 4vw, 64px);
  opacity: 0; transition: opacity .3s var(--ease);
}
/* display:grid above beats the UA's [hidden] { display:none }, so without this the CLOSED
   lightbox is a full-screen invisible layer that eats every click on the page. */
.gbox[hidden] { display: none; }
.gbox.is-open { opacity: 1; }
.gbox__backdrop { position: absolute; inset: 0; background: rgba(10, 14, 12, .88); cursor: zoom-out; }
.gbox__stage { position: relative; margin: 0; max-width: min(1200px, 100%); }
.gbox__img {
  display: block; max-width: 100%; max-height: 80vh; width: auto; height: auto;
  margin-inline: auto; box-shadow: 0 18px 60px rgba(0, 0, 0, .5);
}
.gbox__btn {
  position: absolute; z-index: 1;
  display: grid; place-items: center;
  width: 44px; height: 44px; padding: 0; border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .35);
  background: none; color: var(--white); cursor: pointer;
  transition: background .25s var(--ease), color .25s var(--ease);
}
.gbox__btn:hover { background: var(--white); color: var(--ink); }
.gbox__btn svg { width: 22px; height: 22px; }
.gbox__close { top: clamp(14px, 2.4vw, 28px); right: clamp(14px, 2.4vw, 28px); }
.gbox__prev { left: clamp(10px, 2vw, 26px); top: 50%; transform: translateY(-50%); }
.gbox__next { right: clamp(10px, 2vw, 26px); top: 50%; transform: translateY(-50%); }
