/* ==========================================================================
   UPLOADING: put this file on the server BEFORE index.html.

   index.html references it as style.css?v=<hash of this file>. Upload the HTML
   first and it starts advertising a URL the server cannot yet satisfy; the
   first request in that gap makes Cloudflare cache the OLD stylesheet under
   the NEW URL, and it serves that for the next four hours no matter what you
   upload afterwards. That has happened on this site, and it is invisible from
   the origin: curl with a cache-busting query string reads straight past the
   edge and reports everything as fine.

   Order: assets, then css/ and js/, then index.html last.
   ========================================================================== */

/* ==========================================================================
   Milkhouse Gate - One Milkhouse Gate, Guildford GU1 3EZ
   ========================================================================== */

/* --- Fonts ---------------------------------------------------------------- */

@font-face {
  font-family: 'Montserrat';
  src: url('../assets/fonts/montserrat-variable.woff2') format('woff2-variations');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Roboto Condensed';
  src: url('../assets/fonts/roboto-condensed-variable.woff2') format('woff2-variations');
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

/* Metric-matched fallback faces, so the system font that paints first occupies
   exactly the same box as the webfont that replaces it on swap, instead of
   reflowing every heading/paragraph on the page when the swap happens.
   Backed by Arial/Helvetica (present, or metric-compatible via OS/fontconfig
   substitution, on effectively every platform) purely so ascent/descent/
   line-gap/size-adjust have a real font to apply to - the override values
   below replace that font's own metrics outright, so it does not matter
   which of the two local() sources actually resolves.

   Values were measured, not guessed: ascent-override/descent-override/
   line-gap-override come from each webfont's own OS/2 or hhea metrics
   (whichever set has the USE_TYPO_METRICS bit set), read from the woff2
   files with fontTools:
     Montserrat        - unitsPerEm 1000, typo ascender 968, descender -251, line gap 0
     Roboto Condensed  - unitsPerEm 2048, hhea ascender 1900, descender -500, line gap 0
   size-adjust comes from comparing canvas measureText() widths of a fixed
   test string set in each real webfont against Arial, at the weight each
   family is predominantly used at on this page (700 for headings, 400 for
   body copy). */
@font-face {
  font-family: 'Montserrat Fallback';
  src: local('Arial'), local('Helvetica');
  ascent-override: 96.8%;
  descent-override: 25.1%;
  line-gap-override: 0%;
  size-adjust: 109.56%;
}
@font-face {
  font-family: 'Roboto Condensed Fallback';
  src: local('Arial'), local('Helvetica');
  ascent-override: 92.77%;
  descent-override: 24.41%;
  line-gap-override: 0%;
  size-adjust: 89.09%;
}

/* --- Tokens --------------------------------------------------------------- */

:root {
  --navy:  #172D4B;
  --blue:  #36A9E1;
  --sky:   #99D7F7;
  --white: #FFFFFF;
  --ink:   #1B2430;
  --hair:  rgb(255 255 255 / 0.22);

  /* The brochure's blue-to-navy gradient, plus a flat navy scrim over it.
     The raw gradient alone paints near-pure --blue across the top-left of
     every .section--dark - exactly where the eyebrow, h2, lede and first
     feature row sit - where --sky measured as low as 1.82:1 and white 2.91:1
     against the real composited backdrop.

     A flat navy scrim is the least destructive fix available. Navy over navy
     is a no-op, so the 62%-100% end of the gradient still renders at exactly
     #172D4B and the whole darkening falls on the blue end, tapering off
     naturally as the gradient approaches navy. The brand gradient survives;
     it simply starts at a deeper steel blue.

     The alpha was swept, not guessed (tools/contrast-audit.mjs, at 375px and
     1440px): 0.55 still leaves four elements failing, 0.58 leaves two, and
     0.60 is the first value at which everything passes - but only just, with
     the worst element at 4.58:1 against a 4.5:1 requirement. 0.62 buys real
     headroom (worst 4.71:1) for a negligible amount of extra darkening, so
     that a font-rendering or antialiasing difference on another platform
     cannot tip an element back under. */
  --grad:
    linear-gradient(rgb(23 45 75 / 0.62), rgb(23 45 75 / 0.62)),
    linear-gradient(115deg, var(--blue) 0%, var(--navy) 62%);

  --font-head: 'Montserrat', 'Montserrat Fallback', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-body: 'Roboto Condensed', 'Roboto Condensed Fallback', system-ui, -apple-system, 'Segoe UI', sans-serif;

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 1rem;
  --space-4: 1.5rem;
  --space-5: 2.5rem;
  --space-6: 4rem;
  --space-7: 6rem;
  --space-8: 9rem;

  --wrap:  78rem;
  --nav-h: 4.75rem;

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

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

*, *::before, *::after { box-sizing: border-box; }

html {
  scroll-padding-top: var(--nav-h);
  -webkit-text-size-adjust: 100%;
}
/* Smooth scrolling is motion, so it is opted into under the same positive
   guard as everything else rather than declared here and switched off again
   in a prefers-reduced-motion: reduce block. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
  line-height: 1.65;
  color: var(--ink);
  background: var(--white);
  overflow-x: hidden;
}

img, picture, svg { display: block; max-width: 100%; height: auto; }

h1, h2, h3, h4 {
  font-family: var(--font-head);
  font-weight: 700;
  line-height: 1.12;
  margin: 0 0 var(--space-3);
  text-wrap: balance;
}

/* Each term 20% down from clamp(2rem, 1.2rem + 3.6vw, 4rem): the heading grew
   a line longer when "To Let:" moved into it, and at the old size it dominated
   the hero. Reducing the whole clamp rather than just the cap keeps the same
   growth curve across viewports. */
h1 { font-size: clamp(1.6rem, 0.96rem + 2.88vw, 3.2rem); letter-spacing: 0.01em; }
h2 { font-size: clamp(1.6rem, 1.1rem + 2.2vw, 2.75rem); letter-spacing: 0.02em; }
h3 { font-size: clamp(1.15rem, 1rem + 0.7vw, 1.5rem); letter-spacing: 0.04em; }

p { margin: 0 0 var(--space-3); max-width: 62ch; }

a { color: inherit; }

ul { margin: 0; padding: 0; list-style: none; }

table { border-collapse: collapse; width: 100%; }

/* Visible focus for keyboard users on every interactive element. */
:focus-visible {
  outline: 3px solid var(--sky);
  outline-offset: 3px;
}

/* For fragments that must not be split across a line break - the postcode in
   the hero address, which was breaking between "GU1" and "3EZ" on phones.
   The &nbsp; in the markup does the work; this is belt and braces for anything
   that ends up here with an ordinary space. */
.nowrap { white-space: nowrap; }

.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;
}

/* --- Layout --------------------------------------------------------------- */

.wrap {
  width: min(100% - 2.5rem, var(--wrap));
  margin-inline: auto;
}

.section {
  padding-block: clamp(3.5rem, 2rem + 6vw, var(--space-8));
  position: relative;
}

.section--dark {
  background: var(--grad);
  color: var(--white);
}
.section--dark h2, .section--dark h3 { color: var(--white); }

.section--light {
  background: var(--white);
  color: var(--ink);
}
.section--light h2, .section--light h3 { color: var(--navy); }

.eyebrow {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 0.8125rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  margin: 0 0 var(--space-2);
  /* --navy, not --blue: --blue is only 2.65:1 on white and fails AA at this
     size. Per the design spec, accent blue on a light background is for rules
     and decoration only - the .rule directly beneath this keeps it. */
  color: var(--navy);
}
.section--dark .eyebrow { color: var(--sky); }

/* A short accent rule beneath section headings, echoing the brochure. */
.rule {
  width: 4rem;
  height: 3px;
  background: var(--blue);
  border: 0;
  margin: 0 0 var(--space-4);
}
.section--dark .rule { background: var(--sky); }

/* --- Buttons -------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.75rem 1.5rem;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 0.8125rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  border: 2px solid var(--blue);
  border-radius: 0;
  background: var(--blue);
  /* --navy, not --white. White on --blue measures 2.65:1, which fails AA at
     this size, and nobody had measured it because the button shipped `hidden`
     until CONFIG.BROCHURE_URL was set. Navy on --blue is 5.25:1 and keeps the
     accent fill, which swapping the fill for navy would have lost. The hover
     state is unaffected: it goes to a transparent fill with --blue text, which
     over the navy bar measures 5.25:1 the other way round. */
  color: var(--navy);
  cursor: pointer;
  transition: background 0.25s var(--ease), color 0.25s var(--ease),
              border-color 0.25s var(--ease);
}
.btn:hover, .btn:focus-visible {
  background: transparent;
  color: var(--blue);
}
.section--dark .btn:hover, .section--dark .btn:focus-visible { color: var(--sky); }

