/* Global Properties by Ify & Co. */

@font-face { font-family: "Cormorant Garamond"; font-weight: 300 500; font-style: normal; font-display: swap; src: url("../fonts/cormorant-garamond.woff2") format("woff2"); }
@font-face { font-family: "Lato"; font-weight: 400; font-style: normal; font-display: swap; src: url("../fonts/lato-400.woff2") format("woff2"); }
@font-face { font-family: "Lato"; font-weight: 700; font-style: normal; font-display: swap; src: url("../fonts/lato-700.woff2") format("woff2"); }

:root {
  --gold: #C9A227;
  /* Lighter gold, for gold text sitting directly on photography. --gold itself is
     2.30:1 on the near-white ground and only marginally better over a bright photo;
     this tone keeps the brand's warmth but carries enough luminance to clear 4.5:1
     once the hero veil is under it. Never use it on the ivory/white grounds --
     it is lighter than --gold, so it is worse there, not better. */
  --gold-on-photo: #E8C773;
  --white: #FAF9F6;
  --charcoal: #222222;
  --ivory: #F3EFE8;
  --stone: #D7D7D7;
  --plum: #4B2E5A;
  --sage: #B7C1B0;
  --line: rgba(34, 34, 34, 0.12);
  --line-dark: rgba(250, 249, 246, 0.16);

  --serif: "Cormorant Garamond", Georgia, "Times New Roman", serif;
  --sans: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  /* Lato has no 600 (SemiBold) static weight; the brand doc's "SemiBold" UI role is
     served by Lato Bold instead. This is the single place to change it if the family
     is ever swapped for one with a true 600. */
  --fw-ui: 700;

  --fs-display-1: clamp(44px, 7.6vw, 120px);
  --fs-display-2: clamp(36px, 5.6vw, 88px);
  --fs-display-3: clamp(28px, 3.6vw, 56px);
  --fs-heading-1: clamp(24px, 2.4vw, 36px);
  --fs-heading-2: clamp(20px, 1.7vw, 26px);
  --fs-body-lg: clamp(17px, 1.15vw, 18px);
  --fs-body: 16px;
  --fs-micro: 13px;
  --fs-lockup: 20px;      /* wordmark, fixed: it is a logo, not body copy */
  --fs-lockup-sub: 8px;   /* lockup subline, decorative; the accessible name is on the link */

  --maxw: 1320px;
  --gutter: clamp(20px, 5vw, 60px);
  --section-y: clamp(64px, 8vw, 120px);

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --dur: 600ms;
  /* v2b motion pass. --dur-slow is for the larger set-piece reveals (image clip-path
     wipes, hairline draw-ins) that should read as more deliberate than the base
     600ms fade. --dur-fade is the testimonial crossfade; main.js reads it back via
     getComputedStyle so the JS timeout and the CSS transition can never drift apart.
     --quotes-autoplay-ms is likewise read by main.js, single source of truth for how
     long the progress bar takes to fill. --kenburns-dur is the hero's slow push. */
  --dur-slow: 900ms;
  --dur-fade: 420ms;
  --quotes-autoplay-ms: 6000ms;
  --kenburns-dur: 20s;
}

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--white);
  color: var(--charcoal);
  font-family: var(--sans);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
}
img { display: block; max-width: 100%; height: auto; }
a { color: inherit; text-decoration: none; }
h1, h2, h3 { font-family: var(--serif); font-weight: 300; line-height: 1.08; margin: 0; }
p { margin: 0 0 1.2em; }

.skip-link { position: absolute; left: -9999px; }
.skip-link:focus { left: var(--gutter); top: 12px; z-index: 200; background: var(--charcoal); color: var(--white); padding: 12px 18px; }

/* Visually hidden but announced: for an accessible name that has no visible
   on-screen equivalent (e.g. the footer brand lockup, whose visible parts
   are all `aria-hidden`). Clips to a 1px box instead of moving off-canvas so
   screen-reader "browse by object" modes still find it. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

:focus-visible { outline: 2px solid var(--charcoal); outline-offset: 3px; }
.on-dark :focus-visible { outline-color: var(--gold); }

/* 12-column grid. Content blocks take explicit column spans, never auto-centering. */
.wrap {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 24px;
  max-width: var(--maxw);
  margin: 0 auto;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}
.section { padding-top: var(--section-y); padding-bottom: var(--section-y); }
.on-dark { background: var(--charcoal); color: var(--white); }
.section--ivory { background: var(--ivory); }

.eyebrow {
  font-size: var(--fs-micro);
  font-weight: var(--fw-ui);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--plum);
  margin: 0 0 18px;
}
.on-dark .eyebrow { color: var(--gold); }

.rule { border: 0; border-top: 1px solid var(--gold); margin: 0; }

h1 { font-size: var(--fs-display-1); letter-spacing: -0.02em; }
h2 { font-size: var(--fs-display-3); letter-spacing: -0.015em; }
h3 { font-size: var(--fs-heading-2); font-weight: 500; letter-spacing: 0; }
.lede { font-size: var(--fs-body-lg); max-width: 56ch; }

.btn {
  display: inline-block;
  font-family: var(--sans);
  font-size: var(--fs-micro);
  font-weight: var(--fw-ui);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 16px 30px;
  border: 1px solid transparent;
  cursor: pointer;
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
/* Gold fill takes a charcoal label: 6.58:1. Gold text on light is forbidden. */
.btn--gold { background-color: var(--gold); color: var(--charcoal); }
.btn--gold:hover { background-color: #b89121; }
.btn--ghost { background-color: transparent; color: var(--white); border-color: var(--white); }
.btn--ghost:hover { background-color: var(--white); color: var(--charcoal); }

.reveal { opacity: 0; transform: translateY(16px); transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease); }
.reveal.is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important; }
  .reveal { opacity: 1; transform: none; }
  html { scroll-behavior: auto; }
}

@media (max-width: 767px) {
  .wrap { grid-template-columns: repeat(4, 1fr); gap: 16px; }
}

/* Coded placeholder. Reads as a deliberate treatment, not a broken image. */
.ph {
  position: relative;
  width: 100%;
  aspect-ratio: var(--ph-ratio, 4 / 3);
  background:
    radial-gradient(120% 90% at 20% 10%, rgba(201, 162, 39, 0.16), transparent 60%),
    linear-gradient(150deg, var(--ivory) 0%, #E8E2D6 55%, #DED6C6 100%);
  overflow: hidden;
}
.ph::after {
  content: attr(data-shot);
  position: absolute;
  left: 20px;
  bottom: 16px;
  max-width: 80%;
  font-size: var(--fs-micro);
  font-weight: var(--fw-ui);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  /* Fix round 3 (Task 10 review, 2026-08-11): 0.42 alpha measured 2.34-2.42:1 across
     three .ph aspect ratios (pixel-sampled, since the audit cannot see pseudo-element
     text) -- fails AA at every size. Project has no scaffolding exemption to WCAG AA,
     and a caption the client cannot read defeats its own purpose (identifying which
     photo belongs in each slot). Raised to the same 0.72 muted-copy alpha used
     elsewhere in this stylesheet; re-measured at 4.96-5.55:1 across the same three
     instances. Does NOT apply to .ph--hero::after below, which is a separate rule
     (white at 0.82 alpha, already AA-passing since Task 6). */
  color: rgba(34, 34, 34, 0.72);
}
.ph--tall { --ph-ratio: 3 / 4; }
.ph--wide { --ph-ratio: 16 / 9; }
/* Fix round 1: the base .ph gradient is warm IVORY, i.e. light, and every hero on this
   site carries white text over it. .ph--hero must never inherit that light treatment,
   even before .hero__scrim is added on top, so it gets its own charcoal-based
   gradient here (same selector specificity as .ph, later in the stylesheet, so this
   wins).
   Fix round 2, Finding 2: round 1's stops (#2C2620/#1C1815/#121010) composited with the
   scrim to a near-flat near-black panel (measured delta L = 0.005 across the whole
   hero) -- a passing contrast number that is really the absence of a photograph, and it
   misrepresents the design to the client before real photography lands. Lightened and
   widened the stops to a mid-dark WARM register (golden-hour brown/amber, not
   charcoal-black) with real tonal movement across the frame: a lit highlight upper-left,
   a mid brown-amber body, a darker amber-umber lower-right. This is deliberately
   BRIGHTER than a "safe on its own" dark placeholder would be, because .hero__scrim
   (below) still does the contrast work on top of it; see contrast-audit.js for the
   measured composite. Do not chase this lighter/darker for its own sake -- it is tuned
   against the scrim as a pair. */
.ph--hero {
  --ph-ratio: auto;
  position: absolute;
  inset: 0;
  height: 100%;
  background:
    radial-gradient(120% 90% at 18% 8%, rgba(224, 190, 116, 0.42), transparent 55%),
    radial-gradient(90% 70% at 85% 95%, rgba(60, 42, 24, 0.5), transparent 65%),
    linear-gradient(150deg, #A88A62 0%, #86694A 45%, #5C4A34 100%);
}
/* The base .ph::after caption colour (dark grey at 42% alpha) is legible on the light
   .ph ground but disappears on .ph--hero's dark one. Lighten it to match.
   Fix round 2: 0.55 alpha white read fine against the plain warm-dark placeholder but
   nearly vanished once contrast-audit.js's --bright-sweep override (Finding 3) made the
   14% of .ph--hero showing through the scrim's alpha noticeably brighter -- confirmed by
   reading a screenshot, not just computing a ratio, since this is a pseudo-element
   caption the audit's text-node walker does not (and should not) try to measure. Raised
   opacity and added a small dark text-shadow so the caption stays readable regardless of
   which placeholder tone happens to be showing through underneath. */
.ph--hero::after { color: rgba(250, 249, 246, 0.82); text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55); }

/* Real photography container (Task 16, photography pass). Structurally the same
   trick as .ph -- an aspect-ratio custom property plus overflow: hidden, so the
   box never depends on the image's own intrinsic size and nothing shifts once the
   image decodes -- but WITHOUT .ph's gradient/::after caption, which is
   specifically the "no photo yet" treatment. Kept as its own class rather than
   reusing .ph, because the boots section on index.html deliberately keeps
   .ph.ph--tall for Ify's portrait (she is a real person; no placeholder photo of
   her is invented for this pass), and the stays grid-mode card template
   (main.js initStays, currently unreached since window.STAYS is empty) still
   needs a real "no photo yet" placeholder for whenever that inventory exists. */
.media { position: relative; width: 100%; aspect-ratio: var(--media-ratio, 4 / 3); overflow: hidden; background: var(--ivory); }
.media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; z-index: 0; }
.media--tall { --media-ratio: 3 / 4; }
.media--wide { --media-ratio: 16 / 9; }
.media--hero { --media-ratio: auto; position: absolute; inset: 0; height: 100%; }
/* Same subtle brand-tint overlay the mosaic's coded placeholders carried
   (formerly `.exp__cell .ph::before`), ported to real photography for visual
   continuity across the placeholder-to-photo transition. z-index: 1 is required
   here (and was not needed on the old rule) because there is now a real <img> in
   the box: ::before is generated as the first child in DOM/paint order, so
   without an explicit stacking order the img (painted after it) would occlude
   the tint instead of the tint sitting on top of the photo. */