.btn--ghost {
  background: transparent;
  border-color: var(--white);
  color: var(--white);
}
.btn--ghost:hover, .btn--ghost:focus-visible {
  background: var(--white);
  color: var(--navy);
}

/* --- Motion --------------------------------------------------------------- */
/* Every animation lives inside this guard. With reduced motion requested,
   nothing below applies and .reveal elements stay at their final position. */

@media (prefers-reduced-motion: no-preference) {
  .js .reveal {
    opacity: 0;
    transform: translate3d(0, 24px, 0);
    transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
  }
  .js .reveal.is-visible {
    opacity: 1;
    transform: none;
  }
}

/* --- Stagger -------------------------------------------------------------- */
/* --stagger-i is set per child by the reveal module. */

@media (prefers-reduced-motion: no-preference) {
  .js [data-stagger] > * {
    opacity: 0;
    transform: translate3d(0, 18px, 0);
    transition:
      opacity 0.55s var(--ease) calc(var(--stagger-i, 0) * 60ms),
      transform 0.55s var(--ease) calc(var(--stagger-i, 0) * 60ms);
  }
  .js [data-stagger].is-visible > * {
    opacity: 1;
    transform: none;
  }
}

/* --- Skip link ------------------------------------------------------------ */

.skip-link {
  position: absolute;
  top: -100%;
  left: var(--space-3);
  z-index: 200;
  padding: 0.75rem 1.25rem;
  background: var(--navy);
  color: var(--white);
  font-family: var(--font-head);
  font-weight: 600;
  text-decoration: none;
}
.skip-link:focus { top: var(--space-3); }

/* --- Navigation ----------------------------------------------------------- */

.nav {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 100;
  background: transparent;
  transition: background 0.35s var(--ease), box-shadow 0.35s var(--ease);
}
/* Two separate thresholds, deliberately.

   The background arrives as soon as the page has scrolled the height of the bar
   itself (~76px). Before this, the bar stayed transparent until 80vh, so the
   nav links spent most of the hero sitting straight on top of the h1 and the
   suite figures, which read as a mess rather than as a transparent header.

   The brand keeps the later 80vh threshold (.is-stuck) so it appears once the
   hero's own logo has gone, not on top of it. See .nav__brand below. */
.nav.is-solid {
  background: var(--navy);
  box-shadow: 0 2px 24px rgb(0 0 0 / 0.28);
}

.nav__inner {
  width: min(100% - 2.5rem, var(--wrap));
  margin-inline: auto;
  min-height: var(--nav-h);
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.nav__brand { flex: 0 0 auto; line-height: 0; }
.nav__logo  { width: 160px; height: auto; }

/* Brand handoff. The hero carries the full lockup, so printing the same
   artwork again in the bar 120px above it reads as a duplicate rather than a
   choice. While the hero is on screen the bar shows navigation only; the
   brand arrives with the navy background once the hero is behind us, at which
   point it is the only thing anchoring the page.

   Gated on .js and driven by `visibility`, not opacity alone, for two
   reasons: without script .is-stuck never gets set, so the brand must default
   to visible; and an invisible-but-focusable "home" link would put a
   silent stop in the keyboard tab order. `visibility` is deliberately
   transitioned at 0s (delayed out, immediate in) so it flips at the ends of
   the opacity fade rather than being animated. The box is never removed from
   the flex row, so nothing else in the bar shifts. */
.js .nav__brand {
  opacity: 0;
  visibility: hidden;
  transform: translateY(-0.375rem);
}
.js .nav.is-stuck .nav__brand,
.js .nav.is-menu-open .nav__brand {
  opacity: 1;
  visibility: visible;
  transform: none;
}

@media (prefers-reduced-motion: no-preference) {
  .js .nav__brand {
    transition:
      opacity 0.4s var(--ease),
      transform 0.4s var(--ease),
      visibility 0s linear 0.4s;
  }
  .js .nav.is-stuck .nav__brand,
  .js .nav.is-menu-open .nav__brand {
    transition:
      opacity 0.4s var(--ease),
      transform 0.4s var(--ease),
      visibility 0s linear 0s;
  }
}

.nav__menu {
  margin-inline-start: auto;
  display: flex;
  align-items: center;
  gap: var(--space-5);
}
.nav__menu > ul {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}
/* Scoped to the list, not the whole menu. The Download Brochure button is a
   sibling of the <ul> inside .nav__menu, so an unscoped `.nav__menu a` also hit
   the button and gave it the nav link's transparent underline. That was then
   undone with `border-bottom: 0 !important`, which stripped the bottom edge off
   a .btn whose border is meant to run all the way round: invisible while the
   button is filled, and three-sided the moment hover made the fill transparent. */
.nav__menu > ul a {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 0.75rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--white);
  padding-block: 0.5rem;
  border-bottom: 2px solid transparent;
  transition: border-color 0.25s var(--ease), color 0.25s var(--ease);
}
.nav__menu > ul a:hover { color: var(--sky); }
.nav__menu > ul a.is-active { border-bottom-color: var(--sky); color: var(--sky); }

.nav__cta { padding: 0.6rem 1.15rem; }
.nav__cta[hidden] { display: none; }

/* Hamburger */
.nav__toggle {
  display: none;
  margin-inline-start: auto;
  width: 2.75rem; height: 2.75rem;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
}
.nav__toggle-bars,
.nav__toggle-bars::before,
.nav__toggle-bars::after {
  display: block;
  width: 26px; height: 2px;
  background: var(--white);
  margin-inline: auto;
  transition: opacity 0.2s linear;
}
.nav__toggle-bars::before,
.nav__toggle-bars::after { content: ''; position: relative; }
.nav__toggle-bars::before { top: -8px; }
.nav__toggle-bars::after  { top: 6px; }

@media (prefers-reduced-motion: no-preference) {
  .nav__toggle-bars,
  .nav__toggle-bars::before,
  .nav__toggle-bars::after {
    transition: transform 0.3s var(--ease), opacity 0.2s linear;
  }
}