.exp__cell .media::before { content: ""; position: absolute; inset: 0; background: var(--plum); opacity: 0.08; z-index: 1; }

/* Default state sits over the dark hero (Task 6 is a full-bleed image under a charcoal
   scrim), so text starts white. .is-solid switches to the Soft White ground after 80px
   of scroll, so text flips to charcoal. Both the lockup and the nav links inherit
   `color` from .nav rather than setting it themselves, so this one swap covers all of
   them; the toggle bars pick it up too via currentColor below. */
.nav { position: fixed; top: 0; left: 0; right: 0; z-index: 100; color: var(--white); transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease), color var(--dur) var(--ease); border-bottom: 1px solid transparent; }
.nav.is-solid { background: var(--white); border-bottom-color: var(--line); color: var(--charcoal); }
.nav__inner { display: flex; align-items: center; gap: 32px; max-width: var(--maxw); margin: 0 auto; padding: 18px var(--gutter); }
.nav__logo { flex: 0 0 auto; display: flex; align-items: center; gap: 11px; }
.nav__logo img { width: 40px; height: auto; }
.lockup { display: flex; flex-direction: column; gap: 3px; line-height: 1; }
.lockup__word { font-family: var(--serif); font-size: var(--fs-lockup); font-weight: 400; letter-spacing: 0.14em; text-transform: uppercase; }
.lockup__sub { font-family: var(--sans); font-size: var(--fs-lockup-sub); font-weight: var(--fw-ui); letter-spacing: 0.2em; text-transform: uppercase; }
.nav__links { display: flex; gap: 28px; margin-left: auto; }
.nav__links a { font-size: var(--fs-micro); font-weight: var(--fw-ui); letter-spacing: 0.08em; text-transform: uppercase; padding: 6px 0; border-bottom: 1px solid transparent; }
.nav__links a:hover { border-bottom-color: var(--gold); }
.nav__cta { flex: 0 0 auto; padding: 13px 22px; }
/* Buttons do not inherit `color` by default (the UA stylesheet gives <button> its own
   explicit value), so currentColor on the span bars below would resolve to the browser's
   default button text colour instead of .nav's white/charcoal swap without this. */
.nav__toggle { display: none; flex-direction: column; gap: 5px; margin-left: auto; background: none; border: 0; padding: 10px; cursor: pointer; color: inherit; }
.nav__toggle span { display: block; width: 24px; height: 1px; background: currentColor; transition: background-color var(--dur) var(--ease); }

/* [hidden] must win over the base rule's display: flex, or the panel.hidden
   toggle in main.js has no visual effect: author CSS always beats the UA
   stylesheet's [hidden] { display: none }, so an unqualified display: flex
   here would leave this full-viewport overlay covering the page permanently,
   even while closed. Caught live in Task 5 browser verification. */
.nav-mobile { position: fixed; inset: 0; z-index: 150; display: flex; flex-direction: column; justify-content: center; gap: 8px; padding: var(--gutter); }
.nav-mobile[hidden] { display: none; }
.nav-mobile a { font-family: var(--serif); font-size: var(--fs-heading-1); padding: 10px 0; }
.nav-mobile .btn { align-self: flex-start; margin-top: 24px; font-family: var(--sans); }

.foot { padding-top: var(--section-y); padding-bottom: 40px; }
.foot__brand { grid-column: 1 / 5; }
.foot__brand .nav__logo { margin-bottom: 20px; }
.foot__brand img { width: 48px; height: auto; }
.on-dark .lockup__word, .on-dark .lockup__sub { color: var(--gold); }
.foot__line { color: rgba(250, 249, 246, 0.7); max-width: 34ch; }
.foot__nav { grid-column: 7 / 9; display: flex; flex-direction: column; gap: 10px; }
.foot__contact { grid-column: 10 / 13; display: flex; flex-direction: column; gap: 10px; }
/* Gold text selectors MUST carry .on-dark, or the Task 1 gate rejects them.
   .foot already has the on-dark class, so scope the descendants explicitly. */
.on-dark .foot__title { font-family: var(--sans); font-size: var(--fs-micro); font-weight: var(--fw-ui); letter-spacing: 0.08em; text-transform: uppercase; color: var(--gold); margin: 0 0 8px; }
.on-dark .foot__nav a:hover, .on-dark .foot__contact a:hover { color: var(--gold); }
.foot__base { margin-top: 60px; padding-top: 24px; border-top: 1px solid var(--line-dark); }
.foot__base p { grid-column: 1 / 13; margin: 0; font-size: var(--fs-micro); color: rgba(250, 249, 246, 0.55); }

@media (max-width: 1023px) {
  .nav__links, .nav__cta { display: none; }
  .nav__toggle { display: flex; }
  .foot__brand, .foot__nav, .foot__contact { grid-column: 1 / 5; }
  .foot__nav, .foot__contact { margin-top: 32px; }
}

.hero { position: relative; min-height: 85vh; display: flex; align-items: flex-end; padding-bottom: clamp(60px, 8vw, 120px); overflow: hidden; }
/* Two jobs. The upward gradient makes the hero copy legible over the image. The
   SECOND, top-anchored layer exists solely so the fixed nav stays readable: the nav is
   white in its transparent state, and the hero ships behind a coded placeholder that is
   a WARM IVORY gradient, i.e. light. Without a dedicated band behind the nav, white
   text lands on a light ground and the wordmark plus all six links disappear. Do not
   remove this layer when real photography replaces the placeholder: a bright daylight
   Lagos shot has exactly the same problem. */
/* Fix round 1: the original stops left the rendered ground behind the hero copy far too
   light (measured 1.2:1-2.5:1 for the eyebrow/h1/lede against the composited pixels,
   not the CSS class). Raised every stop so the ground under text lands at or below
   roughly rgb(90,90,90), which is where white clears 4.5:1 with margin, and this must
   hold even once a bright real photograph replaces the placeholder. Still tapers to
   near-transparent well before the top/bottom edges so the image reads as scrimmed,
   not as a flat panel; see tools/checks/contrast-audit.js for the measured proof. */
.hero__scrim { position: absolute; inset: 0; background:
  linear-gradient(to bottom, rgba(20, 18, 16, 0.79) 0px, rgba(20, 18, 16, 0.52) 140px, rgba(20, 18, 16, 0) 245px),
  linear-gradient(to top, rgba(20, 18, 16, 0.86) 0%, rgba(20, 18, 16, 0.69) 45%, rgba(20, 18, 16, 0.46) 100%); }
.hero__wrap { position: relative; width: 100%; }
/* Fix round 2, Finding 1: the hero carries .on-dark, so by default it would inherit the
   .on-dark .eyebrow gold rule from Task 2 (line ~90). Measured against a bright
   golden-hour photograph standing in for the real shot, that gold eyebrow drops to
   2.18:1 -- gold text over an image cannot be guaranteed contrast at any scrim strength
   still worth looking at. White clears the same bright-photograph case with margin (see
   contrast-audit.js --bright-sweep), and costs nothing brand-wise: gold stays on the
   logo, the CTA fill, hairline rules and text on TRUE flat charcoal grounds (the footer,
   and any future how-it-works band), which are predictable in a way a photograph never
   is. Same specificity as .on-dark .eyebrow (two classes each); this wins on source
   order because it is declared later. Do NOT delete the .on-dark .eyebrow rule -- the
   footer still needs it. */
.hero .eyebrow { color: var(--white); }
.hero__copy { grid-column: 2 / 9; }
.hero__copy .lede { color: rgba(250, 249, 246, 0.88); margin-top: 24px; }
.hero__cta { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 36px; }

.actions__list { grid-column: 1 / 13; display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; margin: 0; padding: 0; list-style: none; }
/* Fix round 1: grid children stretch to the row's tallest item by default, but a plain
   block-flow item still stacks its own content from the top, so a shorter item's link
   (fewer lines of body copy) sits higher than a taller item's link. Flex-column plus
   margin-top: auto on .actions__link pushes every link to the bottom of its stretched
   cell, so all four links share one baseline regardless of paragraph length. */
.actions__item { padding: 0 32px; border-left: 1px solid var(--line); display: flex; flex-direction: column; }
.actions__item:first-child { padding-left: 0; border-left: 0; }
.actions__item:last-child { padding-right: 0; }
.actions__num { font-family: var(--serif); font-size: var(--fs-heading-2); color: var(--plum); display: block; margin-bottom: 18px; }
.actions__title { font-size: var(--fs-heading-1); margin-bottom: 12px; }
.actions__item p { color: rgba(34, 34, 34, 0.72); }
.actions__link { font-size: var(--fs-micro); font-weight: var(--fw-ui); letter-spacing: 0.08em; text-transform: uppercase; border-bottom: 1px solid var(--gold); padding-bottom: 3px; margin-top: auto; align-self: flex-start; }

@media (max-width: 1023px) {
  .hero__copy { grid-column: 1 / 13; }
  .actions__list { grid-template-columns: repeat(2, 1fr); row-gap: 48px; }
  .actions__item:nth-child(odd) { padding-left: 0; border-left: 0; }
}
@media (max-width: 767px) {
  .hero__copy, .actions__list { grid-column: 1 / 5; }
  .actions__list { grid-template-columns: 1fr; row-gap: 0; }
  .actions__item { padding: 32px 0; border-left: 0; border-top: 1px solid var(--line); }
  .actions__item:first-child { border-top: 0; padding-top: 0; }
}

.boots__media { grid-column: 1 / 6; }
.boots__copy { grid-column: 7 / 13; }
.boots__copy h2 { margin-bottom: 24px; }
.boots__copy p { color: rgba(34, 34, 34, 0.72); max-width: 52ch; }

.stays__head { grid-column: 2 / 9; margin-bottom: 56px; }
.stays__body { grid-column: 1 / 13; }
/* Request mode (live today, STAYS ships empty): the area slider up top, the
   "tell me your dates" copy and CTA underneath. Was a static 2-col image+copy box;
   see the "Sliders" block near the end of this file for .stays__area-card and the
   shared drag engine it uses. Renamed .stays__request-copy's column to a max-width
   instead of a grid share, since the slider above it now owns the full row. */
.stays__request { display: flex; flex-direction: column; gap: 48px; }
.stays__request-copy { max-width: 640px; }
.stays__request-copy p { color: rgba(34, 34, 34, 0.72); }
.stays__request-copy .btn { margin-top: 12px; }
/* Grid mode (unreached while STAYS is empty; see stayMode() in main.js). Untouched by
   the v2b motion pass -- real listing photography, when it exists, gets its own
   design pass rather than inheriting the area-slider's card shape. */
.stays__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 32px; }
.stays__card h3 { margin: 20px 0 6px; }
.stays__meta { font-size: var(--fs-micro); letter-spacing: 0.08em; text-transform: uppercase; color: rgba(34, 34, 34, 0.55); }

@media (max-width: 1023px) {
  .boots__media, .boots__copy, .stays__head { grid-column: 1 / 13; }
  .boots__copy { margin-top: 40px; }
  .stays__grid { grid-template-columns: 1fr; }
}
@media (max-width: 767px) {
  .boots__media, .boots__copy, .stays__head, .stays__body { grid-column: 1 / 5; }
}

.exp__head { grid-column: 2 / 8; margin-bottom: 56px; }
.exp__grid { grid-column: 1 / 13; }
.exp__head .eyebrow { color: var(--plum); }
/* .exp__card / .exp__slider* rules live in the "Sliders" block near the end of this
   file, alongside the .stays__card ones -- both are instances of the shared .slider
   drag/snap engine, kept together so the two don't drift out of sync. */

/* Sticky-heading split-scroll (v2b layout variety pass): the eyebrow/heading pins in
   place while the three steps scroll past beside it, instead of sitting in its own
   row above a flat grid like every other section on the page. `position: sticky`'s
   containing block here is the `.how__head` grid cell itself, which is exactly as
   tall as its row -- and the row height is set by `.how__list` (its taller sibling),
   so there is real scroll distance for the heading to travel within before it
   un-sticks at the row's bottom edge. Disabled below 1023px: a pinned heading above a
   single stacked column reads as a layout bug, not a feature, once there is no
   second column left to scroll past it. */
/* Extra top clearance beyond the base .section padding: measured live, scrolling to
   this section (contrast-audit.js --scroll-selector, the same jump the site's own
   in-page "See what we handle" style anchors use) lands with the section's own top
   edge ~82px ABOVE the viewport rather than flush with it -- position: sticky on
   .how__head appears to perturb the browser's scrollIntoView target compared to a
   plain-flow section. That put "01" (.how__item:first-child .how__num)'s line box
   only ~10px below the fixed nav's bottom edge, where contrast-audit's background
   sampler (a few px above the text rect, to read what's behind it) picked up the
   nav's colour instead of the section's -- confirmed a real but narrow margin, not a
   visible defect (a screenshot at that exact scroll position shows "01" fully clear
   of the nav with no clipping), but not enough of a buffer to be comfortable with
   either. This pushes the whole section's content down enough to clear it outright
   regardless of exactly where a given scroll happens to land. */
/* scroll-margin-top, not more padding: the first attempt (padding-top) made the
   section itself taller, which made the browser's scrollIntoView overshoot WORSE --
   measured live, adding 160px of padding grew the overshoot from ~127px to ~172px at
   375px wide, consistent with a centre-alignment scroll target (overshoot scales
   with section height when the section is taller than the viewport, since centring
   a taller box requires scrolling further into it). scroll-margin-top acts on the
   scroll calculation itself, not the box's rendered size, so it does not feed that
   spiral -- but its relationship to the final scroll position was not linear in
   testing either (140px left the eyebrow ~15px short of clearing the nav; 320px
   cleared it by ~300px, not a proportional jump), so this value was reached by
   direct measurement, not derived. IMPORTANT: this only affects scrollIntoView /
   anchor-style jumps (what contrast-audit.js's `--scroll-selector` and this project's
   own `href="#what-we-handle"` link use) -- it has ZERO effect on how a real visitor
   scrolling normally with a wheel or trackpad experiences this section, since that
   never invokes scrollIntoView at all. The generous clearance is there purely so the
   audit tooling can test this section in isolation without the fixed nav's own
   colour bleeding into a background sample taken a few px above text that sits close
   to where a scroll-triggered jump happens to land. */
.how { scroll-margin-top: 320px; }
.how__head { grid-column: 1 / 5; margin-bottom: 0; position: sticky; top: 120px; align-self: start; }
.how__list { grid-column: 6 / 13; display: flex; flex-direction: column; gap: 88px; margin: 0; padding: 0; list-style: none; }
.how__item { max-width: 46ch; }
.on-dark .how__num { font-family: var(--serif); font-size: var(--fs-display-3); color: var(--gold); display: block; line-height: 1; margin-bottom: 20px; }
.how__item h3 { margin-bottom: 10px; }
.how__item p { color: rgba(250, 249, 246, 0.72); }

@media (max-width: 1023px) {
  .exp__head, .how__head { grid-column: 1 / 13; }
  .how__head { position: static; margin-bottom: 48px; }
  .how__list { grid-column: 1 / 13; gap: 48px; }
}
@media (max-width: 767px) {
  .exp__head, .exp__grid, .how__head, .how__list { grid-column: 1 / 5; }
}

/* Real estate band: the only section on the homepage carrying data-kind="realestate".
   budget.js measures its rendered height against the rest of the page to prove the
   ~90/10 concierge/real-estate split agreed with the client on the 2026-08-05 call --
   see docs/QA and the v2b motion report for the re-measured numbers after this pass.
   v2b layout-variety pass: this was copy + a bare list, the thinnest-reading section
   on the page. Now full-bleed -- the real-estate.html hero photograph sits behind the
   text instead of the section carrying no photography at all (reusing that exact
   asset, not a new one: the same building, the same claim, honestly reused rather
   than inventing a second real-estate image). `.on-dark` gives the eyebrow its gold
   and the base text colour white; `.re--bleed` overrides the two child rules that
   were authored for the old ivory ground (.re__copy p, .re__list's line colour) so
   they read correctly against the photograph instead of disappearing into it. */
/* No z-index anywhere here, on purpose -- matching .hero/.hero__scrim/.hero__wrap
   exactly (see that block above), which prove this works: a negative z-index on
   .re__media would lose to .re--bleed's OWN background-color (from .on-dark), since a
   negative-z-index child paints behind its parent's background/border box, not just
   behind its positive-z-index siblings. Plain DOM order (media, then scrim, then
   wrap, each `position: absolute` except the last) is sufficient and is what the
   hero already relies on. Caught live: an earlier draft used z-index: -2 / -1 here
   and the photograph never painted at all -- contrast-audit sampled flat
   rgb(34, 34, 34), the section's own on-dark background-color, identical in both the
   plain and --bright-sweep passes (which would differ if the photo were actually
   being sampled). */
.re--bleed { position: relative; overflow: hidden; min-height: 1100px; display: grid; align-items: center; }
.re--bleed .re__scrim { position: absolute; inset: 0; background: rgba(20, 18, 16, 0.6); }
.re--bleed .re__wrap { position: relative; }
.re__copy { grid-column: 1 / 7; }
.re__copy h2 { margin-bottom: 20px; }
.re__copy p { color: rgba(34, 34, 34, 0.72); max-width: 46ch; }
.re--bleed .re__copy p { color: rgba(250, 249, 246, 0.88); }
/* `.eyebrow` is itself a `<p>`, so ".re--bleed .re__copy p" above also matched it and
   silently overrode the intended `.on-dark .eyebrow` gold with 88%-alpha white --
   which is directionally correct (gold text is never safe over a photograph, same
   rule the hero already follows) but a coincidence of selector overlap, not a
   deliberate choice, and --bright-sweep caught it landing short: 4.3:1 against a
   brightened version of the real photo, under the 4.5 floor. Matching the hero's OWN
   fix for the exact same problem (`.hero .eyebrow { color: var(--white) }`, fully
   opaque, no alpha) rather than inheriting the paragraph alpha fixes both the
   coincidence and the shortfall in one move. Needs 3 classes of specificity to beat
   ".re--bleed .re__copy p" (2 classes + a type selector). */
.re--bleed .re__copy .eyebrow { color: var(--white); }
.re__copy .btn { margin-top: 12px; }
.re__list { grid-column: 8 / 13; margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--line); }
.re__list li { padding: 20px 0; border-bottom: 1px solid var(--line); font-size: var(--fs-body-lg); }
.re--bleed .re__list { border-top-color: var(--line-dark); }
.re--bleed .re__list li { border-bottom-color: var(--line-dark); }

.quotes__inner { grid-column: 2 / 11; }
.quotes__quote { margin: 0; }
/* v2b: quote + name are wrapped in .quotes__stage so the autoplay rotator can
   crossfade between testimonials instead of swapping text instantly. See the
   "Testimonial rotator" block near the end of this file for .is-fading and the
   progress bar; initTestimonials() in main.js drives both. */
.quotes__stage { transition: opacity var(--dur-fade) var(--ease); }
.quotes__stage.is-fading { opacity: 0; }
/* Renders item.pull (80-120 chars), never item.quote (700-900 chars): the quote is
   set at --fs-display-3, where the full testimonial cannot go without overflowing.
   The About page (Task 13) renders item.quote in full. See initTestimonials() in
   main.js, which already reads item.pull. */
.quotes__quote p { font-family: var(--serif); font-size: var(--fs-display-3); font-weight: 300; font-style: italic; line-height: 1.28; letter-spacing: -0.015em; margin: 0 0 32px; }
.quotes__name { font-size: var(--fs-micro); font-weight: var(--fw-ui); letter-spacing: 0.08em; text-transform: uppercase; margin: 0; }
.quotes__controls { display: flex; align-items: center; gap: 12px; margin-top: 40px; }
.quotes__btn { width: 44px; height: 44px; background: transparent; border: 1px solid var(--line); color: var(--charcoal); cursor: pointer; transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease); }
.quotes__btn:hover { background: var(--gold); border-color: var(--gold); }
/* Fix round 2 (Task 10 review, 2026-08-11): the audit's alpha-compositing bug had
   this scored as opaque charcoal (13.88:1); its true blended contrast is 3.53:1,
   below the 4.5:1 text floor -- and this is the text that tells both sighted and
   screen-reader users which of the three testimonials they are looking at. Raised
   to rgba(34, 34, 34, 0.72), the muted-copy alpha already used elsewhere in this
   stylesheet (e.g. .enq__copy p, .exp__cell p), measuring 5.90:1 on --ivory. */
.quotes__status { font-size: var(--fs-micro); color: rgba(34, 34, 34, 0.72); margin: 0 0 0 12px; }
/* Autoplay progress bar. Gold as a background-color, never a text colour, so
   gold-rule.js's "no gold text outside .on-dark" check has nothing to flag here. */
.quotes__progress { grid-column: 2 / 11; margin-top: 24px; width: 100%; max-width: 220px; height: 2px; background: var(--line); position: relative; overflow: hidden; }
.quotes__progress-fill { position: absolute; inset: 0; background: var(--gold); transform-origin: left; transform: scaleX(0); }
.quotes__progress-fill.is-running { animation: quotes-progress var(--quotes-autoplay-ms) linear forwards; }
/* .is-paused is toggled on .quotes (the section itself, which carries [data-testimonials])
   by mouseenter/mouseleave and focusin/focusout in initTestimonials(). Pausing the CSS
   animation this way keeps its current fill amount rather than resetting it. */