.nav__toggle[aria-expanded="true"] .nav__toggle-bars { background: transparent; }
.nav__toggle[aria-expanded="true"] .nav__toggle-bars::before { transform: translateY(8px) rotate(45deg); }
.nav__toggle[aria-expanded="true"] .nav__toggle-bars::after  { transform: translateY(-6px) rotate(-45deg); }

@media (max-width: 900px) {
  /* Without JS the hamburger cannot do anything, so it stays hidden and
     .nav__menu below degrades to a plain in-flow stacked list: always
     visible, no off-canvas positioning, no reliance on script. The nav bar
     itself drops out of `position: fixed` too - a fixed bar tall enough to
     hold six stacked links would permanently occlude the hero underneath,
     which is the "overlapping mess" this fallback must avoid. Flowing it
     normally pushes the hero down instead, and gives the stacked list a
     solid background so link text is always readable. */
  html:not(.js) .nav {
    position: static;
    background: var(--navy);
  }

  .js .nav__toggle { display: block; }

  .nav__inner { flex-wrap: wrap; }

  .nav__menu {
    margin-inline-start: 0;
    flex-basis: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
  }
  .nav__menu > ul { flex-direction: column; align-items: stretch; gap: 0; width: 100%; }
  .nav__menu > ul a { display: block; font-size: 1rem; padding-block: 0.75rem; }

  /* JS-only: collapse the in-flow list above into an off-canvas overlay
     that .nav__toggle opens/closes. */
  .js .nav__menu {
    position: fixed;
    inset: var(--nav-h) 0 0 0;
    justify-content: center;
    gap: var(--space-5);
    background: var(--navy);
    transform: translateX(100%);
    visibility: hidden;
    /* Going `position: fixed` takes the list out of .nav__inner, and with it
       the `min(100% - 2.5rem, ...)` gutter every other element on the page
       sits inside. Without this the links start hard against x: 0 and the
       first letter of each one touches the screen edge. 1.25rem restores
       exactly half of that 2.5rem gutter, so the open menu lines up with the
       page rather than with the viewport. */
    padding-inline: 1.25rem;
  }

  /* The bar keeps its transparent-over-hero background until .is-stuck, so
     opening the menu at the top of the page left a see-through strip above a
     solid navy panel. The two are one surface while the menu is open. */
  .js .nav.is-menu-open { background: var(--navy); }
  .js .nav__menu > ul { gap: var(--space-4); }
  .js .nav__menu.is-open { transform: none; visibility: visible; }
}

@media (max-width: 900px) and (prefers-reduced-motion: no-preference) {
  .js .nav__menu { transition: transform 0.35s var(--ease); }
}

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

.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  align-items: center;
  overflow: hidden;
  background: var(--navy);
  color: var(--white);
  padding-block: calc(var(--nav-h) + var(--space-6)) var(--space-7);
  isolation: isolate;
}

.hero__media {
  position: absolute;
  inset: -8% 0 -8% 0;   /* overscan so parallax never exposes an edge */
  z-index: -2;
  will-change: transform;
}
/* The <picture> wrapper needs an explicit box of its own. It defaults to
   `display: inline` with auto height, so the img's `height: 100%` below was
   resolving against an auto-height parent and silently falling back to auto:
   the photo rendered at its own 2400x1365 aspect ratio (221px tall at 390px
   wide) and the rest of the hero was flat navy gradient. */
.hero__media picture {
  display: block;
  width: 100%;
  height: 100%;
}
.hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Gradient scrim: keeps text at AA contrast over any part of the photo.

   Two gradients, doing different jobs. The 180deg one darkens top and bottom
   so the nav and the scroll cue stay legible. The 100deg one is the one that
   matters for the copy: the hero text is a left-hand column, so weighting the
   scrim to the left buys contrast exactly where text sits and leaves the open
   end of the street, on the right, at close to full strength. Flattening the
   whole frame instead would cost the photograph for no benefit.

   Every stop here is measured with tools/contrast-audit.mjs, not estimated.
   The sequence that produced them: moving .hero__address up under the heading
   put --sky text on a brighter band of the photo and dropped it to 3.62:1 at
   375px and "To Let" to 4.45:1 at 1920px. Weighting the 100deg gradient
   (0.55 flat to 0.82/0.62/0.30) fixed both desktop widths with room to spare,
   the address measuring 5.96:1 at 1440px.

   375px then needed its own rule below, because a left-to-right gradient does
   almost nothing on a narrow portrait crop where the text spans the full
   width. There the vertical gradient has to carry it. */
.hero::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(180deg, rgb(23 45 75 / 0.72) 0%, rgb(23 45 75 / 0.52) 45%, rgb(23 45 75 / 0.92) 100%),
    linear-gradient(100deg,
      rgb(23 45 75 / 0.82) 0%,
      rgb(23 45 75 / 0.62) 34%,
      rgb(23 45 75 / 0.30) 58%,
      rgb(54 169 225 / 0.18) 100%);
}