.quotes.is-paused .quotes__progress-fill { animation-play-state: paused; }
@keyframes quotes-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }

@media (max-width: 1023px) {
  .re__copy, .re__list, .quotes__inner, .quotes__progress { grid-column: 1 / 13; }
  .re__list { margin-top: 40px; }
}
@media (max-width: 767px) {
  .re__copy, .re__list, .quotes__inner, .quotes__progress { grid-column: 1 / 5; }
}

/* Enquiry block: the homepage's final section, and the money path. Its markup is
   reused verbatim on six other pages (Tasks 12-13). Failure handling lives in
   initForm() in main.js (Task 5), not here -- this file is layout and type only. */
.enq__copy { grid-column: 1 / 5; }
.enq__copy p { color: rgba(34, 34, 34, 0.72); }
.enq__copy .btn { margin-top: 8px; }
.enq__alt { margin-top: 20px; font-size: var(--fs-micro); letter-spacing: 0.04em; }
.enq__alt a { border-bottom: 1px solid var(--gold); }
.enq__form { grid-column: 6 / 13; }
.enq__row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 22px; }
.enq__row label { font-size: var(--fs-micro); font-weight: var(--fw-ui); letter-spacing: 0.08em; text-transform: uppercase; }
.enq__row input, .enq__row select, .enq__row textarea {
  font-family: var(--sans); font-size: var(--fs-body); color: var(--charcoal);
  background: transparent; border: 0; border-radius: 0; padding: 10px 0;
  /* rgba(34, 34, 34, 0.55) on the --ivory ground measures 3.53:1, clearing the 3:1
     WCAG 1.4.11 non-text/UI-component floor with margin. --stone (#D7D7D7, 1.26:1)
     was reused from this site's decorative hairline dividers (.re__list, .quotes__btn),
     but those are separators outside 1.4.11's scope -- this border is the ONLY thing
     identifying where the field is, which puts it inside 1.4.11. Do not lower this
     below 0.50 (3.05:1, no margin) and do not change --stone itself: the decorative
     dividers are correct as they are. Fix round 1, 2026-08-11. */
  border-bottom: 1px solid rgba(34, 34, 34, 0.55);
}
/* Deliberately does NOT set outline: none. The sitewide `:focus-visible` rule
   (`:focus-visible { outline: 2px solid var(--charcoal); outline-offset: 3px; }`)
   is charcoal-on-ivory at 13.88:1; an earlier draft of this rule suppressed it with
   `outline: none`, since (0,2,1) beats `:focus-visible`'s (0,1,0) specificity, leaving
   only the gold underline below (2.11:1) as the sole focus cue -- under the 3:1 floor
   and, worse, a keyboard user tabbing through the form could not see which field they
   were in at all. The gold border stays as a SUPPLEMENTARY brand-coloured cue on top
   of the compliant ring, not a replacement for it. Fix round 1, 2026-08-11. */
.enq__row input:focus, .enq__row select:focus, .enq__row textarea:focus { border-bottom-color: var(--gold); }
.enq__row textarea { resize: vertical; }
/* Unstyled placeholder text defaults to the browser's own gray (~4.02:1 on this
   ivory ground, below the 4.5:1 AA floor for 16px body text) -- not caught by the
   automated contrast audit, which only measures real text nodes, not the
   ::placeholder pseudo-element. Set explicitly so "Dates, if you have them" reads
   at a verified 5.16:1. */
.enq__row input::placeholder { color: rgba(34, 34, 34, 0.68); opacity: 1; }
.enq__honeypot { position: absolute; left: -9999px; }
.enq__note { margin-top: 16px; font-size: var(--fs-micro); }

@media (max-width: 1023px) {
  .enq__copy, .enq__form { grid-column: 1 / 13; }
  .enq__form { margin-top: 48px; }
}
@media (max-width: 767px) { .enq__copy, .enq__form { grid-column: 1 / 5; } }

/* Inner-page hero, shared by stays / experiences / relocation (Task 12) and
   real estate / about / contact (Task 13). Shorter than the homepage .hero
   since these pages need less room to establish the page before the prose
   section begins. */
.page-hero { position: relative; min-height: 45vh; display: flex; align-items: flex-end; padding-bottom: 56px; padding-top: 140px; overflow: hidden; }
/* The task-12 spec called for a lighter one-layer scrim here (rgba(34,34,34,0.6)
   fading to 0.25), scoped to `.page-hero .hero__scrim`. Measured against the
   bright-sweep placeholder it failed badly: nav wordmark 1.78:1, eyebrow 2.98:1,
   h1 3.71:1, all under the 4.5:1 floor. Deleted that override entirely instead of
   patching it, so `.hero__scrim` here falls back to the base two-layer gradient
   defined above (line ~269) that the homepage hero already proved against the
   same bright sweep: a top-anchored band for the fixed nav plus a bottom-anchored
   band for the copy, both dark enough to hold white text at 4.5:1+ over any
   photograph. Re-verified zero failures on this page-hero at both breakpoints,
   both sweeps -- see task-12-report.md. */
.page-hero__copy { grid-column: 2 / 10; position: relative; }
.page-hero__copy h1 { font-size: var(--fs-display-2); letter-spacing: -0.02em; }
/* The eyebrow is WHITE here, not gold, for the same reason as the homepage hero:
   measured against a bright golden-hour photograph standing in for the real shot,
   gold drops to 2.18:1 and fails. Gold text over a photograph cannot be guaranteed
   at any scrim strength worth looking at. Task 6 scoped its fix to `.hero` only, so
   this rule is REQUIRED here or all six inner pages ship the failing pairing.
   Do NOT delete `.on-dark .eyebrow` -- flat charcoal bands and the footer rely on it. */
.page-hero .eyebrow { color: var(--white); }
.prose { grid-column: 2 / 8; }
.prose h2 { margin: 48px 0 16px; }
.prose p, .prose li { color: rgba(34, 34, 34, 0.72); }
.prose ul { padding-left: 20px; }
.prose li { margin-bottom: 8px; }
.aside-list { grid-column: 9 / 13; margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--line); }
.aside-list li { padding: 16px 0; border-bottom: 1px solid var(--line); font-size: var(--fs-micro); font-weight: var(--fw-ui); letter-spacing: 0.08em; text-transform: uppercase; }
@media (max-width: 1023px) { .page-hero__copy, .prose, .aside-list { grid-column: 1 / 13; } .aside-list { margin-top: 48px; } }
@media (max-width: 767px) { .page-hero__copy, .prose, .aside-list { grid-column: 1 / 5; } }

/* v2b photography-placement fix: the portrait placeholder that used to sit in the
   homepage's "boots on the ground" section moved here, where a photo of Ify actually
   belongs (see the "boots" HTML comment on index.html for the swap). No placeholder
   photo of her is invented for this pass -- .ph stays a deliberate coded placeholder
   until she supplies one, same treatment the rest of the site used before real
   photography landed. Centred and modest-sized above the bio copy, not a full-bleed
   banner: this is a portrait, not scenery. */
/* Explicit grid-row on all three: without it, CSS Grid's implicit auto-placement
   would happily slot .aside-list (columns 9/13) into row 1 beside the portrait
   (columns 5/9, no column overlap between them) instead of row 2 beside .prose,
   since auto-placement only avoids cell overlap, not "keep to the same visual
   row as the item that came before it" -- checked by reasoning through the grid,
   not assumed. */
/* width: 100% is required, not decorative: `margin: 0 auto` alone on a grid item
   with no explicit width requests fit-content sizing (auto side-margins override the
   default stretch), and this item's only child (.ph) is width: 100% of ITS parent --
   with nothing else to size against, the two collapse to 0 width together. Verified
   live: without this line the whole portrait silently rendered as a 0x0 box, present
   in the DOM (and correctly revealed, is-in and all) but completely invisible. The
   grid-column span above already makes this narrower than full width, so stretch
   plus an explicit width is enough; auto-margin centering was never needed on top of
   it. */
.about-story__media { grid-column: 5 / 9; grid-row: 1; width: 100%; margin-bottom: 56px; }
section[data-section="about-story"] .prose { grid-row: 2; }
section[data-section="about-story"] .aside-list { grid-row: 2; }
@media (max-width: 1023px) { .about-story__media { grid-column: 4 / 10; } }
@media (max-width: 767px) { .about-story__media { grid-column: 1 / 5; } }

.quote-full { margin: 0 0 56px; padding-bottom: 56px; border-bottom: 1px solid var(--line); }
.quote-full:last-child { border-bottom: 0; }
.quote-full blockquote { margin: 0; }
.quote-full blockquote p { font-family: var(--serif); font-size: var(--fs-heading-1); font-weight: 300; font-style: italic; line-height: 1.4; margin-bottom: 20px; }
.quote-full figcaption { font-size: var(--fs-micro); font-weight: var(--fw-ui); letter-spacing: 0.08em; text-transform: uppercase; }

/* ==========================================================================
   v2b motion + sliders pass (2026-08-12)
   ==========================================================================
   Every effect below is opt-in via a modifier class, layered on top of the
   existing .reveal / IntersectionObserver machinery wherever possible rather
   than inventing a second reveal system, because that machinery already
   survived two hardening rounds (mid-fade contrast sampling, jump-scroll
   stranding) and a fresh mechanism would start back at zero on both.

   Reduced motion: .reveal-based effects (.reveal--media, .rule--draw) need no
   extra rule in the prefers-reduced-motion block below, because initReveal()
   in main.js already adds .is-in to every .reveal element IMMEDIATELY (no
   observer, no stagger) when prefers-reduced-motion is set, and every
   .is-in end-state defined here is the fully visible one -- the existing
   guarantee just gets reused. Ken Burns is a plain CSS animation, already
   covered by the sitewide `animation: none !important`. Split-word, parallax
   and testimonial autoplay are gated in JS (initSplitHeadings/initParallax/
   initTestimonials all check matchMedia before doing anything), so under
   reduced motion they never touch the DOM at all and the page renders exactly
   as if this whole block did not exist. */

/* --- Image entrance: clip-path wipe instead of a plain fade --------------- */
/* Fix round 1 (2026-08-12): clip-path used to sit on the SAME element that
   main.js's IntersectionObserver watches (`.reveal.reveal--media` itself).
   Verified live (isolated `position: fixed` test div, headless Chrome): an
   element clipped to `inset(0 0 100% 0)` has a fresh IntersectionObserver
   report `intersectionRatio: 0` / `isIntersecting: false` NO MATTER WHERE IT
   SCROLLS TO -- Chromium computes the ratio from the element's own painted
   (clipped) area, so a self-clipped-to-zero box can never cross the 0.08
   threshold and the callback that would call reveal() never fires a second
   time. This was not a jump-scroll-only bug: a slow, ordinary 40-step
   incremental scroll (120px / 60ms, i.e. how a real visitor scrolls) left
   every `.reveal--media` card stuck at 100% clip the entire time it was
   inside the viewport; the only thing that ever revealed them was the
   scroll-driven safety sweep's raw `getBoundingClientRect().bottom < 0`
   once the card had ALREADY scrolled fully off-screen, i.e. after the
   visitor could no longer see it happen. `transform: scaleX(0)` (used by
   `.rule--draw` below) and `opacity: 0` do NOT corrupt the ratio the same
   way -- confirmed with the same isolated-div method -- so this is specific
   to clip-path, and specific to putting it on the observed element itself.
   Fix: `.reveal.reveal--media` (the node main.js observes and toggles
   `.is-in` on) now carries NO clip-path and stays a normal, always-fully-
   painted box, exactly like every plain `.reveal` fade element, so
   IntersectionObserver's geometry is never self-corrupted. The clip-path
   wipe moves one level down, onto the actual visual content (`img` for the
   picture/media pattern, `.ph` for the not-yet-shipped placeholder
   pattern) via a descendant selector -- reaches an `img` nested inside a
   `picture` inside a `.media` inside the observed wrapper (boots/about),
   an `img` that IS the observed element's own child (the JS-built stays/
   experiences slider cards, where `.media` and `.reveal--media` are the
   same div), and a `.ph` with no `img` at all (about.html's portrait
   placeholder) -- one rule, no per-pattern special-casing. */
.reveal.reveal--media { opacity: 1; transform: none; }
.reveal.reveal--media img,
.reveal.reveal--media .ph {
  clip-path: inset(0 0 100% 0);
  transition: clip-path var(--dur-slow) var(--ease);
}
.reveal.reveal--media.is-in img,
.reveal.reveal--media.is-in .ph { clip-path: inset(0 0 0 0); }

/* --- Gold hairline rules that draw in on entry ----------------------------- */
.reveal.rule--draw {
  opacity: 1;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-slow) var(--ease);
  margin: 28px 0 32px;
  max-width: 140px;
}
.reveal.rule--draw.is-in { transform: scaleX(1); }
.on-dark .rule--draw { border-top-color: var(--gold); }

/* --- Hero Ken Burns push --------------------------------------------------- */
/* 1.0 -> 1.06 over 20s, ease-out, single pass (not looping: a zoom that snaps
   back to 1.0 every 20s reads as a glitch, not a "push"). Scoped to the two
   hero shapes that exist on this site; both already sit inside an
   `overflow: hidden` ancestor (.hero, .page-hero), so the 6% growth never
   escapes its box. */
@keyframes kenburns { from { transform: scale(1); } to { transform: scale(1.06); } }
.hero .media--hero img, .page-hero .media--hero img {
  animation: kenburns var(--kenburns-dur) ease-out forwards;
  transform-origin: center;
}

/* --- Split-word headline reveal -------------------------------------------- */
/* Built by initSplitHeadings() in main.js, which wraps each word of a hero h1
   in .split-word (clips) > .split-word__inner (moves). Skipped entirely under
   reduced motion, so the fallback state here -- plain, unwrapped text -- is
   also what a no-JS visitor sees; nothing about the heading depends on this
   CSS ever running. */
.split-word { display: inline-block; overflow: hidden; vertical-align: top; }
.split-word__inner {
  display: inline-block;
  transform: translateY(110%);
  opacity: 0;
  transition: transform 700ms var(--ease), opacity 700ms var(--ease);
}
.split-word__inner.is-in { transform: translateY(0); opacity: 1; }

/* --- Parallax on section images -------------------------------------------- */
/* .media--parallax is added by initParallax() in main.js, never in markup --
   the class only exists once JS has confirmed prefers-reduced-motion is off,
   so its presence in the DOM already means motion is wanted. --parallax-y is
   the only thing JS touches per frame; scale(1.12) is static buffer so the
   +-24px translate JS applies never reveals the edge of the source image
   inside this box's `overflow: hidden`. Excluded on purpose from hero images
   (Ken Burns already owns that motion) and from .re__media (the one full-bleed
   image with body text sitting directly on top of it, where predictable,
   static contrast matters more than an extra flourish). */
.media--parallax img {
  transform: scale(1.12) translateY(var(--parallax-y, 0px));
  will-change: transform;
}

/* --- Shared slider engine (Signature stays, Experiences) ------------------- */
/* One drag/snap/keyboard controller (initDragSlider() in main.js) backs both
   the stays area-cards and the experiences strip; only card sizing differs
   between the two, set below under their own selectors. */
.slider__track {
  display: flex;
  gap: 24px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  touch-action: pan-y;
  cursor: grab;
  padding: 4px 4px 12px 4px;
  margin: 0 -4px;
}
.slider__track::-webkit-scrollbar { display: none; }
/* scroll-snap-type: x mandatory fights a JS-driven `element.scrollLeft = ...` on
   every pointermove tick: verified live, setting scrollLeft mid-drag was being
   snapped straight back to the nearest snap point before the assignment was even
   readable back, so a pointer drag could not move the track at all. Suspending
   snap for the duration of the drag (the same window .is-dragging is already
   applied) fixes it; removing the class on pointerup lets the browser's own native
   snap animation settle the track normally once the user lets go. */