/* Narrow viewports: `cover` crops the 16:9 frame to a tall portrait, so the
   copy sits over the middle of a sunlit building face rather than over sky or
   shadow, and it spans the full width, which puts it past the dark end of the
   100deg gradient. The vertical gradient therefore has to do the work on its
   own here. Heavier than the desktop scrim by design: on a phone the photo is
   a tight crop that reads as texture behind the text rather than as a view,
   and the page is far more likely to be read outdoors. */
@media (max-width: 760px) {
  .hero::after {
    background:
      linear-gradient(180deg, rgb(23 45 75 / 0.84) 0%, rgb(23 45 75 / 0.76) 45%, rgb(23 45 75 / 0.94) 100%),
      linear-gradient(100deg, rgb(23 45 75 / 0.55) 0%, rgb(54 169 225 / 0.18) 100%);
  }
}

.hero__logo {
  width: min(21rem, 62vw);
  height: auto;
  margin-bottom: var(--space-5);
}

/* "To Let:" is the opening of the heading rather than a line above it, and is
   the only part in --sky. --sky, not --blue: --blue measures 2.89:1 against the
   hero's composited backdrop and would fail AA at this size. */
.hero__title-lead { color: var(--sky); }

.hero__title {
  text-transform: uppercase;
  max-width: 20ch;
  margin-bottom: var(--space-3);
  text-shadow: 0 2px 24px rgb(0 0 0 / 0.35);
}

.hero__suites {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
  margin-bottom: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--hair);
  max-width: 44rem;
}
.hero__suites li { display: grid; gap: 0.15rem; }

.hero__suite-label {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 0.75rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--sky);
}
.hero__suite-size {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.5rem, 1.1rem + 1.8vw, 2.25rem);
  line-height: 1.1;
}
/* The count-up module rewrites this text node's digits every animation
   frame; without a reserved width the shrinking/growing digit count
   reflows the flex row it sits in (measured as layout shift in Lighthouse).
   Reserving the final value's width keeps that box a constant size for the
   ~1.4s the animation runs.

   --count-ch is set inline on each counter, because the two final values are
   not the same width: measured with canvas measureText() against Montserrat
   700 and divided by the advance of "0", "3,000" is 4.24ch but "1,055" is
   only 3.69ch (the "1" and the comma are both narrow). A single shared 4.3ch
   reservation therefore left 0.6ch - about 15px at the desktop size - of dead
   space between "1,055" and its " sq ft". Each counter now reserves its own
   value's width plus a small margin for cross-browser rendering variance. */
.hero__suite-size [data-count-to] {
  display: inline-block;
  min-width: calc(var(--count-ch, 4.3) * 1ch);
}
.hero__suite-metric { font-size: 0.9375rem; opacity: 0.82; }

/* Directly under the heading, and the only place the address appears in the
   hero now that the lockup no longer carries a strapline. Sized up from 1rem
   so it reads as part of the headline block rather than a footnote.

   --sky, not --blue. --blue (#36A9E1) is the brand's mid blue and looks like
   the obvious choice, but measured against the hero's composited backdrop
   (#40556d where this line sits) it comes out at 2.89:1, well under the 4.5:1
   AA needs at this size. --sky is the same family, reads as the brand blue
   against navy, and measures 4.90:1 on the same backdrop. The old 0.88 opacity
   is gone with it: dimming the text would have taken that back under. */
.hero__address {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: clamp(0.9375rem, 0.85rem + 0.4vw, 1.1875rem);
  line-height: 1.35;
  letter-spacing: 0.05em;
  color: var(--sky);
  margin: 0 0 var(--space-5);
}

.hero__scroll {
  position: absolute;
  inset-block-end: var(--space-4);
  inset-inline-start: 50%;
  translate: -50% 0;
  display: block;
  padding: var(--space-2);
}
.hero__scroll-line {
  display: block;
  width: 1px;
  height: 3rem;
  background: linear-gradient(180deg, transparent, var(--sky));
}

@media (prefers-reduced-motion: no-preference) {
  .hero__scroll-line { animation: scroll-cue 2.4s var(--ease) infinite; transform-origin: top; }
  @keyframes scroll-cue {
    0%, 100% { transform: scaleY(0.35); opacity: 0.4; }
    50%      { transform: scaleY(1);    opacity: 1; }
  }
}

@media (max-width: 640px) {
  .hero { min-height: 92svh; }
  .hero__suites { gap: var(--space-4); }

  /* Logo centred on phones, while the copy below it stays left-aligned.
     At this width the heading wraps to fill the column but the logo is capped
     at 62vw, so left-aligning both left the logo stopping well short of the
     right edge and reading as misaligned rather than as a deliberate ragged
     edge. Centring gives it its own axis. Desktop keeps everything left,
     where the wider column makes the shared left edge read as intended. */
  .hero__logo { margin-inline: auto; }
}

/* --- Split layout (text beside media) ------------------------------------- */

.split {
  display: grid;
  gap: clamp(var(--space-4), 4vw, var(--space-6));
  align-items: center;
}

@media (min-width: 900px) {
  .split { grid-template-columns: 1fr 1.1fr; }
  .split--reverse .split__media { order: -1; }
}