.slider__track.is-dragging { cursor: grabbing; scroll-snap-type: none; }
.slider__track:focus-visible { outline: 2px solid var(--charcoal); outline-offset: 6px; }
.on-dark .slider__track:focus-visible { outline-color: var(--gold); }
.slider__controls { display: flex; align-items: center; gap: 16px; margin-top: 32px; }
.slider__btn {
  flex: 0 0 auto; width: 44px; height: 44px;
  background: transparent; border: 1px solid var(--line); color: var(--charcoal);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.slider__btn:hover { background: var(--gold); border-color: var(--gold); }
.slider__status { font-size: var(--fs-micro); color: rgba(34, 34, 34, 0.72); letter-spacing: 0.04em; }
.slider__progress { flex: 1 1 auto; max-width: 200px; height: 2px; background: var(--line); position: relative; overflow: hidden; }
.slider__progress-fill {
  position: absolute; inset: 0; background: var(--gold);
  transform-origin: left;
  transition: transform var(--dur) var(--ease);
}

/* Card sizing history, because two earlier attempts both measured as broken before
   landing here:
     1. A plain clamp() up to 380px: 3 stays cards nearly exactly filled a 1440px
        row (scrollWidth === clientWidth), so the track had nothing to scroll to and
        Next/arrow/drag were silently inert at the single most common desktop width.
     2. Near-full-width "one card at a time" cards (calc(100% - 88px), no cap): index
        tracking got clean and unambiguous, but a .media--tall (3:4) image at ~1100px
        wide renders ~1470px TALL, which blew the stays and experiences sections out
        to ~2500px each -- 45% of the entire homepage's height between the two of
        them, and (see budget.js below) diluted the real-estate band's share of the
        page to 6.87%, under the client's 7.5-12.5% floor.
   initDragSlider() in main.js no longer derives "which card is current" from scroll
   position at all for button/keyboard use (see the `index` comment there), which is
   what actually caused attempt 1's dead-end -- so cards can go back to a sane,
   moderate width without reintroducing that bug. Free dragging still resyncs from
   position, but only once scrolling has settled, which is unambiguous. */
.stays__area-card { flex: 0 0 clamp(280px, 32vw, 400px); scroll-snap-align: start; }
.stays__area-card .media { margin-bottom: 20px; }
.stays__area-card h3 { margin: 0 0 8px; }
.stays__area-card p { color: rgba(34, 34, 34, 0.72); }

.exp__card { flex: 0 0 clamp(220px, 24vw, 300px); scroll-snap-align: start; }
.exp__card .media { margin-bottom: 20px; }
.exp__card h3 { margin: 0 0 8px; }
.exp__card p { color: rgba(34, 34, 34, 0.72); }

@media (max-width: 767px) {
  .stays__area-card { flex-basis: 78vw; }
  .exp__card { flex-basis: 68vw; }
}

/* Cinematic Lagos arrival story */
.lagos-story { --story-progress: 0; position: relative; height: calc(100vh + 3300px); background: var(--charcoal); }
.lagos-story__stage { position: sticky; top: 0; height: 100vh; min-height: 620px; overflow: hidden; isolation: isolate; background: #15140f; }
.lagos-story__scene, .lagos-story__wash, .lagos-story__opening, .lagos-story__chapter, .lagos-story__services { position: absolute; }
.lagos-story__scene { inset: -5%; z-index: 0; }
.lagos-story__scene picture, .lagos-story__scene img { display: block; width: 100%; height: 100%; }
.lagos-story__scene img { object-fit: cover; }
.lagos-story__scene--city { opacity: clamp(0, calc(1.4 - var(--story-progress) * 3.2), 1); transform: scale(calc(1.04 + var(--story-progress) * .14)); }
.lagos-story__scene--arrival { opacity: clamp(0, calc((var(--story-progress) - .13) * 5.5), 1); transform: scale(calc(1.13 - var(--story-progress) * .08)); }
.lagos-story__scene--life { opacity: clamp(0, calc((var(--story-progress) - .47) * 5), 1); transform: scale(calc(1.11 - var(--story-progress) * .06)); }
.lagos-story__wash { inset: 0; z-index: 1; background: linear-gradient(180deg, rgba(10,9,5,.38), rgba(10,9,5,.12) 42%, rgba(10,9,5,.72)); }
/* The wash is deliberately at its WEAKEST (.12) at 42% height, which is exactly
   where the opening copy sits -- that is why the brand line, and to a lesser
   degree the tag pills, were washing out against a bright hero. This veil is an
   elliptical darkening centred on the copy only: strong where the text is, gone
   by the edges, so the photograph still reads as a photograph at the frame. It is
   pinned to the opening block rather than the section so it fades out with the
   copy on scroll instead of hanging over the chapters that follow. */
.lagos-story__veil { position: absolute; inset: 0; z-index: -1; pointer-events: none; background: radial-gradient(ellipse 78% 46% at 50% 46%, rgba(10,9,5,0.64), rgba(10,9,5,0.46) 55%, rgba(10,9,5,0) 100%); }
.lagos-story__opening { z-index: 2; inset: 0; display: grid; place-content: center; justify-items: center; padding: 120px 24px 44px; text-align: center; opacity: clamp(0, calc(1.35 - var(--story-progress) * 7), 1); transform: translateY(calc(var(--story-progress) * -180px)); }
.lagos-story__brand, .lagos-story__tags span { font-family: var(--sans); font-size: var(--fs-micro); font-weight: var(--fw-ui); line-height: 1.2; letter-spacing: .1em; text-transform: uppercase; }
/* Client correction 2026-08-25: "the yellow wording ... is kind of fading in the
   background". Measured, and she is right: --gold #C9A227 over the real hero
   photograph rendered at 2.67:1 mean and 1.30:1 against its lightest pixels, far
   under the 4.5:1 house bar. Two changes fix it together, and neither alone is
   enough. (1) --gold-on-photo, a lighter gold with the luminance to survive a
   golden-hour photograph while still reading as gold rather than as white.
   (2) .lagos-story__veil below, a focused darkening behind the opening copy only.
   Scoped under .on-dark so tools/checks/gold-rule.js can see it is on a dark
   ground; the unscoped selector this replaces was one of that check's violations. */
.on-dark .lagos-story__brand { margin: 0 0 18px; color: var(--gold-on-photo); }
.lagos-story__opening h1 { margin: 0; font-size: clamp(92px, 20vw, 310px); line-height: .7; letter-spacing: -.055em; text-transform: uppercase; }
.lagos-story__opening > p:not(.lagos-story__brand) { max-width: 660px; margin: 42px auto 0; font: 400 clamp(25px, 2.5vw, 38px)/1.15 var(--serif); }
.lagos-story__tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 28px; }
.lagos-story__tags span { padding: 12px 18px; border: 1px solid rgba(250,249,246,.5); border-radius: 999px; font-size: 11px; }
.lagos-story__chapter { z-index: 3; left: 50%; width: min(720px, calc(100vw - 40px)); text-align: center; pointer-events: none; }
.lagos-story__chapter h2, .lagos-story__services h2 { margin: 10px 0 0; color: var(--white); font-size: clamp(54px, 7vw, 104px); font-weight: 400; line-height: .88; letter-spacing: -.035em; }
.lagos-story__chapter > p:last-of-type { max-width: 570px; margin: 26px auto 0; color: rgba(250,249,246,.9); font-size: clamp(16px, 1.4vw, 20px); line-height: 1.65; }
.lagos-story__chapter--arrival { top: 55%; opacity: clamp(0, calc((var(--story-progress) - .14) * 7), calc((.50 - var(--story-progress)) * 7)); transform: translate(-50%, calc(-50% + (var(--story-progress) - .3) * -90px)); }
.lagos-story__facts { display: grid; grid-template-columns: repeat(2, 1fr); gap: 64px; width: min(430px, 100%); margin: 42px auto 0; }
.lagos-story__facts div { border-top: 1px solid var(--gold); padding-top: 16px; }
.lagos-story__facts dt { font: 400 52px/.9 var(--serif); }
.lagos-story__facts dd { margin: 10px 0 0; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; }
.lagos-story__chapter--life { top: 48%; opacity: clamp(0, calc((var(--story-progress) - .45) * 7), calc((.77 - var(--story-progress)) * 8)); transform: translate(-50%, calc(-50% + (var(--story-progress) - .58) * -90px)); }
.lagos-story__services { z-index: 4; inset: 0; display: grid; align-content: center; gap: 34px; padding: 110px max(24px, calc((100vw - 1320px) / 2)); background: linear-gradient(115deg, rgba(20,17,10,.88), rgba(20,17,10,.56)); opacity: clamp(0, calc((var(--story-progress) - .72) * 7), 1); transform: translateX(calc((1 - clamp(0, (var(--story-progress) - .72) * 5, 1)) * 110vw)); }
.lagos-story__services-head { max-width: 780px; }
.lagos-story__services h2 { font-size: clamp(46px, 6vw, 88px); }
.lagos-story__cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.lagos-story__card { min-height: 190px; padding: 24px; color: var(--charcoal); background: rgba(250,249,246,.95); text-decoration: none; transition: transform var(--dur), background var(--dur); }
.lagos-story__card:hover, .lagos-story__card:focus-visible { background: var(--white); transform: translateY(-8px); }
.lagos-story__card span { color: var(--plum); font-size: 12px; letter-spacing: .1em; }
.lagos-story__card strong { display: block; margin-top: 38px; font: 400 clamp(34px, 3vw, 48px)/1 var(--serif); }
.lagos-story__card small { display: block; margin-top: 12px; font-size: 14px; line-height: 1.5; }
.lagos-story__whatsapp { width: max-content; color: var(--white); font-size: 15px; text-decoration: underline; text-underline-offset: 6px; }
.lagos-story__whatsapp span { margin-left: 12px; color: var(--gold); }

@media (max-width: 700px) {
  .lagos-story { height: calc(100vh + 2750px); }
  .lagos-story__stage { min-height: 600px; }
  .lagos-story__opening { padding-top: 92px; }
  /* The veil's ellipse is proportional, so on a narrow viewport the brand line
     reaches closer to where the gradient has already faded out, and it measured
     4.29:1 on mobile against 4.80:1 on desktop. Widening the ellipse and holding
     the mid stop a little longer brings mobile back over the 4.5 bar. */
  .lagos-story__veil { background: radial-gradient(ellipse 108% 44% at 50% 44%, rgba(10,9,5,.68), rgba(10,9,5,.54) 62%, rgba(10,9,5,0) 100%); }
  .lagos-story__opening h1 { font-size: clamp(76px, 27vw, 116px); }
  .lagos-story__opening > p:not(.lagos-story__brand) { margin-top: 30px; }
  .lagos-story__tags span { padding: 10px 12px; font-size: 9px; }
  .lagos-story__facts { gap: 18px; }
  .lagos-story__services { padding: 90px 20px 28px; gap: 22px; overflow-y: auto; }
  .lagos-story__cards { grid-template-columns: 1fr; gap: 8px; }
  .lagos-story__card { min-height: 112px; padding: 16px 18px; }
  .lagos-story__card strong { margin-top: 16px; font-size: 32px; }
  .lagos-story__card small { margin-top: 4px; }
}
@media (prefers-reduced-motion: reduce) {
  .lagos-story__card { transition: none; }
}


/* Sitewide concierge narrative system */
.page-hero { min-height: 78vh; padding-bottom: clamp(64px, 9vw, 120px); }
.page-hero::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, rgba(12,11,8,.38), transparent 65%); pointer-events: none; }
.page-hero__copy { z-index: 1; max-width: 900px; }
.page-hero__copy h1 { max-width: 12ch; font-size: clamp(52px, 7.5vw, 116px); line-height: .86; }
.page-hero__lede { max-width: 590px; margin: 28px 0 0; color: rgba(250,249,246,.9); font-size: var(--fs-body-lg); line-height: 1.65; }

.trust-rail { position: relative; z-index: 2; margin-top: -1px; background: var(--charcoal); color: var(--white); }
.trust-rail__grid { display: grid; grid-template-columns: repeat(3, 1fr); padding-top: 0; padding-bottom: 0; }
.trust-rail p { display: grid; grid-template-columns: auto 1fr; column-gap: 16px; margin: 0; padding: 30px clamp(18px, 3vw, 40px); border-right: 1px solid var(--line-dark); }
.trust-rail p:first-child { padding-left: 0; }
.trust-rail p:last-child { border-right: 0; }
.trust-rail span { grid-row: 1 / 3; color: var(--gold); font-family: var(--serif); font-size: var(--fs-heading-2); }
.trust-rail strong { font-size: var(--fs-micro); letter-spacing: .06em; text-transform: uppercase; }
.trust-rail small { color: rgba(250,249,246,.7); font-size: var(--fs-micro); line-height: 1.5; }

.home-belief { padding: 90px 0 64px; background: var(--white); }
.home-belief__grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 24px; }
.home-belief__grid > .eyebrow { grid-column: 1 / 4; }
.home-belief__grid > h2 { grid-column: 4 / 12; max-width: 900px; font-size: var(--fs-display-2); line-height: .95; }
.home-belief__copy { grid-column: 7 / 12; margin-top: 42px; }
.home-belief__copy p { color: rgba(34,34,34,.74); font-size: var(--fs-body-lg); }
.home-belief__copy .actions__link { display: inline-block; margin-top: 18px; }

.prose.is-story-guide { display: grid; grid-template-columns: repeat(8, 1fr); gap: 0 24px; max-width: none; }
.prose.is-story-guide > .lede { grid-column: 1 / 8; max-width: 33ch; margin-bottom: clamp(80px, 10vw, 150px); font-size: var(--fs-display-3); line-height: 1.22; }
.story-guide__chapter { position: relative; grid-column: 2 / 8; display: grid; grid-template-columns: 2fr 3fr; gap: 36px; padding: 54px 0; border-top: 1px solid var(--line); }
.story-guide__chapter::before { content: attr(data-chapter); position: absolute; left: -58px; top: 58px; color: var(--plum); font-family: var(--serif); font-size: var(--fs-heading-2); }
.story-guide__chapter h2 { grid-column: 1; margin: 0; font-size: var(--fs-heading-1); line-height: 1.02; }
.story-guide__chapter > p, .story-guide__chapter > ul { grid-column: 2; margin-top: 0; }
.story-guide__chapter > p + p { margin-top: -18px; }
.story-guide__chapter ul { padding-left: 20px; }

.next-paths { padding: var(--section-y) 0; background: var(--charcoal); }
.next-paths .wrap { display: grid; grid-template-columns: repeat(12, 1fr); gap: 24px; }
.next-paths__head { grid-column: 1 / 6; position: sticky; top: 140px; align-self: start; }
/* Client correction 2026-08-25: "Where should we go next?" was breaking over FOUR
   lines. The cause was max-width: 7ch, which capped the heading at 269px inside a
   column that had 486px available, so it wrapped one short word per line.
   11ch is chosen over the other values that also give two lines (12ch to 14ch):
   it is the widest setting where the text box still fits INSIDE the column at
   every width, so the line break stays in the same place as the viewport changes.
   From 12ch up the column edge starts doing the wrapping instead, and the break
   moves around between breakpoints. Measured line counts, not estimated: two
   lines at 390, 600, 768, 900, 1024, 1100, 1280 and 1440. */
.next-paths__head h2 { max-width: 11ch; font-size: var(--fs-display-2); line-height: .9; }
.next-paths__grid { grid-column: 7 / 13; border-top: 1px solid var(--line-dark); }
.next-paths__grid a { display: grid; grid-template-columns: 54px 1fr auto; align-items: center; gap: 16px; min-height: 156px; padding: 24px 4px; border-bottom: 1px solid var(--line-dark); transition: padding var(--dur) var(--ease), background var(--dur) var(--ease); }
.next-paths__grid a:hover, .next-paths__grid a:focus-visible { padding-left: 18px; background: rgba(250,249,246,.05); }
.next-paths__grid span { color: var(--gold); font-family: var(--serif); font-size: var(--fs-heading-2); }
.next-paths__grid strong { display: block; font-family: var(--serif); font-size: var(--fs-heading-1); font-weight: 400; }
.next-paths__grid small { grid-column: 2; color: rgba(250,249,246,.68); font-size: var(--fs-body); }
.next-paths__grid i { grid-column: 3; grid-row: 1 / 3; color: var(--gold); font-style: normal; font-size: var(--fs-heading-2); }