.split__body h2 { max-width: 16ch; }

.split__media { margin: 0; }
.split__media img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}
.split__media figcaption {
  margin-top: var(--space-2);
  font-size: 0.8125rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  opacity: 0.72;
}

/* --- Suites --------------------------------------------------------------- */

.suite__head { max-width: 46rem; margin-bottom: var(--space-6); }

.suite__size {
  display: block;
  font-size: clamp(1.05rem, 0.9rem + 0.6vw, 1.4rem);
  font-weight: 600;
  letter-spacing: 0.04em;
  margin-top: var(--space-2);
  /* Same reason as .eyebrow: this is the headline "3,000 sq ft (278.7 sq m)"
     figure on a white section, and --blue would put it at 2.65:1. */
  color: var(--navy);
}
.section--dark .suite__size { color: var(--sky); }

.suite__lede { font-size: 1.0625rem; max-width: 52ch; }

/* Feature icon grid */
/* Six across on desktop, whatever the feature count. `auto-fit` was reflowing
   to seven or eight columns on wide screens, which shrank each icon and forced
   labels like "Kitchen / Break Out Space" onto four lines. A fixed six-column
   track lets the icons be large enough to read as drawings rather than marks.

   The row itself is centred, and the items are centred within their tracks, so
   a final short row (five items under six, or two under six) sits under the
   middle of the block instead of hanging off the left edge. `justify-content`
   does that for the grid as a whole; the max-width on the track keeps a
   two-item row from stretching each item across half the page. */
.features {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 9.5rem));
  justify-content: center;
  gap: var(--space-5) var(--space-4);
  padding-block: var(--space-5);
  margin-bottom: var(--space-6);
  border-block: 1px solid var(--hair);
}
@media (min-width: 560px) {
  .features { grid-template-columns: repeat(3, minmax(0, 9.5rem)); }
}
@media (min-width: 780px) {
  .features { grid-template-columns: repeat(4, minmax(0, 9.5rem)); }
}
@media (min-width: 1000px) {
  .features { grid-template-columns: repeat(6, minmax(0, 10.5rem)); }
}
.section--light .features { border-color: rgb(23 45 75 / 0.14); }

/* `align-content: start` is what top-aligns the label block. Each item is a
   two-row grid (icon, then label) and the rows are sized from the tallest item
   in the row, so with the default stretch a one-line label was centring itself
   in the space a two-line label needed, and the labels stepped up and down
   across the row. Pinning both rows to the top means every icon shares a
   baseline and every label starts on the same line, however many lines it
   then runs to. */
.features__item {
  display: grid;
  grid-template-rows: auto 1fr;
  align-content: start;
  justify-items: center;
  gap: var(--space-3);
  text-align: center;
}
.features__item img {
  width: 4.5rem;
  height: 4.5rem;
  object-fit: contain;
}
@media (min-width: 1000px) {
  .features__item img { width: 5.25rem; height: 5.25rem; }
}
.section--light .features__item img {
  /* Source icons are light blue on transparent; darken them on white so they
     stay legible without needing a second set of assets. */
  filter: brightness(0.62) saturate(1.5);
}
.features__item span {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 0.75rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  line-height: 1.45;
  max-width: 13ch;
}

/* Plan beside photography */
.suite__grid {
  display: grid;
  gap: clamp(var(--space-4), 4vw, var(--space-6));
}
@media (min-width: 960px) {
  .suite__grid { grid-template-columns: 0.85fr 1.15fr; align-items: start; }
}

.suite__plan { margin: 0; }
/* The tinted panel and padding that used to sit behind these are gone. They
   existed because the plans were cropped out of the brochure PDF and carried a
   background; the supplied artwork is transparent, drawn in navy for the white
   section and white for the navy one, so it sits directly on the section. */
.suite__plan img { width: 100%; }
.suite__plan figcaption {
  margin-top: var(--space-2);
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0.7;
}

.suite__shots {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-3);
}
.suite__shots img {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}

/* Click-to-enlarge triggers on the suite photographs. The floor plans are
   deliberately NOT among them - they are read in place, at full width, and a
   lightbox on a drawing that is already legible adds a step for nothing.
   are <a href> elements pointing at the full-size file, so they are focusable
   and usable without script; main.js intercepts the click and opens the
   lightbox instead. `display: block` stops the inline-element baseline gap
   under each image. */
.zoom {
  display: block;
  position: relative;
  line-height: 0;
  cursor: zoom-in;
  outline-offset: 3px;
}
.zoom img { transition: opacity 0.3s var(--ease); }
.zoom:hover img,
.zoom:focus-visible img { opacity: 0.82; }


@media (max-width: 520px) {
  .suite__shots { grid-template-columns: 1fr; }
  .features { grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr)); }
}

/* --- Neighbourhood (a subsection of Location) ------------------------------ */

/* Separated from the map above it by a hairline with equal space either side,
   rather than by a second section's worth of padding. The old arrangement -
   .section--light media followed by .section--light gallery - put two full
   padding-blocks back to back with nothing between them, so the join read as a
   gap in the page rather than as a change of subject. */