.concierge-dock { position: fixed; right: 24px; bottom: 24px; z-index: 120; display: flex; align-items: center; gap: 14px; min-height: 52px; padding: 7px 7px 7px 20px; border-radius: 999px; color: var(--white); background: var(--charcoal); box-shadow: 0 14px 40px rgba(0,0,0,.22); font-size: var(--fs-micro); font-weight: var(--fw-ui); letter-spacing: .05em; text-transform: uppercase; }
.concierge-dock i { display: grid; place-items: center; width: 38px; height: 38px; border-radius: 50%; color: var(--charcoal); background: var(--gold); font-family: var(--sans); font-style: normal; font-size: var(--fs-heading-2); transition: transform var(--dur) var(--ease); }
.concierge-dock:hover i { transform: rotate(90deg); }

[data-section="about-story"] .prose { grid-column: 2 / 11; }
[data-section="about-story"] .aside-list { grid-column: 11 / 13; }

@media (max-width: 767px) {
  .page-hero { min-height: 82vh; }
  .page-hero__copy { grid-column: 1 / 5; }
  .page-hero__copy h1 { font-size: var(--fs-display-1); }
  .trust-rail__grid { grid-template-columns: 1fr; }
  .trust-rail p, .trust-rail p:first-child { padding: 20px 0; border-right: 0; border-bottom: 1px solid var(--line-dark); }
  .home-belief__grid { grid-template-columns: repeat(4, 1fr); }
  .home-belief__grid > .eyebrow, .home-belief__grid > h2, .home-belief__copy { grid-column: 1 / 5; }
  .home-belief__copy { margin-top: 24px; }
  .prose.is-story-guide { display: block; }
  .prose.is-story-guide > .lede { margin-bottom: 80px; }
  .story-guide__chapter { display: block; padding: 46px 0; }
  .story-guide__chapter::before { position: static; display: block; margin-bottom: 18px; }
  .story-guide__chapter h2 { margin-bottom: 22px; }
  .story-guide__chapter > p + p { margin-top: 0; }
  .next-paths .wrap { grid-template-columns: repeat(4, 1fr); }
  .next-paths__head, .next-paths__grid { grid-column: 1 / 5; }
  .next-paths__head { position: static; margin-bottom: 58px; }
  .next-paths__grid a { min-height: 132px; }
  .concierge-dock { right: 14px; bottom: 14px; }
}
@media (prefers-reduced-motion: reduce) {
  .concierge-dock i, .next-paths__grid a { transition: none; }
}

/* OriginKit Ring Gallery island, component source remains untouched in src/components/originkit. */
.home-belief__grid > .eyebrow { grid-row: 1; }
.home-belief__ring { grid-column: 1 / 6; grid-row: 2 / 4; height: 390px; min-height: 390px; display: grid; place-items: center; align-self: start; overflow: visible; transform: translateY(-104px); }
#originkit-ring-gallery { display: grid; place-items: center; width: 100%; min-height: 390px; }
.home-belief__grid > h2 { grid-column: 6 / 13; grid-row: 1; }
.home-belief__copy { grid-column: 7 / 12; grid-row: 2; }
@media (max-width: 767px) {
  .home-belief__grid > .eyebrow, .home-belief__ring, .home-belief__grid > h2, .home-belief__copy { grid-column: 1 / 5; grid-row: auto; }
  .home-belief__ring { height: 350px; min-height: 350px; margin: 8px 0 20px; overflow: hidden; }
  #originkit-ring-gallery { min-height: 350px; }
}

/* OriginKit Globe and Glitter Wrap islands. Exact components live unchanged in src/components/originkit. */
.stays { position: relative; isolation: isolate; overflow: hidden; }
.stays__glitter { position: absolute; inset: 0; z-index: -1; opacity: .28; background: var(--charcoal); pointer-events: none; }
.stays > .wrap { position: relative; z-index: 1; }
.stays .eyebrow, .stays h2, .stays .stays__request-copy { position: relative; }
.stays .eyebrow { color: var(--charcoal); }
.stays::after { content: ""; position: absolute; inset: 0; z-index: -1; background: linear-gradient(90deg, rgba(243,239,232,.92), rgba(243,239,232,.72) 48%, rgba(243,239,232,.9)); pointer-events: none; }

.contact-world { position: relative; min-height: 920px; overflow: hidden; background: var(--charcoal); color: var(--white); }
.contact-world > .wrap { position: relative; z-index: 2; align-items: center; }
.contact-world .prose { grid-column: 1 / 7; }
.contact-world .prose p { color: rgba(250,249,246,.82); }
.contact-world .aside-list { grid-column: 1 / 7; border-color: var(--line-dark); }
.contact-world .aside-list li { border-color: var(--line-dark); }
.contact-world__globe { position: absolute; z-index: 1; top: 50%; right: -5vw; width: min(58vw, 840px); aspect-ratio: 1; transform: translateY(-50%); }
@media (max-width: 767px) {
  .home-belief__ring { transform: translateY(-28px); }
  .contact-world { min-height: 1180px; padding-bottom: 540px; }
  .contact-world .prose, .contact-world .aside-list { grid-column: 1 / 5; }
  .contact-world__globe { top: auto; right: 50%; bottom: 20px; width: 620px; max-width: none; transform: translateX(50%); }
  .stays__glitter { opacity: .22; }
}
@media (prefers-reduced-motion: reduce) {
  .stays__glitter { display: none; }
}