.location__neighbourhood {
  margin-top: clamp(2.5rem, 1.5rem + 4vw, 4.5rem);
  padding-top: clamp(2.5rem, 1.5rem + 4vw, 4.5rem);
  border-top: 1px solid var(--hair);
}

/* --- Gallery -------------------------------------------------------------- */

.gallery__head { max-width: 46rem; margin-bottom: var(--space-6); }

.gallery__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-3);
}
@media (min-width: 900px) {
  .gallery__grid { grid-template-columns: repeat(4, 1fr); }
  .gallery__item--wide { grid-column: span 2; }
}

.gallery__item {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: var(--navy);
  cursor: pointer;
  overflow: hidden;
  position: relative;
}
.gallery__item img {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
}
.gallery__item::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--navy);
  opacity: 0;
  pointer-events: none;
}
.gallery__item:hover::after,
.gallery__item:focus-visible::after { opacity: 0.22; }

/* Positive guard, like every other animation in this file. This block used to
   be the mirror image - the zoom applied unconditionally and a
   prefers-reduced-motion: reduce block turned it back off - which is equally
   correct but meant the file carried two opposite conventions. The darkening
   overlay above stays outside the guard: it is a colour change, not motion,
   and it is the hover affordance itself. */
@media (prefers-reduced-motion: no-preference) {
  .gallery__item img { transition: transform 0.6s var(--ease), opacity 0.4s var(--ease); }
  .gallery__item::after { transition: opacity 0.4s var(--ease); }
  .gallery__item:hover img,
  .gallery__item:focus-visible img { transform: scale(1.05); }
}

/* Only ever rendered with JavaScript disabled - the buttons above have no
   href, so this is the sole route to the full-size photographs. */
.gallery__noscript {
  margin-top: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--sky);
  background: var(--white);
}
.gallery__noscript p { margin: 0 0 var(--space-2); font-weight: 700; color: var(--navy); }
.gallery__noscript ul {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-2);
}
/* --navy for the same reason as .eyebrow - --blue is 2.65:1 on this white
   panel. These links only ever render with JavaScript off, where they are the
   sole route to the full-size photographs, so they must be legible. */
.gallery__noscript a { color: var(--navy); }

/* --- Lightbox ------------------------------------------------------------- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 300;
  display: grid;
  place-items: center;
}
.lightbox[hidden] { display: none; }

.lightbox__backdrop {
  position: absolute;
  inset: 0;
  background: rgb(9 18 31 / 0.94);
  border: 0;
}

.lightbox__panel {
  position: relative;
  width: min(100% - 2rem, 72rem);
  display: grid;
  justify-items: center;
  gap: var(--space-3);
}

.lightbox__img {
  width: auto;
  max-width: 100%;
  max-height: 78svh;
  object-fit: contain;
}
/* The local area map is an SVG, so it has no intrinsic pixel size to fill the
   panel with; `width: auto` alone leaves it at its 532px viewBox width in the
   middle of a large screen. Sizing to the panel and letting object-fit keep the
   ratio makes it use the space, at whatever resolution the display has. */
.lightbox__img[src$=".svg"] { width: 100%; height: 78svh; }

.lightbox__caption {
  margin: 0;
  color: var(--white);
  font-size: 0.875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-align: center;
}

.lightbox__btn {
  position: absolute;
  z-index: 2;
  width: 3rem;
  height: 3rem;
  display: grid;
  place-items: center;
  font-size: 2rem;
  line-height: 1;
  color: var(--white);
  background: rgb(23 45 75 / 0.75);
  border: 1px solid var(--hair);
  border-radius: 0;
  cursor: pointer;
  transition: background 0.25s var(--ease);
}
.lightbox__btn:hover { background: var(--blue); }

.lightbox__btn--close { top: -3.5rem; right: 0; }
.lightbox__btn--prev  { left: 0;  top: 50%; translate: 0 -50%; }
.lightbox__btn--next  { right: 0; top: 50%; translate: 0 -50%; }

/* `display: grid` above beats the `hidden` attribute, which is only a UA-level
   `display: none`. Without this the previous/next arrows still painted when the
   lightbox opened a single item, offering navigation through a set of one. */
.lightbox__btn[hidden] { display: none; }

@media (max-width: 640px) {
  .lightbox__btn--prev { left: 0; }
  .lightbox__btn--next { right: 0; }
  .lightbox__btn { width: 2.5rem; height: 2.5rem; font-size: 1.5rem; }
}

/* --- Location ------------------------------------------------------------- */

.location__head { max-width: 52rem; margin-bottom: var(--space-6); }

/* Replaces the full-width illustrated map that used to sit here. The map is
   the same drawing, now opened in the lightbox from this one line. */
.location__map-link { margin: 0 0 var(--space-6); }

.travel {
  display: grid;
  gap: var(--space-5);
  margin-bottom: var(--space-6);
}
@media (min-width: 820px) {
  .travel { grid-template-columns: repeat(3, 1fr); gap: var(--space-6); }
}