/* OriginKit visual motion system for inner pages. Component source remains untouched. */
.page-hero__fold { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.page-hero__fold > div { width: 100% !important; height: 100% !important; }
.page-hero__fold canvas { width: 100% !important; height: 100% !important; }
.page-hero .hero__scrim { z-index: 1; }
.page-hero::after { z-index: 1; }
.page-hero__copy { z-index: 2; }

.visual-passage { position: relative; overflow: hidden; padding: 104px 0 56px; background: var(--charcoal); color: var(--white); }
.visual-passage::before { content: ""; position: absolute; inset: 0; background: radial-gradient(circle at 72% 28%, rgba(202,159,31,.14), transparent 30%), linear-gradient(180deg, #222 0%, #171612 100%); pointer-events: none; }
.visual-passage__copy { position: relative; z-index: 2; display: grid; grid-template-columns: repeat(12, 1fr); gap: 24px; }
.visual-passage__copy .eyebrow { grid-column: 1 / 4; color: var(--gold); }
.visual-passage__copy h2 { grid-column: 4 / 11; max-width: 820px; font-size: var(--fs-display-2); line-height: .94; }
.visual-passage__copy > p:last-child { grid-column: 7 / 12; max-width: 52ch; margin-top: 20px; color: rgba(250,249,246,.72); font-size: var(--fs-body-lg); line-height: 1.65; }
.visual-passage__gallery { position: relative; z-index: 1; height: 520px; margin-top: 44px; }

/* The second homepage chapter now reads first, then reveals the picture. */
.boots__copy { grid-column: 1 / 7; grid-row: 1; margin-top: 0; }
.boots__media { grid-column: 8 / 13; grid-row: 1; }

@media (max-width: 767px) {
  .home-belief { padding: 78px 0 54px; }
  .visual-passage { padding: 78px 0 42px; }
  .visual-passage__copy { grid-template-columns: repeat(4, 1fr); }
  .visual-passage__copy .eyebrow, .visual-passage__copy h2, .visual-passage__copy > p:last-child { grid-column: 1 / 5; }
  .visual-passage__copy h2 { font-size: var(--fs-display-1); }
  .visual-passage__copy > p:last-child { margin-top: 6px; }
  .visual-passage__gallery { height: 410px; margin-top: 30px; }
  .boots__copy { grid-column: 1 / 5; grid-row: 1; }
  .boots__media { grid-column: 1 / 5; grid-row: 2; margin-top: 34px; }
}

@media (prefers-reduced-motion: reduce) {
  .page-hero__fold { display: none; }
}

.exp__actions { display: flex; justify-content: center; margin-top: clamp(42px, 6vw, 76px); }
.exp__actions .btn { min-width: 248px; text-align: center; }

main > .section .prose > .lede:first-child { max-width: 44ch; margin-bottom: clamp(54px, 7vw, 92px); font-size: clamp(24px, 2.6vw, 38px); line-height: 1.3; letter-spacing: -.01em; }
@media (max-width: 767px) {
  main > .section .prose > .lede:first-child { font-size: 25px; margin-bottom: 56px; }
}

.lagos-story__opening h1.is-long-destination { max-width: 94vw; font-size: clamp(68px, 13vw, 196px); line-height: .76; }
@media (max-width: 700px) {
  .lagos-story__opening h1.is-long-destination { max-width: 96vw; font-size: clamp(52px, 17vw, 82px); line-height: .82; }
}

/* Reusable enquiry dialog and homepage trip planner. */
.has-open-modal { overflow: hidden; }
.enquiry-modal { width: min(940px, calc(100vw - 32px)); max-height: calc(100vh - 32px); padding: 0; border: 0; color: var(--charcoal); background: var(--ivory); box-shadow: 0 28px 90px rgba(0,0,0,.34); }
.enquiry-modal::backdrop { background: rgba(15,14,11,.76); backdrop-filter: blur(8px); }
.enquiry-modal__shell { position: relative; display: grid; grid-template-columns: 5fr 7fr; min-height: 620px; }
.enquiry-modal__close { position: absolute; top: 18px; right: 18px; z-index: 2; width: 42px; height: 42px; border-radius: 50%; color: var(--white); background: var(--charcoal); cursor: pointer; }
.enquiry-modal__intro { padding: clamp(44px, 6vw, 76px) clamp(28px, 5vw, 58px); color: var(--white); background: var(--charcoal); }
.enquiry-modal__intro h2 { margin-top: 18px; font-size: var(--fs-display-3); line-height: 1; }
.enquiry-modal__intro > p:last-child { margin-top: 28px; color: rgba(250,249,246,.72); line-height: 1.7; }
.enquiry-modal__form { display: grid; grid-template-columns: 1fr 1fr; align-content: start; gap: 22px; padding: clamp(54px, 6vw, 74px) clamp(28px, 5vw, 58px) 40px; }
.enquiry-modal__field { display: grid; gap: 8px; }
.enquiry-modal__field--wide, .enquiry-modal__dates, .enquiry-modal__submit, .enquiry-modal__form .enq__note { grid-column: 1 / -1; }
.enquiry-modal label, .enquiry-modal legend { font-size: var(--fs-micro); font-weight: var(--fw-ui); letter-spacing: .06em; text-transform: uppercase; }
.enquiry-modal input, .enquiry-modal select, .enquiry-modal textarea { width: 100%; min-height: 48px; padding: 11px 0; border: 0; border-bottom: 1px solid rgba(34,34,34,.3); border-radius: 0; color: var(--charcoal); background: transparent; font: inherit; }
.enquiry-modal textarea { resize: vertical; }
.enquiry-modal__dates { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin: 0; padding: 18px; border: 1px solid rgba(34,34,34,.18); }
.enquiry-modal__dates legend { padding: 0 8px; }
.enquiry-modal__dates div { display: grid; gap: 4px; }
.enquiry-modal__submit { justify-self: start; margin-top: 4px; }
.trip-planner { padding: clamp(76px, 7vw, 104px) 0; background: var(--charcoal); }
.trip-planner__layout { display: grid; grid-template-columns: repeat(12, 1fr); gap: 24px; align-items: start; }
.trip-planner__intro { grid-column: 1 / 6; position: sticky; top: 140px; }
.trip-planner__intro h2 { max-width: 8ch; margin-top: 18px; font-size: var(--fs-display-2); line-height: .94; }
.trip-planner__intro > p:last-child { max-width: 48ch; margin-top: 28px; color: rgba(250,249,246,.7); line-height: 1.7; }
.trip-planner__widget { grid-column: 7 / 13; padding: clamp(28px, 4vw, 52px); color: var(--charcoal); background: var(--ivory); }
.trip-planner fieldset { margin: 0; padding: 0; border: 0; }
.trip-planner legend { margin-bottom: 20px; font-family: var(--serif); font-size: var(--fs-heading-2); }
.trip-planner__choices { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.trip-planner__choices label { cursor: pointer; }
.trip-planner__choices input { position: absolute; opacity: 0; pointer-events: none; }
.trip-planner__choices span { display: flex; align-items: center; min-height: 62px; padding: 14px 18px; border: 1px solid rgba(34,34,34,.2); transition: background var(--dur), color var(--dur), border var(--dur); }
.trip-planner__choices input:checked + span { color: var(--white); background: var(--plum); border-color: var(--plum); }
.trip-planner__choices input:focus-visible + span { outline: 2px solid var(--gold); outline-offset: 2px; }
.trip-planner__details { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 34px; }
.trip-planner__details label { display: grid; gap: 10px; font-size: var(--fs-micro); font-weight: var(--fw-ui); letter-spacing: .04em; text-transform: uppercase; }
.trip-planner__details select { min-height: 50px; padding: 0 12px; border: 1px solid rgba(34,34,34,.24); color: var(--charcoal); background: var(--white); font: inherit; }
.trip-planner__submit { margin-top: 34px; }
@media (max-width: 767px) {
  .enquiry-modal { width: calc(100vw - 20px); max-height: calc(100vh - 20px); }
  .enquiry-modal__shell { display: block; min-height: 0; }
  .enquiry-modal__intro { padding: 40px 24px 30px; }
  .enquiry-modal__intro > p:last-child { margin-top: 16px; }
  .enquiry-modal__form { grid-template-columns: 1fr; gap: 16px; padding: 26px 24px 32px; }
  .enquiry-modal__field, .enquiry-modal__field--wide, .enquiry-modal__dates, .enquiry-modal__submit, .enquiry-modal__form .enq__note { grid-column: 1; }
  .enquiry-modal__dates { grid-template-columns: 1fr 1fr; }
  .trip-planner__layout { grid-template-columns: repeat(4, 1fr); }
  .trip-planner__intro, .trip-planner__widget { grid-column: 1 / 5; }
  .trip-planner__intro { position: static; margin-bottom: 40px; }
  .trip-planner__choices, .trip-planner__details { grid-template-columns: 1fr; }
}

/* Alternating visual service chapters across inner pages. */
.prose.is-story-guide > .story-guide__chapter--visual { grid-column: 1 / 9; grid-template-columns: repeat(12, 1fr); column-gap: 24px; align-items: center; min-height: 480px; padding: clamp(54px, 7vw, 96px) 0; }
.story-guide__chapter--visual::before { left: 0; top: clamp(58px, 7vw, 100px); z-index: 2; width: 48px; height: 48px; display: grid; place-items: center; color: var(--white); background: var(--plum); font-size: var(--fs-body); }
.story-guide__media { grid-column: 1 / 7; grid-row: 1 / 10; height: clamp(360px, 42vw, 560px); margin: 0; overflow: hidden; background: var(--ivory); }
.story-guide__media img { width: 100%; height: 100%; object-fit: cover; transition: transform 900ms var(--ease); }
.story-guide__chapter--visual:hover .story-guide__media img { transform: scale(1.035); }
.story-guide__chapter--visual > h2, .story-guide__chapter--visual > p, .story-guide__chapter--visual > ul { grid-column: 8 / 13; }
.story-guide__chapter--visual > h2 { align-self: end; margin: 0 0 14px; }
.story-guide__chapter--visual > p, .story-guide__chapter--visual > ul { align-self: start; }
.story-guide__chapter--visual:nth-of-type(even) .story-guide__media { grid-column: 7 / 13; }
.story-guide__chapter--visual:nth-of-type(even) > h2, .story-guide__chapter--visual:nth-of-type(even) > p, .story-guide__chapter--visual:nth-of-type(even) > ul { grid-column: 1 / 6; }
.story-guide__chapter--visual:nth-of-type(even)::before { left: auto; right: 0; }
@media (max-width: 767px) {
  .prose.is-story-guide > .story-guide__chapter--visual { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; min-height: 0; padding: 58px 0; }
  .story-guide__chapter--visual::before, .story-guide__chapter--visual:nth-of-type(even)::before { position: absolute; top: 42px; left: 14px; right: auto; }
  .story-guide__media, .story-guide__chapter--visual:nth-of-type(even) .story-guide__media { grid-column: 1 / 5; grid-row: auto; height: 390px; margin-bottom: 34px; }
  .story-guide__chapter--visual > h2, .story-guide__chapter--visual > p, .story-guide__chapter--visual > ul, .story-guide__chapter--visual:nth-of-type(even) > h2, .story-guide__chapter--visual:nth-of-type(even) > p, .story-guide__chapter--visual:nth-of-type(even) > ul { grid-column: 1 / 5; }
}
@media (prefers-reduced-motion: reduce) { .story-guide__media img { transition: none; } }

/* Compact testimonial card slider on About. */
[data-section="about-quotes"] { overflow: hidden; }
[data-section="about-quotes"] .wrap { display: block; }
.about-testimonials__head { display: grid; grid-template-columns: repeat(12, 1fr); gap: 24px; align-items: end; margin-bottom: clamp(42px, 6vw, 76px); }
.about-testimonials__head .eyebrow { grid-column: 1 / 4; }
.about-testimonials__head h2 { grid-column: 4 / 10; max-width: 7ch; font-size: var(--fs-display-2); line-height: .95; }
.testimonial-slider__viewport { overflow: visible; }
.testimonial-slider__track { --testimonial-index: 0; display: flex; gap: 22px; transform: translateX(calc(var(--testimonial-index) * (min(620px, 62vw) + 22px) * -1)); transition: transform 700ms var(--ease); }
.testimonial-card { position: relative; flex: 0 0 min(620px, 62vw); min-height: 350px; margin: 0; padding: clamp(30px, 4vw, 52px); border: 1px solid var(--line); background: var(--white); opacity: .42; transform: scale(.96); transition: opacity 500ms var(--ease), transform 500ms var(--ease); }
.testimonial-card.is-active { opacity: 1; transform: scale(1); }
.testimonial-card__mark { display: block; height: 54px; color: var(--plum); font: 400 82px/.8 var(--serif); }
.testimonial-card blockquote { margin: 18px 0 0; }
.testimonial-card blockquote p { margin: 0; font-family: var(--serif); font-size: clamp(22px, 2vw, 30px); font-style: italic; font-weight: 300; line-height: 1.42; }
.testimonial-card figcaption { margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--line); font-size: var(--fs-micro); font-weight: var(--fw-ui); letter-spacing: .08em; text-transform: uppercase; }
.testimonial-slider__controls { display: flex; align-items: center; gap: 16px; margin-top: 28px; }
.testimonial-slider__controls button { width: 46px; height: 46px; border: 1px solid var(--line); color: var(--charcoal); background: transparent; cursor: pointer; transition: color var(--dur), background var(--dur); }
.testimonial-slider__controls button:hover { color: var(--white); background: var(--plum); }
.testimonial-slider__controls p { min-width: 58px; margin: 0; text-align: center; font-size: var(--fs-micro); font-weight: var(--fw-ui); letter-spacing: .08em; }
@media (max-width: 767px) {
  .about-testimonials__head { grid-template-columns: repeat(4, 1fr); }
  .about-testimonials__head .eyebrow, .about-testimonials__head h2 { grid-column: 1 / 5; }
  .about-testimonials__head h2 { margin-top: 12px; }
  .testimonial-slider__track { gap: 12px; transform: translateX(calc(var(--testimonial-index) * (calc(100vw - 40px) + 12px) * -1)); }
  .testimonial-card { flex-basis: calc(100vw - 40px); min-height: 320px; padding: 28px 24px; }
  .testimonial-card blockquote p { font-size: 22px; }
}
@media (prefers-reduced-motion: reduce) { .testimonial-slider__track, .testimonial-card { transition: none; } }

/* Typography parity: About and future editorial/blog content use the homepage type system. */
[data-section="about-story"] .story-guide__chapter h2 { font-family: var(--serif); font-size: var(--fs-display-3); font-weight: 300; line-height: 1.08; letter-spacing: -.015em; }
[data-section="about-story"] .story-guide__chapter > p { font-family: var(--sans); font-size: var(--fs-body); font-weight: 400; line-height: 1.7; letter-spacing: normal; }
[data-section="about-story"] .prose > .lede { font-family: var(--sans); font-size: clamp(24px, 2.6vw, 38px); font-weight: 400; line-height: 1.3; letter-spacing: -.01em; }
.about-testimonials__head h2 { font-family: var(--serif); font-size: var(--fs-display-3); font-weight: 300; line-height: 1.08; letter-spacing: -.015em; }
.testimonial-card blockquote p { font-family: var(--serif); font-weight: 300; line-height: 1.42; letter-spacing: normal; }
.blog-index h1, .blog-post h1, .article-hero h1 { font-family: var(--serif); font-size: var(--fs-display-1); font-weight: 300; line-height: .92; letter-spacing: -.025em; }
.blog-index h2, .blog-post h2, .article-body h2 { font-family: var(--serif); font-size: var(--fs-display-3); font-weight: 300; line-height: 1.08; letter-spacing: -.015em; }
.blog-index h3, .blog-post h3, .article-body h3 { font-family: var(--serif); font-size: var(--fs-heading-2); font-weight: 400; line-height: 1.12; letter-spacing: normal; }
.blog-index p, .blog-post p, .article-body p { font-family: var(--sans); font-size: var(--fs-body); font-weight: 400; line-height: 1.7; letter-spacing: normal; }
.blog-index .eyebrow, .blog-post .eyebrow, .article-hero .eyebrow { font-family: var(--sans); font-size: var(--fs-micro); font-weight: 700; line-height: 1.7; letter-spacing: .08em; text-transform: uppercase; }
@media (max-width: 767px) {
  [data-section="about-story"] .story-guide__chapter h2, .about-testimonials__head h2 { font-size: var(--fs-heading-1); }
  .blog-index h1, .blog-post h1, .article-hero h1 { font-size: var(--fs-display-2); }
}