.travel__table caption {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  text-align: start;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 0.875rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sky);
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-2);
  border-bottom: 1px solid var(--hair);
}
.travel__table caption img { width: 2rem; height: 2rem; object-fit: contain; }

.travel__table th,
.travel__table td {
  padding: 0.7rem 0;
  border-bottom: 1px solid rgb(255 255 255 / 0.12);
  font-weight: 400;
  text-align: start;
  font-size: 0.9375rem;
}
.travel__table th { font-family: var(--font-body); }
.travel__table td { text-align: end; white-space: nowrap; opacity: 0.9; }
.travel__table td + td { padding-inline-start: var(--space-3); }

/* Interactive Mapbox container. Hidden until a token is configured. */
.location__interactive[hidden] { display: none; }
.location__mapbox {
  width: 100%;
  height: clamp(20rem, 46vh, 32rem);
  background: rgb(255 255 255 / 0.05);
}
.location__map-note {
  margin: var(--space-2) 0 0;
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0.65;
}

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

.contact__head { max-width: 44rem; margin-bottom: var(--space-6); }

.agents {
  display: grid;
  gap: var(--space-6);
}
@media (min-width: 800px) {
  .agents { grid-template-columns: repeat(2, 1fr); gap: var(--space-7); }
}

/* Both logos occupy an identical box, so the contact details underneath start
   on the same line in both columns.

   Constraining width alone did not do that. The two files have different
   aspect ratios (SHW 1.56, Owen Isherwood 1.90), so at a shared 11rem width
   SHW rendered 41px taller and pushed its agents down, leaving Alex Bellion's
   details floating above Tim Shaw's and Elliot Hope's. Fixing the height and
   letting `object-fit: contain` letterbox the width inside it reverses that:
   each logo is scaled to whatever width its own ratio needs, and the box below
   is identical either way. `object-position: left bottom` keeps them sitting
   on a shared baseline and hard against the column's left edge, so the
   letterboxing never reads as a stray indent. */
.agent__logo {
  width: auto;
  max-width: 16.5rem;
  height: 4.875rem;
  object-fit: contain;
  object-position: left bottom;
  margin-bottom: var(--space-4);
}

.agent__people {
  display: grid;
  gap: var(--space-4);
}
@media (min-width: 520px) {
  .agent__people { grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); }
}
.agent__people p { margin: 0 0 0.15rem; }
.agent__people a {
  color: var(--navy);
  text-decoration-color: var(--blue);
  text-underline-offset: 0.15em;
}
/* Hover/focus keep --navy as the text colour (13.9:1 on white); --blue measures
   only 2.65:1 on white and would fail WCAG AA if used as the link's own colour,
   so the affordance is a stronger underline instead of a colour swap. */
.agent__people a:hover,
.agent__people a:focus-visible {
  text-decoration-thickness: 2px;
  text-decoration-color: var(--navy);
}

.agent__name {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1.0625rem;
  margin-bottom: var(--space-2) !important;
  color: var(--navy);
}

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

.footer {
  background: var(--navy);
  color: var(--white);
  padding-block: var(--space-6) var(--space-4);
  font-size: 0.875rem;
}

.footer__top {
  display: grid;
  gap: var(--space-5);
  padding-bottom: var(--space-5);
  border-bottom: 1px solid var(--hair);
}
@media (min-width: 760px) {
  .footer__top { grid-template-columns: 1fr auto; align-items: start; }
}

.footer__logo { width: 13rem; height: auto; margin-bottom: var(--space-3); }
.footer__address { margin: 0 0 var(--space-1); letter-spacing: 0.04em; }
.footer__w3w a { color: var(--sky); text-decoration: none; letter-spacing: 0.04em; }
.footer__w3w a:hover { text-decoration: underline; }

.footer__nav ul {
  display: grid;
  gap: var(--space-2);
}
.footer__nav a {
  color: var(--white);
  text-decoration: none;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 0.75rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  opacity: 0.82;
}
.footer__nav a:hover { opacity: 1; color: var(--sky); }

.footer__legal { padding-block: var(--space-5); }
.footer__legal-title {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--sky);
  margin-bottom: var(--space-2);
}
.footer__legal p {
  max-width: none;
  font-size: 0.6875rem;
  line-height: 1.6;
  opacity: 0.62;
  margin: 0;
}

.footer__credit {
  margin: 0;
  padding-top: var(--space-4);
  border-top: 1px solid var(--hair);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  opacity: 0.7;
}
.footer__credit a { color: var(--sky); }

/* --- Media (360 tour / video) --------------------------------------------- */

.media[hidden] { display: none; }
.media__head { max-width: 44rem; margin-bottom: var(--space-5); }

.media__frames {
  display: grid;
  gap: var(--space-4);
}
@media (min-width: 900px) {
  .media__frames:has(> * + *) { grid-template-columns: repeat(2, 1fr); }
}

.media__frames iframe {
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
  display: block;
  background: var(--navy);
}
