/* ==========================================================================
   Sai Palm View — theme layer + all section styles.
   Load order matters: vendor.css (Bootstrap) must come first.
   ========================================================================== */

/* ===== theme ===== */
/* ==========================================================================
   Sai Palm View — Bootstrap 5.3 theme layer

   Loaded AFTER bootstrap.min.css. Retunes Bootstrap's own CSS variables so that
   stock Bootstrap classes (.text-primary, .btn, .accordion, .row, .container-fluid)
   already carry the Figma design tokens. Sections should reach for Bootstrap
   utilities first and only fall back to their own CSS for geometry Bootstrap
   cannot express.
   ========================================================================== */

:root {
  /* ---- The design unit -------------------------------------------------
     The Figma frame is 1920 wide. --u is one Figma pixel. js/main.js sets it
     to clientWidth/1920 (exactly 1px at design width); this is the fallback. */
  --u: calc(100vw / 1920);

  /* ---- Palette --------------------------------------------------------- */
  --spv-cream: #fffded;
  --spv-paper: #f0ede9;
  --spv-gold: #a9834a;
  --spv-gold-line: #cfa854;
  --spv-ink: #595a5c;
  --spv-rule: #daccb1;
  --spv-overlay-paper: rgba(240, 237, 233, 0.69);

  /* ---- Bootstrap overrides --------------------------------------------- */
  --bs-body-bg: var(--spv-cream);
  --bs-body-bg-rgb: 255, 253, 237;
  --bs-body-color: var(--spv-ink);
  --bs-body-color-rgb: 89, 90, 92;

  --bs-primary: var(--spv-gold);
  --bs-primary-rgb: 169, 131, 74;
  --bs-primary-text-emphasis: var(--spv-gold);
  --bs-link-color: var(--spv-gold);
  --bs-link-color-rgb: 169, 131, 74;
  --bs-link-hover-color: var(--spv-gold-line);
  --bs-link-hover-color-rgb: 207, 168, 84;
  --bs-border-color: var(--spv-rule);
  --bs-border-color-translucent: rgba(169, 131, 74, 0.2);

  --bs-font-sans-serif: "Inter", "Haas Grot Disp Trial", "Helvetica Neue", Arial, sans-serif;
  --bs-body-font-family: var(--bs-font-sans-serif);
  --bs-body-font-weight: 400;
  --bs-body-line-height: 1.2;

  /* Design font stacks */
  --spv-font-display: "Cormorant Garamond", "Canela Deck Trial", Georgia, serif;
  --spv-font-sans: var(--bs-font-sans-serif);
  --spv-font-script: "Fleur De Leah", "Snell Roundhand", cursive;

  /* Focus ring in the brand colour rather than Bootstrap blue */
  --bs-focus-ring-color: rgba(169, 131, 74, 0.35);
}

/* --------------------------------------------------------------------------
   Page shell
   -------------------------------------------------------------------------- */

/* Hold the scrollbar's 6px gutter open even while nothing is scrolling.

   Three things on this page hide the page scrollbar: the loading veil
   (html.spv-loading), Lenis's own lock (.lenis-stopped) and Bootstrap's modal
   and offcanvas. Without a reserved gutter each of those widens the layout by
   the scrollbar's width for as long as it is up and drops it back afterwards,
   and since the whole design is width-driven — full-bleed sections on w-100,
   everything else centred on the 1920 canvas — the page visibly jumps sideways
   on open and again on close.

   Bootstrap has its own answer (padding-right on <body> to stand in for the
   scrollbar it removed), but it cannot reach here: main.js parks Lenis on
   show.bs.modal, .lenis-stopped takes the scrollbar away before Bootstrap
   measures it on the next line, so Bootstrap reads a width of 0 and pads
   nothing. Reserving the gutter in CSS settles it for all four cases at once
   and leaves Bootstrap correctly measuring 0. */
html {
  scrollbar-gutter: stable;
}

body {
  overflow-x: hidden;
}

img {
  display: block;
  max-width: 100%;
}

/* --------------------------------------------------------------------------
   Containers — expressed through Bootstrap's own gutter variable.
   .container-fluid pads by --bs-gutter-x * .5 on each side, so a 280u gutter
   reproduces the design's 140px side margin at 1920. The footer uses 112px.
   -------------------------------------------------------------------------- */
.spv-gutter {
  --bs-gutter-x: calc(280 * var(--u));
}

.spv-gutter-footer {
  --bs-gutter-x: calc(224 * var(--u));
}

/* Rows inside a design container should not inherit the page gutter. */
.spv-gutter > .row,
.spv-gutter-footer > .row {
  --bs-gutter-x: 0;
}

/* --------------------------------------------------------------------------
   Type utilities — Bootstrap's .fs-1..6 scale does not reach the display sizes
   this design uses, so the Figma scale is exposed in the same spirit.
   .fs-d<N> == N Figma px.
   -------------------------------------------------------------------------- */
.fs-d180 { font-size: calc(180 * var(--u)); }
.fs-d110 { font-size: calc(110 * var(--u)); }
.fs-d105 { font-size: calc(105 * var(--u)); }
.fs-d93  { font-size: calc(93  * var(--u)); }
.fs-d80  { font-size: calc(80  * var(--u)); }
.fs-d79  { font-size: calc(79  * var(--u)); }
.fs-d64  { font-size: calc(64  * var(--u)); }
.fs-d58  { font-size: calc(58  * var(--u)); }
.fs-d48  { font-size: calc(48  * var(--u)); }
.fs-d44  { font-size: calc(44  * var(--u)); }
.fs-d40  { font-size: calc(40  * var(--u)); }
.fs-d38  { font-size: calc(38  * var(--u)); }
.fs-d36  { font-size: calc(36  * var(--u)); }
.fs-d32  { font-size: calc(32  * var(--u)); }
.fs-d29  { font-size: calc(29.2 * var(--u)); }
.fs-d28  { font-size: calc(28  * var(--u)); }
.fs-d24  { font-size: calc(24  * var(--u)); }
.fs-d22  { font-size: calc(22  * var(--u)); }
.fs-d20  { font-size: calc(20  * var(--u)); }
.fs-d18  { font-size: calc(18  * var(--u)); }
.fs-d16  { font-size: calc(16  * var(--u)); }
.fs-d14  { font-size: calc(14  * var(--u)); }

/* Font families */
.ff-display { font-family: var(--spv-font-display); }
.ff-sans    { font-family: var(--spv-font-sans); }
.ff-script  { font-family: var(--spv-font-script); text-transform: lowercase; }

/* Line heights Bootstrap does not ship. .lh-d<N> == N Figma px of leading. */
.lh-d40 { line-height: calc(40 * var(--u)); }
.lh-d36 { line-height: calc(36 * var(--u)); }
.lh-d30 { line-height: calc(30 * var(--u)); }
.lh-d115 { line-height: 1.15; }

/* Letter-spacing used by the small caps labels */
.ls-wide { letter-spacing: 0.08em; }

/* --------------------------------------------------------------------------
   Brand components, themed through Bootstrap's component variables so the
   stock .btn / .accordion behaviour and markup still apply.
   -------------------------------------------------------------------------- */

/* Gold outline button — Figma 231x64, 2px #CFA854, 20px sans */
.btn-spv {
  --bs-btn-color: var(--spv-gold);
  --bs-btn-border-color: var(--spv-gold-line);
  --bs-btn-border-width: max(1px, calc(2 * var(--u)));
  --bs-btn-border-radius: 0;
  --bs-btn-hover-color: var(--spv-cream);
  --bs-btn-hover-bg: var(--spv-gold-line);
  --bs-btn-hover-border-color: var(--spv-gold-line);
  --bs-btn-active-color: var(--spv-cream);
  --bs-btn-active-bg: var(--spv-gold);
  --bs-btn-active-border-color: var(--spv-gold);
  --bs-btn-padding-x: calc(20 * var(--u));
  --bs-btn-padding-y: calc(12 * var(--u));
  --bs-btn-font-family: var(--spv-font-sans);
  --bs-btn-font-size: calc(20 * var(--u));
  --bs-btn-line-height: 1.2;
  min-width: calc(231 * var(--u));
  min-height: calc(64 * var(--u));
  white-space: nowrap;
  transition: color 0.4s ease, background-color 0.4s ease, border-color 0.4s ease;
}

/* Accordion, retuned to the flat gold rules the design uses. */
.accordion-spv {
  --bs-accordion-bg: transparent;
  --bs-accordion-color: var(--spv-gold);
  --bs-accordion-border-color: var(--spv-rule);
  --bs-accordion-border-width: 0;
  --bs-accordion-border-radius: 0;
  --bs-accordion-inner-border-radius: 0;
  --bs-accordion-btn-padding-x: 0;
  --bs-accordion-btn-padding-y: calc(10 * var(--u));
  --bs-accordion-btn-color: var(--spv-gold);
  --bs-accordion-btn-bg: transparent;
  --bs-accordion-btn-icon-width: 0;
  --bs-accordion-btn-focus-box-shadow: none;
  --bs-accordion-active-color: var(--spv-gold);
  --bs-accordion-active-bg: transparent;
  --bs-accordion-body-padding-x: 0;
  --bs-accordion-body-padding-y: calc(18 * var(--u));
}

.accordion-spv .accordion-button {
  font-family: var(--spv-font-sans);
  font-size: calc(24 * var(--u));
  text-transform: uppercase;
  letter-spacing: 0.02em;
  box-shadow: none;
}

/* The stock chevron is replaced by the design's +/- glyph. */
.accordion-spv .accordion-button::after {
  display: none;
}

.accordion-spv .accordion-item {
  border-bottom: max(1px, calc(1.2 * var(--u))) solid var(--spv-rule);
}

/* --------------------------------------------------------------------------
   Section shell — the absolute-positioning canvas used where the design is a
   free composition rather than a flow layout.
   -------------------------------------------------------------------------- */
.sec {
  position: relative;
  width: 100%;
  overflow: hidden;
  isolation: isolate;
}

/* A full-bleed backdrop layer. Pair with Bootstrap's object-fit-cover. */
.layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* The cream paper wash that sits over most texture plates. */
.paper-wash::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--spv-overlay-paper);
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   "KNOW MORE" circle + arrow link, shared by five sections
   -------------------------------------------------------------------------- */
.know-more {
  position: relative;
  display: inline-block;
  height: calc(134 * var(--u));
  cursor: pointer;
}

.know-more__circle {
  position: absolute;
  top: 0;
  height: calc(134 * var(--u));
  width: calc(128 * var(--u));
  transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Figma models the rule as a zero-height vector whose artwork bleeds out via
   inset(-3.68px -0.63%); the drawn box is 80.158 x 7.364. */
.know-more__rule {
  position: absolute;
  top: calc(63.32 * var(--u));
  height: calc(7.364 * var(--u));
  width: calc(80.158 * var(--u));
  transform-origin: left center;
  transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}

.know-more__rule > img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.know-more__label {
  position: absolute;
  top: calc(55 * var(--u));
  font-family: var(--spv-font-sans);
  font-size: calc(20 * var(--u));
  line-height: 1.2;
  color: var(--spv-gold);
  white-space: nowrap;
}

.know-more:hover .know-more__circle { transform: translateX(calc(12 * var(--u))); }
.know-more:hover .know-more__rule   { transform: scaleX(1.18); }

/* --------------------------------------------------------------------------
   Motion primitives. Initial states apply only under .js-anim, which main.js
   adds to <html>, so the page is fully visible without JS.
   -------------------------------------------------------------------------- */
.js-anim [data-anim] { will-change: transform, opacity; }

.js-anim [data-anim="fade"]        { opacity: 0; }
.js-anim [data-anim="fade-up"]     { opacity: 0; transform: translate3d(0, calc(48 * var(--u)), 0); }
.js-anim [data-anim="slide-left"]  { opacity: 0; transform: translate3d(calc(80 * var(--u)), 0, 0); }
.js-anim [data-anim="slide-right"] { opacity: 0; transform: translate3d(calc(-80 * var(--u)), 0, 0); }
.js-anim [data-anim="zoom-out"]    { transform: scale(1.12); }
.js-anim [data-anim="mask-up"]     { clip-path: inset(100% 0 0 0); }
.js-anim [data-anim="mask-down"]   { clip-path: inset(0 0 100% 0); }
.js-anim [data-anim="clip-reveal"] { clip-path: inset(0 100% 0 0); }

.split-line { display: block; overflow: hidden; }
.split-line__inner { display: block; }

@media (prefers-reduced-motion: reduce) {
  .js-anim [data-anim] {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
  }
}

/* --------------------------------------------------------------------------
   Scrollbar + Lenis
   -------------------------------------------------------------------------- */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--spv-cream); }
::-webkit-scrollbar-thumb { background: rgba(169, 131, 74, 0.45); border-radius: 3px; }

html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-stopped { overflow: hidden; }

/* --------------------------------------------------------------------------
   Responsive position utilities.

   Bootstrap 5.3 ships .position-* but NOT responsive variants of them, so a
   section that is absolutely positioned on desktop and in normal flow on mobile
   otherwise has to fight Bootstrap's !important with its own. These follow
   Bootstrap's own naming and specificity so sections can write
   `position-absolute position-lg-absolute` / `position-static` cleanly.
   -------------------------------------------------------------------------- */
@media (min-width: 576px) {
  .position-sm-static   { position: static !important; }
  .position-sm-relative { position: relative !important; }
  .position-sm-absolute { position: absolute !important; }
}
@media (min-width: 768px) {
  .position-md-static   { position: static !important; }
  .position-md-relative { position: relative !important; }
  .position-md-absolute { position: absolute !important; }
}
@media (min-width: 992px) {
  .position-lg-static   { position: static !important; }
  .position-lg-relative { position: relative !important; }
  .position-lg-absolute { position: absolute !important; }
}
@media (min-width: 1200px) {
  .position-xl-static   { position: static !important; }
  .position-xl-relative { position: relative !important; }
  .position-xl-absolute { position: absolute !important; }
}

/* Figma's plate opacities fall between Bootstrap's 25/50/75 steps. */
.opacity-15 { opacity: 0.15 !important; }
.opacity-26 { opacity: 0.26 !important; }
.opacity-39 { opacity: 0.39 !important; }
.opacity-70 { opacity: 0.70 !important; }


/* ===== section: hero ===== */
/* ==========================================================================
   HERO — Figma component set 249:712, settled frame 249:705 (Component 72)
   Layout is Bootstrap-driven; this file holds only the Figma coordinates,
   the fixed canvas height and the intro timeline's start states.
   ========================================================================== */

.sec--hero {
  height: calc(1080 * var(--u));
}

/* ---- Backdrops ---------------------------------------------------------- */

/* Figma 249:680 / 249:681 sit at opacity 0 in the settled 8th frame. Declared
   here rather than with Bootstrap's .opacity-0, because that utility is
   !important and GSAP's inline styles could never animate past it. */
.sec--hero .hero__plate,
.sec--hero .hero__foliage {
  opacity: 0;
}

/* Figma 249:681 "image 61": 1920x1152 at y=6 — 72px taller than the frame,
   which is the slack the opening zoom uses. */
.sec--hero .hero__foliage {
  top: calc(6 * var(--u));
  height: calc(1152 * var(--u));
}

/* ---- Navigation --------------------------------------------------------- */
/* One row on the masthead line (top 68, 41 tall), centred on the canvas
   between the brand at the left gutter and the developer mark at the right.
   The six labels measure ~497 together, so a 60px gap either side of each
   rule holds the row near the ~797 wide it occupied as two Figma groups. */
.spv-header .hero__nav {
  inset: 0;
  pointer-events: none;
}

.spv-header .hero__nav-group {
  position: absolute;
  transition: top 0.4s cubic-bezier(0.16, 1, 0.3, 1);
  top: calc(68 * var(--u));
  left: 0;
  right: 0;
  height: calc(41 * var(--u));
  gap: calc(60 * var(--u));
  pointer-events: auto;
}

.spv-header .hero__nav-item {
  height: calc(41 * var(--u));
}

/* Hairline rule between items — decorative, so it is drawn rather than
   marked up, and it sits in the middle of the gap it precedes. */
.spv-header .hero__nav-item + .hero__nav-item::before {
  content: "";
  position: absolute;
  left: calc(-30 * var(--u));
  top: 50%;
  width: max(1px, calc(1 * var(--u)));
  height: calc(22 * var(--u));
  background: rgba(255, 255, 255, 0.45);
  transform: translateY(-50%);
}

.spv-header .hero__nav-link {
  line-height: calc(41 * var(--u));
  position: relative;
  transition: color 0.35s ease, font-size 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Gold underline on hover — the rule Figma draws under the active item. */
.spv-header .hero__nav-link::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: calc(2 * var(--u));
  width: 0;
  height: max(1px, calc(1.5 * var(--u)));
  background: var(--spv-gold-line);
  transform: translateX(-50%);
  transition: width 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}

.spv-header .hero__nav-link:hover,
.spv-header .hero__nav-link:focus-visible {
  color: var(--spv-gold-line) !important;
}

.spv-header .hero__nav-link:hover::after,
.spv-header .hero__nav-link:focus-visible::after {
  width: 100%;
}

/* ---- Developer mark ----------------------------------------------------- */
/* Paradise Group, on the right gutter — 86 in, mirroring the brand on the
   left. 220x63.5 (the artwork is 1400x404, so 220 wide lands on 63.5 tall),
   centred on the nav's own centre line at y=88.5. */
.spv-header .hero__partner {
  top: calc(56.75 * var(--u));
  transition:
    top 0.4s cubic-bezier(0.16, 1, 0.3, 1),
    width 0.4s cubic-bezier(0.16, 1, 0.3, 1),
    height 0.4s cubic-bezier(0.16, 1, 0.3, 1);
  right: calc(86 * var(--u));
  width: calc(220 * var(--u));
  height: calc(63.5 * var(--u));
  pointer-events: none;
}

/* ---- Logo --------------------------------------------------------------- */
/* Settled: the Figma artwork size, 67.633 x 138.47 inside a 64.455 x 138 clip
   (the clip is narrower than the artwork, exactly as Figma draws it), parked
   at the left gutter x=86 — the same gutter the sections below open on. */
.spv-header .hero__logo {
  top: calc(20 * var(--u));
  transition:
    top 0.4s cubic-bezier(0.16, 1, 0.3, 1),
    width 0.4s cubic-bezier(0.16, 1, 0.3, 1),
    height 0.4s cubic-bezier(0.16, 1, 0.3, 1);
  left: calc(86 * var(--u));
  width: calc(64.455 * var(--u));
  height: calc(138 * var(--u));
  overflow: hidden;
}

/* Both lockups sit in the same box and cross-fade; .hero__logo itself is left
   alone because the intro timeline owns its opacity and its transform. */
.spv-header .hero__logo > img {
  position: absolute;
  transition-property: width, height, opacity;
  transition-duration: 0.4s, 0.4s, 0.35s;
  transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
  left: 50%;
  top: 50%;
  width: calc(67.633 * var(--u));
  height: calc(138.47 * var(--u));
  transform: translate(-50%, -50%);
  object-fit: contain;
}

/* ---- The fixed bar ------------------------------------------------------
   The masthead is no longer part of the hero: it is a fixed bar the whole
   page scrolls under. Three states, all driven by js/main.js:

     (none)              transparent, and laid out exactly as Figma draws the
                         hero masthead — brand 138 tall at top 20, nav on the
                         68 line, developer mark in the right margin
     --scrolled (y>60)   collapses to an 88 bar over a dark veil, because from
                         here down the page runs cream and white type would
                         vanish
     --hidden            translated off the top edge while the visitor scrolls
                         DOWN; removing it slides the bar back in

   --ready gates the transform transition. Without it the bar would animate in
   from -100% on the first paint of a page that loads already scrolled.

   Only the bar's own height is transitioned for the backdrop; the children are
   absolutely positioned, so their boxes move independently and the hero intro
   timeline's inline transform on .hero__logo is never fought over (the logo is
   resized through its box and its <img>, not through a transform). */
.spv-header {
  height: calc(170 * var(--u));
  z-index: 1030; /* above the page, below the offcanvas (1045) and modal (1055) */
  pointer-events: none; /* the bar is mostly empty; the hero keeps its clicks */
  transition: height 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

/* The veil is a child layer rather than a background, so it can cross-fade
   while the bar itself is sliding. */
.spv-header::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(30, 22, 12, 0);
  transition: background 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}

.spv-header--ready {
  transition:
    height 0.4s cubic-bezier(0.16, 1, 0.3, 1),
    transform 0.45s cubic-bezier(0.16, 1, 0.3, 1);
}

.spv-header--hidden {
  transform: translateY(-100%);
}

.spv-header--scrolled {
  height: calc(100 * var(--u));
}

.spv-header--scrolled::before {
  background: rgba(30, 22, 12, 0.88);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  box-shadow: 0 1px 0 rgba(207, 168, 84, 0.25);
}

/* Compact boxes — everything centres on the 50 line of the 100 bar.

   The brand is a TALL lockup (crest over wordmark over rule, 0.4884 wide to
   high), so the bar's height is really set by how big the logo has to be to
   stay readable: 88 of the 100 is the mark, and the 6 above and below is all
   the bar can spare. Both boxes keep Figma's clip, which is fractionally
   narrower than the artwork it holds — 64.455:67.633 wide, 138:138.47 high —
   so the settled proportions survive the 0.6377 reduction.

   The nav drops to 20 here. At the hero's 24 it out-shouted the brand once
   both were squeezed onto one line. */
.spv-header--scrolled .hero__logo {
  top: calc(6 * var(--u));
  width: calc(41.1 * var(--u));
  height: calc(88 * var(--u));
}

.spv-header--scrolled .hero__logo > img {
  width: calc(43.1 * var(--u));
  height: calc(88.3 * var(--u));
}

.spv-header .hero__logo-light {
  opacity: 0;
}

.spv-header--scrolled .hero__logo-light {
  opacity: 1;
}

.spv-header--scrolled .hero__logo > img:not(.hero__logo-light) {
  opacity: 0;
}

.spv-header--scrolled .hero__nav-group {
  top: calc(29.5 * var(--u));
}

.spv-header--scrolled .hero__nav-link {
  font-size: calc(20 * var(--u));
}

.spv-header--scrolled .hero__partner {
  top: calc(27 * var(--u));
  width: calc(159.4 * var(--u));
  height: calc(46 * var(--u));
}

/* The burger lives outside the bar (it has to outrank the offcanvas it
   closes), so main.js hands it the same hidden state to travel with. */
.spv-burger--hidden {
  transform: translateY(calc(-100% - 18px));
}

@media (prefers-reduced-motion: reduce) {
  .spv-header,
  .spv-header--ready {
    transition: none;
  }
}

/* ---- Tagline ------------------------------------------------------------ */
/* Figma 249:702 — a 1622x127 clip at (149,953) holding the 80px line. */
.sec--hero .hero__tagline {
  left: calc(149 * var(--u));
  top: calc(953 * var(--u));
  width: calc(1622 * var(--u));
  height: calc(127 * var(--u));
}

.sec--hero .hero__tagline-text {
  /* Cormorant Garamond sets ~6% narrower than Canela Deck Thin at 80px, so the
     line is tracked out to hold Figma's 1532px measure inside the 1622 box.
     One line on desktop; the mobile block lets it wrap (Bootstrap's
     .text-nowrap is !important, so the nowrap lives here instead). */
  white-space: nowrap;
  letter-spacing: 0.018em;
  padding-top: calc(15 * var(--u));
}

/* ---- Intro timeline start states ---------------------------------------
   Only applied when JS is driving the sequence (.js-anim on <html>), so the
   settled frame above is what renders with JS off or reduced motion on. */
.js-anim .sec--hero .hero__tower,
.js-anim .spv-header .hero__nav-group,
.js-anim .spv-header .hero__partner,
.js-anim .sec--hero .hero__tagline-text {
  opacity: 0;
}

.js-anim .sec--hero .hero__tagline-text {
  transform: translateY(100%);
}

@media (prefers-reduced-motion: reduce) {
  .js-anim .sec--hero .hero__tower,
  .js-anim .spv-header .hero__nav-group,
  .js-anim .spv-header .hero__partner,
  .js-anim .sec--hero .hero__tagline-text {
    opacity: 1;
  }
  .js-anim .sec--hero .hero__tagline-text {
    transform: none;
  }
}

/* ==========================================================================
   Mobile
   ========================================================================== */
@media (max-width: 991.98px) {
  .sec--hero {
    height: auto;
    min-height: 100svh;
  }

  /* Nav collapses to a centred row under the logo. */
  /* The inline nav is desktop-only; on mobile the hamburger + offcanvas
     carry the menu, so the row is removed rather than squeezed in. */
  .spv-header .hero__nav {
    display: none;
  }

  .spv-header .hero__nav-link {
    font-size: 13px;
    line-height: 1.6;
  }

  /* Settled mobile position: a header — logo top-left, burger top-right. The
     intro still shows the big logo on the centre line, then flies it here. */
  .spv-header .hero__logo {
    top: 16px;
    left: 16px;
    width: 44px;
    height: 92px;
  }

  .spv-header .hero__logo > img {
    width: 46px;
    height: 94px;
  }

  /* Tucked in beside the burger: 16px gutter + the 48px control + a 12px
     channel. Centred on the burger's own centre line (18 + 24 = 42). */
  .spv-header .hero__partner {
    top: 26px;
    right: 76px;
    width: 110px;
    height: 32px;
  }

  /* The bar in px rather than --u: below 992 the masthead is a real header
     (logo top-left, mark and burger top-right), not a scaled Figma frame.
     Both states centre on the burger's own centre line, 42 from the top. */
  .spv-header {
    height: 112px;
  }

  .spv-header--scrolled {
    height: 88px;
  }

  .spv-header--scrolled .hero__logo {
    top: 4px;
    width: 36px;
    height: 76px;
  }

  .spv-header--scrolled .hero__logo > img {
    width: 38px;
    height: 77px;
  }

  .spv-header--scrolled .hero__partner {
    top: 27px;
    right: 76px;
    width: 103px;
    height: 30px;
  }

  .sec--hero .hero__tagline {
    left: 16px;
    right: 16px;
    top: auto;
    bottom: 96px;
    width: auto;
    height: auto;
  }

  .sec--hero .hero__tagline-text {
    font-size: 24px;
    line-height: 1.25;
    white-space: normal;
    letter-spacing: 0.01em;
    padding-top: 0;
  }

  .sec--hero .hero__foliage {
    top: auto;
    bottom: 0;
    height: 62%;
  }
}


/* ===== section: arrival ===== */
/* ==========================================================================
   Sai Palm View — 02 THE GRAND ARRIVAL
   Figma node 282:1206 (Component 75), page offset y = 1080, box 1920x1080.
   Coordinates below are Figma px relative to the section origin, i.e. the
   full-page y minus 1080, expressed through the --u scaling unit.

   Figma saved this node MID-ANIMATION. The settled master (Figma 249:902
   "Component 11") supplies the on-stage geometry used here:
     photo      x=86   y=80   932x752   (parked off-stage at x=-946)
     copy col   x=1099 y=137  733x558   (parked off-stage at x=1929)
     stats row  x=213  y=909  1493x58   (clipped to h=9px / h=1px)

   Bootstrap now carries the layout: the split is `row`/`col-*`, the stat line
   and each stat are `d-flex`, plates use position/size/object-fit/z utilities.
   What stays here is what Bootstrap has no vocabulary for — the fixed section
   height, the Figma coordinates, the plates' inner geometry and opacities, and
   the commented font-substitution compensations.
   ========================================================================== */

/* Fixed 1080-tall canvas — Bootstrap has no height scale at this size.
   The cream fill comes from Bootstrap's .bg-body. */
.sec--arrival {
  height: calc(1080 * var(--u));
}

/* --------------------------------------------------------------------------
   Background plates — exact Figma boxes and the plates' own inner geometry
   -------------------------------------------------------------------------- */

/* Paper texture: 1920x1782 centred on the 1080 band (top -351).
   .paper-wash supplies the rgba(240,237,233,0.69) overlay on top. */
.sec--arrival .arr__paper {
  top: calc(-351 * var(--u));
  height: calc(1782 * var(--u));
}

/* Figma insets the texture inside its frame by 0.03% / 99.93% and runs it at 70%. */
.sec--arrival .arr__paper-img {
  top: 0.03%;
  height: 99.93%;
  opacity: 0.7;
}

/* Flamingo illustration — 407x611 at (1629, 537), bleeds past the right edge */
.sec--arrival .arr__flamingo {
  left: calc(1629 * var(--u));
  top: calc(537 * var(--u));
  width: calc(407 * var(--u));
  height: calc(611 * var(--u));
  opacity: 0.39;
}

/* Palm-shadow border plate — 1946x832 box, source scaled to 114.64% x 178.93% */
.sec--arrival .arr__palm {
  width: calc(1946 * var(--u));
  height: calc(832 * var(--u));
  opacity: 0.15;
}

.sec--arrival .arr__palm-img {
  width: 114.64%;
  height: 178.93%;
  max-width: none; /* the plate is deliberately larger than its frame */
}

/* --------------------------------------------------------------------------
   The two-column split — Bootstrap row; only the Figma origin and the column
   widths live here. 86 + 932 + 81 (free space) + 733 = 1832 = the copy's right
   edge, so justify-content-between reproduces Figma's 81px channel exactly.
   -------------------------------------------------------------------------- */
.sec--arrival .arr__split {
  left: calc(86 * var(--u));
  top: calc(80 * var(--u));
  width: calc(1746 * var(--u));
}

/* Hero photograph — settled on-stage (Figma parked it at left:-946px) */
.sec--arrival .arr__photo {
  width: calc(932 * var(--u));
  height: calc(752 * var(--u));
  background: #fafafa; /* the Figma rectangle's own fill, behind the photo */
}

/* Figma 282:1138 is a video fill (the tower lighting up). The element fills
   the plate like the image did; Bootstrap's object-fit-cover applies to <video>. */
.sec--arrival .arr__video {
  background: #0b0d12; /* the clip's own dusk sky, so a not-yet-decoded frame is not cream */
}

/* The fully-lit end plate that carries the sweep past the clip's last frame.
   The still was shot from the same camera but a touch wider than the clip, so
   it needs registering before it can be dissolved on top: scaling it 1.075 and
   dropping it 2.254% (16px on the clip's 710px height) lands the tower, the
   podium and both palms on their video positions to within a pixel. Clip, still
   and plate are all 1.2394:1, so object-fit-cover neither crops nor letterboxes
   any of them and the two numbers hold at every viewport width.
   js/main.js drives the opacity; it stays at 0 with no JS, leaving the clip. */
.sec--arrival .arr__lit {
  opacity: 0;
  transform: translateY(2.254%) scale(1.075);
}

/* Copy column — settled on-stage (Figma parked it at left:calc(100% + 9px)).
   Figma y=137 against the photo's y=80, so the column is dropped 57px. */
.sec--arrival .arr__copy {
  width: calc(733 * var(--u));
  height: calc(558 * var(--u));
  margin-top: calc(57 * var(--u));
}

/* Palm crest glyph above the heading — Figma "Object" 57x63 at (0, 0) */
.sec--arrival .arr__crest {
  width: calc(57 * var(--u));
  height: calc(63 * var(--u));
  object-position: bottom;
}

/* "THE GRAND arrival" — Canela Thin 80px + Fleur De Leah 80px.
   Figma text box is 647x127 at (0, 55); the 127 line box comes from the
   script face's metrics, so it is reproduced as an explicit line-height. */
.sec--arrival .arr__title {
  /* Figma y=55. Cormorant Garamond's cap height is shorter than Canela's, so the
     block is raised 10px to put the substitute's baseline on the Figma baseline
     (measured against the settled master 249:902: baseline y=279 section-relative). */
  top: calc(45 * var(--u));
  line-height: calc(127 * var(--u));
  white-space: nowrap;
}

/* Figma sets the inter-word space to 105.986px — off the .fs-d* scale. */
.sec--arrival .arr__title-gap {
  font-size: calc(105.986 * var(--u));
}

/* "Where Grandeur Begins Before You Enter" — 40px italic, box 594x48 at (0, 168) */
.sec--arrival .arr__sub {
  /* Figma y=168, raised 1px for the same baseline compensation as the title
     (master 249:902 puts the "Where" baseline at y=340 section-relative). */
  top: calc(167 * var(--u));
  line-height: calc(48 * var(--u));
  white-space: nowrap;
}

/* Body copy — Figma is 20px / 40px leading in a 733x160 box at (0, 240), i.e.
   exactly four lines. Inter sets ~8.5% wider than Haas Grot Disp Roman, so at a
   literal 20px the paragraph runs to five lines and collides with KNOW MORE at
   y=561. The size is trimmed to 18.6px with -0.3px tracking: that restores the
   Figma line breaks and the 160px block height while keeping the 40px leading
   (.lh-d40). Neither the trimmed size nor the tracking is on a Bootstrap scale. */
.sec--arrival .arr__body {
  top: calc(240 * var(--u));
  width: calc(733 * var(--u));
  font-size: calc(18.6 * var(--u));
  letter-spacing: calc(-0.3 * var(--u));
}

/* KNOW MORE circle-arrow — frame 213x134 at (0, 424) */
.sec--arrival .arr__know-more {
  top: calc(424 * var(--u));
  width: calc(213 * var(--u));
}

.sec--arrival .know-more__circle {
  left: calc(57 * var(--u));
}

/* Figma: 79.158x0 vector at (134, 67) whose artwork overflows by
   -0.63% horizontally and -3.68px vertically -> 80.1581 x 7.36396 at (133.501, 63.32) */
.sec--arrival .know-more__rule {
  left: calc(133.501 * var(--u));
  top: calc(63.32 * var(--u));
  width: calc(80.1581 * var(--u));
  height: calc(7.36396 * var(--u));
}

/* --------------------------------------------------------------------------
   Stat row — settled at y=909 (Figma clipped the three wrappers to 9px / 1px)
   Figma: 1493x58 flex row at x=213, 80px channels, 6px bullet + 12px gap per
   item. The per-item widths sum to 1333, so the 160px of free space inside the
   1493 track falls out as two 80px gaps under justify-content-between.
   -------------------------------------------------------------------------- */
.sec--arrival .arr__stats {
  left: calc(213 * var(--u));
  top: calc(909 * var(--u));
  width: calc(1493 * var(--u));
  height: calc(58 * var(--u));
}

/* 12px bullet-to-text gap — off Bootstrap's rem-based .gap-* scale. */
.sec--arrival .arr__stat {
  height: calc(58 * var(--u));
  gap: calc(12 * var(--u));
}

.sec--arrival .arr__stat--1 { width: calc(414 * var(--u)); }
.sec--arrival .arr__stat--2 { width: calc(477 * var(--u)); }
.sec--arrival .arr__stat--3 { width: calc(442 * var(--u)); }

.sec--arrival .arr__stat-dot {
  width: calc(6 * var(--u));
  height: calc(6 * var(--u));
}

.sec--arrival .arr__stat-text {
  line-height: calc(58 * var(--u));
  white-space: nowrap;
  /* Cormorant Garamond sits lower in the 58px line box than Canela does, which
     dropped the baseline 2px below the master's (y=951). Nudge it back so the
     glyph baseline matches while the 6px bullet stays on its Figma centre. */
  top: calc(-2 * var(--u));
  /* Cormorant Garamond defaults to old-style figures; Canela's are lining. */
  font-variant-numeric: lining-nums;
  font-feature-settings: "lnum" 1;
}

/* --------------------------------------------------------------------------
   Mobile — the fixed 1080 canvas becomes normal flow.
   Bootstrap 5.3 ships no responsive `position-*` variant, so the handful of
   nodes that have to leave the absolute canvas are flipped here; everything
   else reflows through the responsive utilities already on the markup
   (flex-lg-nowrap, col-12 col-lg-auto, flex-column flex-lg-row, …).
   -------------------------------------------------------------------------- */
@media (max-width: 991.98px) {
  .sec--arrival {
    height: auto;
    padding: 48px 24px 56px;
  }

  /* Out of the absolute canvas. The two content blocks stay positioned so their
     .z-3 keeps them painting over the decorative plates. */
  .sec--arrival .arr__split,
  .sec--arrival .arr__stats {
    position: relative !important;
    left: auto;
    top: auto;
    width: auto;
  }

  .sec--arrival .arr__crest,
  .sec--arrival .arr__title,
  .sec--arrival .arr__sub,
  .sec--arrival .arr__body,
  .sec--arrival .arr__stat-text {
    position: static !important;
  }

  /* Back into the flow: the desktop `top` offset must be cleared, since the
     element stays positioned so .know-more's own absolute children still work. */
  .sec--arrival .arr__know-more {
    position: relative !important;
    top: auto;
  }

  /* Decorative plates stay absolute — they are the backdrop. */
  .sec--arrival .arr__paper {
    top: 0;
    height: 100%;
  }

  .sec--arrival .arr__paper-img {
    top: 0;
    height: 100%;
  }

  .sec--arrival .arr__palm {
    width: 200%;
    height: 34%;
  }

  .sec--arrival .arr__flamingo {
    left: auto;
    right: -6%;
    top: auto;
    bottom: 0;
    width: 40%;
    height: auto;
    aspect-ratio: 407 / 611;
  }

  .sec--arrival .arr__flamingo-img {
    position: static !important;
    height: auto !important;
  }

  /* Content: normal flow, stacked */
  .sec--arrival .arr__photo {
    width: 100%;
    height: auto;
    aspect-ratio: 932 / 752;
  }

  .sec--arrival .arr__copy {
    width: auto;
    height: auto;
    margin-top: 28px;
  }

  .sec--arrival .arr__crest {
    width: 42px;
    height: 46px;
    margin-bottom: 10px;
  }

  .sec--arrival .arr__title {
    font-size: 40px;
    line-height: 1.45;
    white-space: normal;
  }

  .sec--arrival .arr__title-gap {
    font-size: 46px;
  }

  .sec--arrival .arr__sub {
    margin-top: 4px;
    font-size: 22px;
    line-height: 1.3;
    white-space: normal;
  }

  .sec--arrival .arr__body {
    width: auto;
    margin-top: 18px;
    font-size: 15px;
    line-height: 28px;
    letter-spacing: normal;
  }

  .sec--arrival .arr__know-more {
    display: block;
    width: 160px;
    height: 100px;
    margin-top: 22px;
  }

  .sec--arrival .know-more__circle {
    left: 42px;
    top: 0;
    width: 96px;
    height: 100px;
  }

  .sec--arrival .know-more__rule {
    left: 100px;
    top: 44px;
    width: 60px;
    height: 6px;
  }

  .sec--arrival .know-more__label {
    top: 38px;
    font-size: 15px;
  }

  .sec--arrival .arr__stats {
    height: auto;
    gap: 14px;
    margin-top: 34px;
  }

  .sec--arrival .arr__stat,
  .sec--arrival .arr__stat--1,
  .sec--arrival .arr__stat--2,
  .sec--arrival .arr__stat--3 {
    width: auto;
    height: auto;
    gap: 10px;
  }

  .sec--arrival .arr__stat-dot {
    width: 6px;
    height: 6px;
  }

  .sec--arrival .arr__stat-text {
    top: auto;
    font-size: 24px;
    line-height: 1.3;
    white-space: normal;
  }
}


/* ===== section: architecture ===== */
/* ==========================================================================
   Sai Palm View — THE ARCHITECTURE
   Figma node 175:3918 ("Component 23"), frame 1920x1384 at page y = 2160.
   All coordinates below are frame-relative (page y minus 2160).

   Bootstrap-first: everything this section can say with a utility is said in
   sections/architecture.php — .layer/.position-absolute/.top-0/.start-0/.w-100/
   .h-100/.overflow-hidden/.object-fit-cover for the backdrop plate, .text-center
   /.text-nowrap for the title band, .ff-display/.ff-sans/.fw-light/.fw-normal/
   .fst-italic/.text-uppercase/.text-capitalize/.text-white/.fs-d180/.fs-d40/
   .lh-d115/.lh-d40/.m-0/.start-0/.z-0/.z-2/.z-3/.bg-body/.text-decoration-none for
   type, colour and layering. What is left here is only what Bootstrap has no
   vocabulary for.
   ========================================================================== */

/* The fixed Figma canvas. Bootstrap has no height scale at this size. */
.sec--architecture {
  height: calc(1384 * var(--u));
}

/* --------------------------------------------------------------------------
   Background fill geometry — Figma 175:3920 / 175:3921: the container is
   1921x1384 at 0,0 but the image fill inside it is over-scaled and pushed left
   (w 110.49%, h 99.98%, left -10.5%, top 0.01%). .object-fit-cover does the
   cropping; these four values do the off-centre framing.
   -------------------------------------------------------------------------- */
.sec--architecture .arch__bg-img {
  left: -10.5%;
  top: 0.01%;
  width: 110.49%;
  height: 99.98%;
  max-width: none;
}

/* The diagonal scrim that darkens the right-hand column the copy sits in. */
.sec--architecture .arch__scrim {
  background-image: linear-gradient(
    259.06deg,
    rgba(0, 0, 0, 0.4) 42.513%,
    rgba(0, 0, 0, 0) 58.989%
  );
}

/* --------------------------------------------------------------------------
   Title band — Figma 175:3923 (instance of Component 50), 1920x266 at 0,0.
   In Figma the band is parked mid-reveal: an h-[4px] overflow-clip wrapper at
   top 262 holds an h-[188px] frame pushed to top -262. Resolved to the settled
   state both children land at their natural y: the 188px title mask at y 0
   (title text inside it at y 12) and the 40px sub-line at y 218. Those two
   exact y offsets are the only thing Bootstrap cannot state.
   -------------------------------------------------------------------------- */
.sec--architecture .arch__titleband {
  height: calc(266 * var(--u));
}

.sec--architecture .arch__title {
  top: calc(12 * var(--u));
}

.sec--architecture .arch__sub {
  top: calc(218 * var(--u));
}

/* --------------------------------------------------------------------------
   Copy block — Figma 175:3924, x 1108 / y 326, 692x360, 20px Haas Grot Disp
   "35 Thin" on 40px leading, capitalize, white.
   Metric note: Inter sets ~14% wider than Haas Grot Display, so at the Figma
   20px the block runs 11 lines / 440px and collides with the KNOW MORE link.
   Compensated to 17.2 on -0.024em tracking, which is the largest setting that
   reproduces Figma's own line breaks word for word (3/3/1/2) and so restores
   the 40px leading, the 692px measure and the exact 360px block height,
   clearing the KNOW MORE circle at y 716.
   NOTE: baseline/architecture.png was captured before this compensation landed,
   so it still shows the overflowing set — paragraph 4 spilling a tenth line
   ("Views.") at y 686-726, straight through the KNOW MORE circle. The v2 render
   deliberately differs there; everything else is pixel-for-pixel the baseline.
   -------------------------------------------------------------------------- */
.sec--architecture .arch__copy {
  left: calc(1108 * var(--u));
  top: calc(326 * var(--u));
  width: calc(692 * var(--u));
  height: calc(360 * var(--u));
  font-size: calc(17.2 * var(--u));
  letter-spacing: -0.024em;
}

/* --------------------------------------------------------------------------
   KNOW MORE circle-arrow — Figma 175:3925, x 1108 / y 716, 209x134.
   Circle group at +53 (128x134), rule at +130 / +67 (80.158 wide, the exported
   arrow leaf overhangs it by -3.68px vertically and -0.63% horizontally),
   label at +0 / +55. Geometry comes from .know-more in theme.css; only the
   frame coordinates and the horizontal offsets are section-specific.
   -------------------------------------------------------------------------- */
.sec--architecture .arch__know-more {
  left: calc(1108 * var(--u));
  top: calc(716 * var(--u));
  width: calc(209 * var(--u));
}

.sec--architecture .arch__know-more .know-more__circle {
  left: calc(53 * var(--u));
}

.sec--architecture .arch__know-more .know-more__rule {
  left: calc(130 * var(--u));
}

.sec--architecture .arch__know-more .know-more__rule img {
  top: calc(-3.68 * var(--u));
  left: -0.63%;
  width: 101.26%;
  height: calc(7.36396 * var(--u));
  max-width: none;
}

/* White focus ring, because the link sits on a night photograph. */
.sec--architecture .arch__know-more:focus-visible {
  outline: max(1px, calc(2 * var(--u))) solid var(--bs-white);
  outline-offset: calc(6 * var(--u));
}

/* --------------------------------------------------------------------------
   Mobile — the fixed-height canvas collapses into normal flow.
   Stock Bootstrap 5.3 ships no responsive `position-*` / `w-*` variants, so the
   utilities applied in the markup have to be unwound with !important here.
   -------------------------------------------------------------------------- */
@media (max-width: 991.98px) {
  .sec--architecture {
    height: auto;
    padding: 56px 0 64px;
  }

  .sec--architecture .arch__bg-img {
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
  }

  /* Desktop leans on the 259deg scrim to darken only the right-hand column the
     copy sits in. Stacked, the copy runs over the lit tower, so the wash goes
     full-width and is heaviest through the middle of the column. */
  .sec--architecture .arch__scrim {
    background-image: linear-gradient(
      180deg,
      rgba(0, 0, 0, 0.45) 0%,
      rgba(0, 0, 0, 0.62) 22%,
      rgba(0, 0, 0, 0.62) 88%,
      rgba(0, 0, 0, 0.42) 100%
    );
  }

  /* The background plate is absolutely positioned, so it paints above any
     static in-flow sibling. Keep the two text layers positioned (relative
     with no offsets — still normal flow) so their z-index still applies. */
  .sec--architecture .arch__titleband,
  .sec--architecture .arch__copy {
    position: relative !important;
    left: auto;
    top: auto;
    width: auto !important;
    height: auto;
  }

  .sec--architecture .arch__title,
  .sec--architecture .arch__sub {
    position: static !important;
    width: auto !important;
    height: auto;
    white-space: normal !important;
  }

  .sec--architecture .arch__titleband {
    padding: 0 24px;
  }

  .sec--architecture .arch__title {
    font-size: 40px;
    line-height: 1.08;
  }

  /* .m-0 in the markup is !important, so the stacked rhythm has to out-shout it. */
  .sec--architecture .arch__sub {
    margin-top: 12px !important;
    font-size: 18px;
    line-height: 1.25;
  }

  .sec--architecture .arch__copy {
    margin-top: 28px;
    padding: 0 24px;
    font-size: 15px;
    line-height: 1.9;
    letter-spacing: 0;
  }

  .sec--architecture .arch__copy p + p {
    margin-top: 14px !important;
  }

  .sec--architecture .arch__know-more {
    position: relative !important;
    left: auto;
    top: auto;
    display: block;
    width: 168px;
    height: 104px;
    margin: 28px auto 0;
  }

  .sec--architecture .arch__know-more .know-more__circle {
    left: 42px;
    top: 0;
    width: 100px;
    height: 104px;
  }

  .sec--architecture .arch__know-more .know-more__rule {
    left: 102px;
    top: 52px;
    width: 62px;
  }

  .sec--architecture .arch__know-more .know-more__rule img {
    top: -2.85px;
    height: 5.7px;
  }

  .sec--architecture .arch__know-more .know-more__label {
    top: 42px;
    font-size: 16px;
  }

  .sec--architecture .arch__know-more:focus-visible {
    outline: 2px solid var(--bs-white);
    outline-offset: 6px;
  }
}


/* ===== section: residences ===== */
/* ==========================================================================
   THE RESIDENCES — horizontal pinned scroll
   Figma node 175:3933 · page y = 3544 · all coords below are Figma-px MINUS 3544
   Viewport 1920x1080 clips a 4507x1080 track. main.js owns the pin/scrub
   (data-timeline="residences-horizontal").

   Bootstrap now carries everything it has vocabulary for: the flex stacks
   (d-flex, flex-column, align-items-center), the plate and photo fills
   (position-absolute, top-0, start-0, w-100, h-100, object-fit-cover),
   clipping (overflow-hidden), pointer-events (pe-none), the cream fill
   (bg-body), the type scale (ff-display, ff-sans, ff-script, fs-dNN, lh-dNN,
   fw-light, fst-italic, text-primary, text-body, text-uppercase,
   text-capitalize) and the gold button (.btn.btn-spv).

   What is left here is what Bootstrap cannot say: exact Figma coordinates and
   box sizes, the fixed section height, the mirrored/translucent palm layer, the
   decorative plates' inner-image geometry, the --u-scaled gaps (Bootstrap's
   gap-* scale is rem-based and does not track --u), and the documented
   font-substitution compensations.

   NOTE ON position: several elements are positioned here rather than with
   .position-absolute because the mobile block has to put them back into normal
   flow, and Bootstrap 5.3 ships no responsive position-* variants (the utility
   is !important, so a media query cannot undo it).
   ========================================================================== */

.sec--residences {
  height: calc(1080 * var(--u));
}

/* --------------------------------------------------------------------------
   The track — 4507px wide, translated on X by the central timeline
   -------------------------------------------------------------------------- */
.sec--residences .res__track {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(4507 * var(--u));
  height: calc(1080 * var(--u));
  overflow: hidden;
  will-change: transform;
}

/* --------------------------------------------------------------------------
   Background plate: stucco texture @70% + paper wash @69% (.paper-wash::after)
   Figma: 4507x1782 box parked at y -273 so its centre sits 78px below the fold
   -------------------------------------------------------------------------- */
.sec--residences .res__bg {
  top: calc(-273 * var(--u));
  width: calc(4507 * var(--u));
  height: calc(1782 * var(--u));
}

/* Figma insets the fill a hair inside its frame and drops it to 70%. */
.sec--residences .res__bg-img {
  top: 0.03%;
  width: 99.98%;
  height: 99.93%;
  max-width: none;
  opacity: 0.7;
}

/* --------------------------------------------------------------------------
   Palm-shadow border — mirrored horizontally, 15%, flush to the track's right
   Figma: 1946x832 at x 2561 (4507 - 1946), inner leaf 114.64% x 178.93%
   -------------------------------------------------------------------------- */
.sec--residences .res__palms {
  left: calc(2561 * var(--u));
  width: calc(1946 * var(--u));
  height: calc(832 * var(--u));
  opacity: 0.15;
  transform: scaleX(-1);
}

/* Over-sized on purpose; max-width has to beat the global img { max-width:100% }. */
.sec--residences .res__palms img {
  width: 114.64%;
  height: 178.93%;
  max-width: none;
}

/* --------------------------------------------------------------------------
   Vertical "palm view" lettermark — 8 letters, 40px leading, 14px word gap
   -------------------------------------------------------------------------- */
.sec--residences .res__mark {
  position: absolute;
  left: calc(1024 * var(--u));
  top: calc(81 * var(--u));
  width: calc(19 * var(--u));
  height: calc(334 * var(--u));
  gap: calc(14 * var(--u));
}

/* --------------------------------------------------------------------------
   Intro panel — Figma frame 140,185 · 834x710
   (Figma stored it clipped to w-[5px] + opacity-0; rendered settled here)
   -------------------------------------------------------------------------- */
.sec--residences .res__intro {
  position: absolute;
  left: calc(140 * var(--u));
  top: calc(185 * var(--u));
  width: calc(834 * var(--u));
  height: calc(710 * var(--u));
}

/* Gold palm ornament above the heading — 57x63, bottom-anchored */
.sec--residences .res__leaf {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(57 * var(--u));
  height: calc(63 * var(--u));
  object-position: bottom;
}

.sec--residences .res__intro-body {
  position: absolute;
  left: 0;
  top: calc(87 * var(--u));
  width: calc(834 * var(--u));
  gap: calc(24 * var(--u));
}

.sec--residences .res__intro-head {
  gap: calc(12 * var(--u));
}

/* Figma 175:3162 is a fixed-height text node (834x85) — shorter than Canela's
   own 96px normal leading at 80px, so the designer squeezed it. Pinning the box
   height keeps the subtitle + copy below on their Figma offsets even though the
   Cormorant/Fleur De Leah line box is taller. Head group = 85+12+48 = 145. */
.sec--residences .res__title {
  height: calc(85 * var(--u));
}

/* Figma 175:3163 is 834x48 — Canela's normal leading is 1.2, not Cormorant's.
   Bootstrap's .lh-base is hard-coded to 1.5, so the Figma value is written out. */
.sec--residences .res__sub {
  line-height: calc(48 * var(--u));
}

/* VIEW FLOOR PLANS circle-arrow — .know-more atom, placed at panel-local 0,576 */
.sec--residences .res__cta {
  position: absolute;
  left: 0;
  top: calc(576 * var(--u));
  width: calc(265.158 * var(--u));
}

.sec--residences .res__cta .know-more__circle {
  left: calc(109 * var(--u));
}

.sec--residences .res__cta .know-more__rule {
  left: calc(186 * var(--u));
  top: calc(67 * var(--u));
  width: calc(79.158 * var(--u));
  height: 0;
}

/* Figma renders the 0-height vector with a -3.68px / -0.63% bleed */
.sec--residences .res__cta .know-more__rule img {
  left: calc(-0.5 * var(--u));
  top: calc(-3.68 * var(--u));
  width: calc(80.1581 * var(--u));
  height: calc(7.36396 * var(--u));
  max-width: none;
}

/* --------------------------------------------------------------------------
   Residence cards — staggered photo / caption / button trios.
   The three children each sit on their own track-absolute Figma coordinate, so
   .res__card itself stays a zero-height flow box on desktop.
   -------------------------------------------------------------------------- */
/* The card art is a brochure floor plan — portrait artwork in a landscape box,
   contained rather than cropped, so the box carries the plan's own near-white
   sheet colour for the letterboxing to fall back to. */
.sec--residences .res__photo {
  position: absolute;
  width: calc(899 * var(--u));
  height: calc(745 * var(--u));
  background: #fdfdfd;
  /* The plan is held back behind the card's VIEW FLOOR PLAN button: readable
     as a layout, not as dimensions. One knob so the mobile block can re-pin it
     — --u is viewport-relative but the mobile card is not, so a --u-scaled
     radius would thin out to nothing on a phone. */
  --res-plan-blur: calc(6 * var(--u));
}

.sec--residences .res__photo > img {
  filter: blur(var(--res-plan-blur));
}

.sec--residences .res__caption {
  position: absolute;
  gap: calc(8.76 * var(--u));
}

/* Figma 175:3151 is 200x96 and 175:3152 is 200x35 — Canela normal leading 1.2.
   Caption group therefore measures 96 + 8.76 + 35.04 = 140, as in Figma. */
.sec--residences .res__card-title {
  line-height: calc(96 * var(--u));
}

.sec--residences .res__card-sub {
  line-height: calc(35.04 * var(--u));
}

.sec--residences .res__btn {
  position: absolute;
}

/* 1 — 4 BHK · Oceanic Sky Suites */
.sec--residences .res__card--1 .res__caption {
  left: calc(1202 * var(--u));
  top: calc(115 * var(--u));
  width: calc(235 * var(--u));
}

.sec--residences .res__card--1 .res__photo {
  left: calc(1202 * var(--u));
  top: calc(275 * var(--u));
}

.sec--residences .res__card--1 .res__btn {
  left: calc(1870 * var(--u));
  top: calc(182 * var(--u));
}

/* 2 — 6 BHK · Jodi Residences */
.sec--residences .res__card--2 .res__caption {
  left: calc(2241 * var(--u));
  top: calc(825 * var(--u));
  width: calc(200 * var(--u));
}

.sec--residences .res__card--2 .res__photo {
  left: calc(2241 * var(--u));
  top: calc(60 * var(--u));
}

.sec--residences .res__card--2 .res__btn {
  left: calc(2909 * var(--u));
  top: calc(825 * var(--u));
}

/* 3 — 7 BHK · Jodi Residences */
.sec--residences .res__card--3 .res__caption {
  left: calc(3280 * var(--u));
  top: calc(115 * var(--u));
  width: calc(200 * var(--u));
}

.sec--residences .res__card--3 .res__photo {
  left: calc(3280 * var(--u));
  top: calc(275 * var(--u));
}

.sec--residences .res__card--3 .res__btn {
  left: calc(3834 * var(--u));
  top: calc(191 * var(--u));
}


/* ==========================================================================
   Mobile — the track stops being a canvas and reflows as a stack.
   Bootstrap's responsive variants carry the flow changes it can express
   (flex-row/flex-lg-column, justify-content-center/-lg-start,
   object-fit-cover/-lg-fill); what is left is geometry.
   NOTE: the per-card desktop rules are 0,3,0, so every neutralising override
   below is written at that same specificity (.res__card .res__x).
   ========================================================================== */
@media (max-width: 991.98px) {
  .sec--residences {
    height: auto;
    padding: 56px 0 64px;
  }

  .sec--residences .res__track {
    position: static;
    width: auto;
    height: auto;
    overflow: visible;
    will-change: auto;
  }

  /* Decorative layers stay behind the flow content */
  .sec--residences .res__bg {
    top: 0;
    width: 100%;
    height: 100%;
    z-index: 0;
  }

  .sec--residences .res__bg-img {
    top: 0;
    width: 100%;
    height: 100%;
  }

  .sec--residences .res__palms {
    left: 0;
    width: 100%;
    height: 260px;
    z-index: 0;
  }

  .sec--residences .res__palms img {
    width: 170%;
    height: 100%;
  }

  /* Lettermark becomes a single horizontal rubric */
  .sec--residences .res__mark {
    position: relative;
    z-index: 1;
    left: auto;
    top: auto;
    gap: 14px;
    width: auto;
    height: auto;
    font-size: 13px;
    line-height: 1.4;
    letter-spacing: 0.34em;
    margin: 0 0 32px;
  }

  /* Bootstrap's width utilities have no responsive variants, so .w-100 on the
     letter cells has to be beaten on its own terms. */
  .sec--residences .res__mark-word > span {
    width: auto !important;
  }

  /* Intro panel */
  .sec--residences .res__intro {
    position: relative;
    z-index: 1;
    left: auto;
    top: auto;
    width: auto;
    height: auto;
    padding: 0 24px;
    margin-bottom: 48px;
  }

  .sec--residences .res__leaf {
    position: static;
    width: 57px;
    height: 63px;
    margin-bottom: 18px;
  }

  .sec--residences .res__intro-body {
    position: static;
    width: auto;
    gap: 20px;
  }

  .sec--residences .res__intro-head {
    gap: 10px;
  }

  .sec--residences .res__title {
    font-size: 40px;
    height: auto;
  }

  .sec--residences .res__sub {
    font-size: 21px;
    line-height: 1.2;
  }

  .sec--residences .res__copy {
    font-size: 15px;
    line-height: 28px;
  }

  /* Stays relative so the .know-more children keep anchoring to it */
  .sec--residences .res__cta {
    position: relative;
    left: auto;
    top: auto;
    display: block;
    width: 266px;
    height: 134px;
    margin-top: 22px;
  }

  .sec--residences .res__cta .know-more__circle {
    left: 109px;
    top: 0;
    width: 128px;
    height: 134px;
  }

  .sec--residences .res__cta .know-more__rule {
    left: 186px;
    top: 67px;
    width: 79px;
  }

  .sec--residences .res__cta .know-more__rule img {
    left: -0.5px;
    top: -3.7px;
    width: 80px;
    height: 7.4px;
  }

  .sec--residences .res__cta .know-more__label {
    left: 0;
    top: 55px;
    font-size: 17px;
  }

  /* Cards: caption → photo → button, in normal flow */
  .sec--residences .res__card {
    position: relative;
    z-index: 1;
    padding: 0 24px;
    margin-bottom: 44px;
  }

  .sec--residences .res__card:last-child {
    margin-bottom: 0;
  }

  .sec--residences .res__card .res__caption {
    position: static;
    left: auto;
    top: auto;
    width: auto;
    gap: 6px;
    margin-bottom: 18px;
  }

  .sec--residences .res__card-title {
    font-size: 40px;
    line-height: 1.2;
  }

  .sec--residences .res__card-sub {
    font-size: 20px;
    line-height: 1.2;
  }

  .sec--residences .res__card .res__photo {
    position: relative;
    left: auto;
    top: auto;
    width: 100%;
    height: auto;
    aspect-ratio: 899 / 745;
    margin-bottom: 20px;
    /* ~1/3 of the desktop 6px, matching the card's smaller rendered width so
       the plan is obscured to the same degree rather than more or less. */
    --res-plan-blur: 3px;
  }

  /* .btn-spv's min-width/min-height are --u-scaled; the mobile block pins the
     real box so the label keeps its 231x56 Figma proportions. */
  .sec--residences .res__card .res__btn {
    position: static;
    left: auto;
    top: auto;
    width: 231px;
    height: 56px;
    font-size: 15px;
    padding: 12px 20px;
    border-width: 2px;
  }
}


/* ===== section: amenities ===== */
/* ==========================================================================
   Sai Palm View — AMENITIES  (Figma node 175:3934 "Component 26")
   Frame 1920x3753, page offset y=4624. All child coordinates below are
   frame-relative, exactly as Figma reports them.

   Layout lives in Bootstrap classes on sections/amenities.php. What is left
   here is only what Bootstrap has no vocabulary for: the fixed frame height,
   exact Figma coordinates and box sizes, the scrim gradient, the two
   non-standard opacities, the cropped fill geometry, and the documented
   font-substitution compensations.
   ========================================================================== */

.sec--amenities {
  height: calc(3753 * var(--u));
}

/* --------------------------------------------------------------------------
   Background plate — node 175:3935, 1920x2114 at (0, 1639)
   .paper-wash (theme.css) paints the rgba(240,237,233,.69) wash on top.
   position/left/width + the cover fill are Bootstrap; only the y offset, the
   height and Figma's 70% fill opacity (not on Bootstrap's 0/25/50/75/100
   opacity scale) need saying here.
   -------------------------------------------------------------------------- */
.sec--amenities .amen__plate {
  top: calc(1639 * var(--u));
  height: calc(2114 * var(--u));
}

.sec--amenities .amen__plate img {
  opacity: 0.7;
}

/* --------------------------------------------------------------------------
   Palm-frond watermark — node 175:3936, 974x1073 at (473, 2159), opacity 7%
   -------------------------------------------------------------------------- */
.sec--amenities .amen__watermark {
  left: calc(473 * var(--u));
  top: calc(2159 * var(--u));
  width: calc(974 * var(--u));
  height: calc(1073 * var(--u));
  opacity: 0.07;
}

/* Figma anchors the frond to the bottom of its box; Bootstrap ships
   object-fit but no object-position utilities. */
.sec--amenities .amen__watermark img {
  object-position: bottom;
}

/* --------------------------------------------------------------------------
   Lagoon hero — node 282:1131, 1920x1639 at (0, 0)
   -------------------------------------------------------------------------- */

/* position is written out rather than using .position-absolute because that
   utility is !important and has no responsive variant — the mobile block
   below needs the hero back in flow as position:relative. */
.sec--amenities .amen__hero {
  position: absolute;
  height: calc(1639 * var(--u));
}

/* Figma's inner fill is h-99.95%, not 100%. */
.sec--amenities .amen__hero img {
  height: 99.95%;
}

/* The exact Figma gradient scrim. */
.sec--amenities .amen__hero::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    180deg,
    rgba(0, 0, 0, 0) 27.153%,
    rgba(0, 0, 0, 0.3) 38.073%,
    rgba(0, 0, 0, 0.36) 53.888%,
    rgba(0, 0, 0, 0.6) 77.046%
  );
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   Title band — instance 175:3946 (Component 51), 1920x962 at (0, 478)

   Figma saved this mid-reveal: a 952x16 clip wrapper pinned to the bottom of
   the 962px component (top 946) with the real 952x620 stack inside it at
   top:-604 and the whole thing at opacity 0. Content top = clipH - contentH
   => -604 = 16 - 620, i.e. the stack never moves, only the clip window grows
   from 16px to 620px against its bottom edge. Settled box is therefore
   952x620 at (484, 342) inside the component = (484, 820) in the section,
   bottom edge at y=1440. Anchored by `bottom` so substitute-font drift grows
   the stack upward into the hero instead of down into the photography.

   The column, the centring and the full-width children are Bootstrap
   (d-flex flex-column align-items-center w-100); the gaps are Figma lengths,
   which Bootstrap's rem-based .gap-* scale cannot express.
   -------------------------------------------------------------------------- */
.sec--amenities .amen__band {
  left: calc(484 * var(--u));
  bottom: calc(2313 * var(--u)); /* 3753 - 1440 */
  width: calc(952 * var(--u));
  gap: calc(40 * var(--u));
}

.sec--amenities .amen__band-head,
.sec--amenities .amen__band-text {
  gap: calc(24 * var(--u));
}

/* Leaf mark — node 175:2787, 57x63 */
.sec--amenities .amen__leaf {
  width: calc(57 * var(--u));
  height: calc(63 * var(--u));
}

.sec--amenities .amen__leaf img {
  object-position: bottom;
}

/* "THE landscape" — node 175:2790. The 80px size, white, centring and casing
   are utilities; the height is pinned because the Fleur De Leah span grows the
   line box to ~92px, while Figma's frame 175:2789 is exactly 85 tall — the
   stack below it must not inherit that 7px of substitute-font overshoot. */
.sec--amenities .amen__title {
  height: calc(85 * var(--u));
  line-height: calc(85 * var(--u));
}

/* Body copy — node 175:2791, Haas 35 Thin 24/50. Only the 50px leading is
   left: theme.css ships .lh-d40/36/30/115, not a 50 step. */
.sec--amenities .amen__copy {
  line-height: calc(50 * var(--u));
}

/* VIEW MORE — node 175:2792, 201x134 group on the shared .know-more atom.
   Everything else (white label, left:0 label, 100% imagery) is utilities. */
.sec--amenities .amen__more {
  width: calc(201 * var(--u));
}

.sec--amenities .know-more__circle {
  left: calc(45 * var(--u));
}

/* Figma: h-0 rule box at (122, 67) with the arrow art bleeding -3.68px/-0.63%.
   theme.css already carries that box's top/width/height; only x differs here. */
.sec--amenities .know-more__rule {
  left: calc(121.501 * var(--u));
}

/* --------------------------------------------------------------------------
   Editorial photography — three photo+caption stacks.

   The stacks overlap and stagger across the frame, so each pair is parked at
   its Figma coordinate; inside the pair the photo and its caption are a plain
   Bootstrap column (d-flex flex-column + align-items-lg-start/end), so only
   the anchor point, the box width and the Figma gap live here.
   position is written out for the same reason as the hero: the mobile block
   returns these to normal flow.
   -------------------------------------------------------------------------- */
.sec--amenities .amen__pair {
  position: absolute;
}

/* nodes 175:3940 + 175:3941 — photo 1741x732 at (89, 1512), caption 24 below */
.sec--amenities .amen__pair--zen {
  left: calc(89 * var(--u));
  top: calc(1512 * var(--u));
  width: calc(1741 * var(--u));
  gap: calc(24 * var(--u));
}

/* nodes 175:3942 + 175:3943 — photo 836x651 at (89, 2418), caption 30 below */
.sec--amenities .amen__pair--bonfire {
  left: calc(89 * var(--u));
  top: calc(2418 * var(--u));
  width: calc(836 * var(--u));
  gap: calc(30 * var(--u));
}

/* nodes 175:3944 + 175:3945 — caption 739 wide at (1091, 3491); the 704-wide
   photo hangs off the stack's right edge at (1126, 2632), 30 above it */
.sec--amenities .amen__pair--deck {
  left: calc(1091 * var(--u));
  top: calc(2632 * var(--u));
  width: calc(739 * var(--u));
  gap: calc(30 * var(--u));
}

/* Photo boxes — node 175:3940 is two stacked cover fills, the other two one. */
.sec--amenities .amen__shot--zen {
  width: calc(1741 * var(--u));
  height: calc(732 * var(--u));
}

.sec--amenities .amen__shot--bonfire {
  width: calc(836 * var(--u));
  height: calc(651 * var(--u));
}

.sec--amenities .amen__shot--deck {
  width: calc(704 * var(--u));
  height: calc(829 * var(--u));
}

/* The deck fill is cropped hard left in Figma — 209.34% wide, pulled back
   73.28%, stretched to fill. Bootstrap's sizing utilities are !important, so
   this one image cannot wear them. */
.sec--amenities .amen__shot--deck img {
  position: absolute;
  left: -73.28%;
  top: 0.1%;
  width: 209.34%;
  height: 100%;
  max-width: none;
  object-fit: fill;
}

/* --------------------------------------------------------------------------
   Gold captions — Canela Thin uppercase + Fleur De Leah lowercase script.
   Casing, colour, family, 80px size and alignment are utilities; the Figma box
   and its exact leading are not. white-space and the heading margin reset stay
   here rather than as .text-nowrap / .m-0 because both utilities are
   !important with no responsive variant, and the mobile block below has to
   re-wrap the caption and give it its 24/30px gutter.
   -------------------------------------------------------------------------- */
.sec--amenities .amen__cap {
  margin: 0;
  white-space: nowrap;
}

/* node 175:3941 — 834x110, right aligned to the 1830 margin */
.sec--amenities .amen__cap--zen {
  width: calc(834 * var(--u));
  height: calc(110 * var(--u));
  line-height: calc(110 * var(--u));
}

/* node 175:3943 — 834x110, left aligned to the 89 margin */
.sec--amenities .amen__cap--bonfire {
  width: calc(834 * var(--u));
  height: calc(110 * var(--u));
  line-height: calc(110 * var(--u));
}

/* node 175:3945 — 739x184, right aligned, 2 lines at 84.49% leading */
.sec--amenities .amen__cap--deck {
  width: calc(739 * var(--u));
  height: calc(184 * var(--u));
  line-height: calc(92 * var(--u));
}

/* --------------------------------------------------------------------------
   Mobile — static flow. The band keeps riding the hero scrim because the
   leaf, the type and the arrow art are all white.
   -------------------------------------------------------------------------- */
@media (max-width: 991.98px) {
  .sec--amenities {
    --amen-hero-h: max(460px, 125vw);
    height: auto;
    padding: 0 0 48px;
  }

  .sec--amenities .amen__plate {
    top: 0;
    height: 100%;
  }

  .sec--amenities .amen__hero {
    position: relative;
    height: var(--amen-hero-h);
    margin: 0 0 28px;
  }

  .sec--amenities .amen__band {
    left: 0;
    right: 0;
    bottom: auto;
    top: 0;
    width: 100%;
    height: var(--amen-hero-h);
    padding: 0 24px 28px;
    justify-content: flex-end;
    gap: 18px;
  }

  .sec--amenities .amen__band-head,
  .sec--amenities .amen__band-text {
    gap: 12px;
  }

  .sec--amenities .amen__leaf {
    width: 28px;
    height: 31px;
  }

  .sec--amenities .amen__title {
    /* the desktop box is pinned to Figma's 85px frame; mobile hugs its line */
    height: auto;
    font-size: 34px;
    line-height: 1.15;
  }

  .sec--amenities .amen__copy {
    font-size: 12px;
    line-height: 19px;
  }

  .sec--amenities .amen__copy-accent {
    font-size: 15px;
  }

  .sec--amenities .amen__more {
    width: 110px;
    height: 74px;
  }

  .sec--amenities .know-more__circle {
    left: 24px;
    width: 70px;
    height: 74px;
  }

  .sec--amenities .know-more__rule {
    left: 66px;
    top: 33px;
    width: 44px;
    height: 4px;
  }

  .sec--amenities .know-more__label {
    top: 28px;
    font-size: 12px;
  }

  /* The pairs drop out of the composition and back into the page flow.
     relative, not static: the paper plate is absolutely positioned and would
     otherwise paint its 69% wash over the captions. */
  .sec--amenities .amen__pair {
    position: relative;
    left: auto;
    top: auto;
    width: auto;
    gap: 0;
  }

  /* relative, not static: the fills stay absolute inside their own box */
  .sec--amenities .amen__shot {
    width: calc(100% - 48px);
    height: auto;
    margin: 0 24px 18px;
    aspect-ratio: 1741 / 732;
  }

  .sec--amenities .amen__shot--bonfire {
    aspect-ratio: 836 / 651;
  }

  .sec--amenities .amen__shot--deck {
    aspect-ratio: 704 / 829;
  }

  .sec--amenities .amen__shot--deck img {
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .sec--amenities .amen__cap {
    width: auto;
    height: auto;
    margin: 0 24px 30px;
    font-size: 30px;
    line-height: 1.2;
    white-space: normal;
  }
}


/* ===== section: experience ===== */
/* ==========================================================================
   Sai Palm View — "Grand Amenities" (Figma: Component 45 / variant Component 44)
   Figma node 175:3948, page offset y = 8377, design box 1920 x 1080.
   All coordinates below are section-local (page y already reduced by 8377).

   Layout lives in sections/experience.php as Bootstrap utilities. What is left
   here is only what Bootstrap has no vocabulary for: exact Figma coordinates,
   the fixed section height, rotations, the photo crop, the vignette gradient,
   the decorative plates' inner geometry and the frosted discs.
   ========================================================================== */

.sec--experience {
  height: calc(1080 * var(--u));
}

/* Figma renders every image at its own explicit box; theme.css clamps images to
   their container, which would crush the over-scanned texture / photo plates. */
.sec--experience img {
  max-width: none;
}

/* --------------------------------------------------------------------------
   1. Cream stucco plate + paper wash  (Figma "Background" 175:1997)
   Box: 1920 x 1782 at (0, -508.5). Texture fill at 70%, then the 69% paper wash
   (theme.css .paper-wash). Fully covered by the photograph in the settled
   state — it is the surface the expanding photo mask wipes away.
   -------------------------------------------------------------------------- */
.sec--experience .exp__paper {
  top: calc(-508.5 * var(--u));
  width: calc(1920 * var(--u));
  height: calc(1782 * var(--u));
}

/* Figma's texture fill is laid in at 70%; Bootstrap's opacity scale steps
   0 / 25 / 50 / 75 / 100, so the exact value stays here. */
.sec--experience .exp__paper-img-wrap {
  opacity: 0.7;
}

/* The 4506 x 1781 texture is over-scanned inside the 1920-wide plate. */
.sec--experience .exp__paper-img {
  top: calc(0.535 * var(--u));
  width: calc(4506.24 * var(--u));
  height: calc(1780.75 * var(--u));
}

/* --------------------------------------------------------------------------
   2. Photograph behind an expanding rounded mask (Figma 175:1932 / 175:1933)
   Mask: 2438 x 2148 at (-282, -477), radius 1500 — the settled, fully open
   state of the 41 x 40 circle that sits at (1024, 1040) mid-animation.
   -------------------------------------------------------------------------- */
.sec--experience .exp__mask {
  left: calc(-282 * var(--u));
  top: calc(-477 * var(--u));
  width: calc(2438 * var(--u));
  height: calc(2148 * var(--u));
  border-radius: calc(1500 * var(--u));
}

/* Closed state for the circular reveal. Figma's 41x40 circle at (1024,1040) in
   section coordinates is (1326,1537) inside this mask box. main.js scrubs it
   open; with JS off or reduced motion the photograph is simply already there.

   DESKTOP ONLY. The timeline that opens this is guarded by isDesktop(), so
   applying the closed state at every width left the backdrop clipped to a ~15px
   circle off the side of a phone screen — the section rendered as a flat dark
   ground with no photograph at all. */
@media (min-width: 992px) {
  .js-anim .sec--experience .exp__mask {
    clip-path: circle(calc(20 * var(--u)) at calc(1326 * var(--u)) calc(1537 * var(--u)));
  }
}

@media (prefers-reduced-motion: reduce) {
  .js-anim .sec--experience .exp__mask { clip-path: none; }
}

.sec--experience .exp__photo {
  left: calc(279 * var(--u));
  top: calc(477 * var(--u));
  width: calc(1923 * var(--u));
  height: calc(1421 * var(--u));
}

/* Figma crops the 2033 x 1143 source to 76.12% of its width from x = 11.98%. */
.sec--experience .exp__photo-img {
  left: calc(-302.68 * var(--u));
  width: calc(2526.24 * var(--u));
  height: calc(1421 * var(--u));
  object-fit: cover;
  /* Stacked scenes: only the active one shows. The incoming scene fades up over
     the outgoing one, which stays put underneath until the fade completes. */
  opacity: 0;
  transition: opacity 0.9s ease;
  will-change: opacity;
}

.sec--experience .exp__photo-img.is-active {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .sec--experience .exp__photo-img { transition: none; }
}

/* Radial vignette: ellipse with semi-axes 981.8 x 725.5 on the 1923 x 1421
   plate (= 51.06% of each side), black 10% -> 100%, layer opacity 0.6. */
.sec--experience .exp__photo-vignette {
  background: radial-gradient(
    ellipse 51.06% 51.06% at 50% 50%,
    rgba(0, 0, 0, 0.06) 0%,
    rgba(0, 0, 0, 0.188) 15.54%,
    rgba(0, 0, 0, 0.32) 29.84%,
    rgba(0, 0, 0, 0.468) 50.98%,
    rgba(0, 0, 0, 0.504) 60.94%,
    rgba(0, 0, 0, 0.562) 71.51%,
    rgba(0, 0, 0, 0.6) 91.49%
  );
}

/* --------------------------------------------------------------------------
   3. Painted cloud plates (Figma "image 22 / 23 / 24", 175:1939-1941)
   Parked above the frame in all four Figma variants — that IS their settled
   position; the section transition drifts them down through the viewport.
   Each plate is a rotated artwork inside an unrotated placement box, so the
   box and the rotation have to be given separately.
   -------------------------------------------------------------------------- */
.sec--experience .exp__cloud--a {
  left: calc(1065 * var(--u));
  top: calc(-726 * var(--u));
  width: calc(995.625 * var(--u));
  height: calc(767.681 * var(--u));
}

.sec--experience .exp__cloud--a .exp__cloud-rot {
  transform: rotate(-19.07deg);
}

.sec--experience .exp__cloud--a .exp__cloud-img,
.sec--experience .exp__cloud--b .exp__cloud-img {
  width: calc(877.508 * var(--u));
  height: calc(508.955 * var(--u));
}

.sec--experience .exp__cloud--b {
  left: calc(-188 * var(--u));
  top: calc(-726 * var(--u));
  width: calc(979.316 * var(--u));
  height: calc(718.677 * var(--u));
}

.sec--experience .exp__cloud--b .exp__cloud-rot {
  transform: rotate(15deg);
}

.sec--experience .exp__cloud--c {
  left: calc(512.96 * var(--u));
  top: calc(-603.51 * var(--u));
  width: calc(979.05 * var(--u));
  height: calc(645.421 * var(--u));
}

.sec--experience .exp__cloud--c .exp__cloud-rot {
  transform: rotate(172.69deg) scaleY(-1);
}

.sec--experience .exp__cloud--c .exp__cloud-img {
  width: calc(918.717 * var(--u));
  height: calc(532.856 * var(--u));
}

/* --------------------------------------------------------------------------
   4. Heading block (Figma 175:1934 window + 175:1935 copy + 175:1938 ornament)
   Window 834 x 230 at (543, 21); copy at (0, 85) inside it; palm ornament at
   (388, 7). In the captured frame the window is collapsed to 3px at y = 248 —
   the copy never moves, only the window's top edge travels 248 -> 21.

   position/overflow are written out rather than taken from .position-absolute /
   .overflow-hidden because those utilities are !important and have no
   breakpoint-scoped variant, while the mobile block below returns this window
   to normal flow.
   -------------------------------------------------------------------------- */
.sec--experience .exp__head {
  position: absolute;
  overflow: hidden;
  left: calc(543 * var(--u));
  top: calc(21 * var(--u));
  width: calc(834 * var(--u));
  height: calc(230 * var(--u));
}

.sec--experience .exp__ornament {
  position: absolute;
  left: calc(388 * var(--u));
  top: calc(7 * var(--u));
  width: calc(57 * var(--u));
  height: calc(63 * var(--u));
}

/* Stretched to the window with .start-0/.end-0; only the y offset and the
   12px Figma gap (off Bootstrap's rem-based .gap-* scale) are left. */
.sec--experience .exp__head-copy {
  position: absolute;
  top: calc(85 * var(--u));
  gap: calc(12 * var(--u));
}

/* "GRAND" Canela Thin uppercase + "amenities" Fleur De Leah, both 80px, white
   on the photograph (gold only in the pre-reveal cream variant). Figma's 85px
   line box is also pinned as a height so the substituted script face, whose
   descenders run deeper than Fleur De Leah's metrics in Figma, cannot push the
   subtitle down. */
.sec--experience .exp__title {
  line-height: calc(85 * var(--u));
  height: calc(85 * var(--u));
}

.sec--experience .exp__sub {
  line-height: calc(48 * var(--u));
}

/* --------------------------------------------------------------------------
   5. Amenity index (Figma 175:1942 window, group 175:1943)
   Window 508 x 718 at (1272, 281); group offset (0, 49). In the captured frame
   the window is collapsed to 79px at y = 920 — same bottom edge (999), so the
   reveal is a bottom-anchored wipe, the content itself never moves.
   As with the heading, position/overflow stay explicit because the mobile block
   un-positions this window and Bootstrap's utilities are !important.
   -------------------------------------------------------------------------- */
.sec--experience .exp__list {
  position: absolute;
  overflow: hidden;
  left: calc(1272 * var(--u));
  top: calc(281 * var(--u));
  width: calc(508 * var(--u));
  height: calc(718 * var(--u));
}

.sec--experience .exp__list-inner {
  position: absolute;
  inset: 0;
}

/* Full track: 669 tall hairline, white 40%, 2px stroke. */
.sec--experience .exp__track {
  left: calc(506 * var(--u));
  top: calc(48 * var(--u));
  width: calc(2 * var(--u));
  height: calc(671.007 * var(--u));
}

/* Progress thumb: 95 tall, solid white, 5px round-capped stroke. */
.sec--experience .exp__thumb {
  left: calc(504.5 * var(--u));
  top: calc(46.5 * var(--u));
  width: calc(5 * var(--u));
  height: calc(100 * var(--u));
}

/* Figma puts the five rows at y = 93, 175, 257, 339, 421 inside the window:
   a 48px row on an 82px rhythm, i.e. a 34px gap under a 93px lead-in. Neither
   value is on Bootstrap's rem-based spacing scale, so the flex column set in
   the markup is fed its exact Figma metrics here. */
.sec--experience .exp__items {
  position: absolute;
  inset: 0;
  padding-top: calc(93 * var(--u));
  gap: calc(34 * var(--u));
}

/* The resting row is at 50% and the active/hover row cross-fades to full.
   Bootstrap's .opacity-50 is !important, so it could not be lifted by the
   state selectors below. */
.sec--experience .exp__item {
  height: calc(48 * var(--u));
  opacity: 0.5;
  transition: opacity 0.45s ease;
}

.sec--experience .exp__item.is-active,
.sec--experience .exp__item:hover,
.sec--experience .exp__item.is-active {
  opacity: 1;
}

/* The progress thumb slides down the rail as the selection moves. */
.sec--experience .exp__thumb {
  transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
}

/* Canela Deck Trial Regular Italic 40 / 48, Title Case, white — everything but
   the 48px leading and the no-wrap rule comes from Bootstrap utilities.
   .text-nowrap is not used because it is !important and the mobile block below
   has to let these rows wrap. */
.sec--experience .exp__item-btn {
  line-height: calc(48 * var(--u));
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   6. Prev / next controls (Figma 175:1951) — row at (140, 811), gap 19,
   two 119 x 121 discs, white 20% fill + #CFA854 hairline ring.
   -------------------------------------------------------------------------- */
.sec--experience .exp__nav {
  position: absolute;
  left: calc(140 * var(--u));
  top: calc(811 * var(--u));
  gap: calc(19 * var(--u));
}

.sec--experience .exp__nav-btn {
  width: calc(119 * var(--u));
  height: calc(121 * var(--u));
  transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Figma applies a background blur to each disc; the exported SVG can only carry
   the 20% white fill, the gold ring and the arrow, so the frost lives here.
   The radius is Figma's 59.5 on both axes, not .rounded-circle's 50%, which on
   a 119 x 121 box would draw an ellipse. */
.sec--experience .exp__nav-frost {
  border-radius: calc(59.5 * var(--u));
  -webkit-backdrop-filter: blur(calc(7 * var(--u)));
  backdrop-filter: blur(calc(7 * var(--u)));
}

.sec--experience .exp__nav-btn img {
  width: calc(119 * var(--u));
  height: calc(121 * var(--u));
}

.sec--experience .exp__nav-btn--prev:hover {
  transform: translateX(calc(-8 * var(--u)));
}

.sec--experience .exp__nav-btn--next:hover {
  transform: translateX(calc(8 * var(--u)));
}

/* --------------------------------------------------------------------------
   7. Mobile — static flow, same copy, same assets.
   The paper plate and the clouds are dropped by .d-none/.d-lg-* in the markup.
   -------------------------------------------------------------------------- */
@media (max-width: 991.98px) {
  .sec--experience {
    /* .bg-body reads this token, so the dark mobile ground is a variable swap
       rather than a fight with the utility's !important. #17140F. */
    --bs-body-bg-rgb: 23, 20, 15;
    height: auto;
    padding: 56px 24px 64px;
  }

  .sec--experience .exp__mask {
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    border-radius: 0;
  }

  .sec--experience .exp__photo {
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
  }

  .sec--experience .exp__photo-img {
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  /* The desktop ellipse vignette does not cover a tall portrait crop, so the
     white copy needs a straight top-to-bottom scrim instead. */
  /* The desktop ellipse vignette does not cover a tall portrait crop, so the
     white copy needs a straight top-to-bottom scrim instead. Held at 0.62
     through the band the amenity list occupies: measured against the pool
     render, the lighter 0.45 there left blown-out highlights (luminance 1.0)
     directly behind the first two rows, which no text opacity can survive. */
  .sec--experience .exp__photo-vignette {
    background: linear-gradient(
      180deg,
      rgba(0, 0, 0, 0.7) 0%,
      rgba(0, 0, 0, 0.62) 26%,
      rgba(0, 0, 0, 0.62) 82%,
      rgba(0, 0, 0, 0.72) 100%
    );
  }

  /* The photo plate stays absolutely positioned, so the in-flow copy needs its
     own positioned context to paint above it. */
  .sec--experience .exp__head {
    position: relative;
    z-index: 1;
    left: auto;
    top: auto;
    width: auto;
    height: auto;
    overflow: visible;
    text-align: center;
  }

  .sec--experience .exp__ornament {
    position: static;
    width: 46px;
    height: 51px;
    margin: 0 auto 14px;
  }

  .sec--experience .exp__head-copy {
    position: static;
    gap: 6px;
  }

  .sec--experience .exp__title {
    font-size: 30px;
    line-height: 1.15;
    height: auto;
  }

  .sec--experience .exp__sub {
    font-size: 15px;
    line-height: 1.35;
  }

  .sec--experience .exp__list {
    position: relative;
    z-index: 1;
    left: auto;
    top: auto;
    width: auto;
    height: auto;
    overflow: visible;
    margin-top: 34px;
    padding-right: 18px;
  }

  .sec--experience .exp__list-inner {
    position: relative;
    inset: auto;
  }

  /* The rail spans the list column; the thumb's travel is measured from these
     two boxes at runtime (see initAmenitySwitcher), so the heights here are the
     single source of truth on mobile just as the Figma values are on desktop. */
  .sec--experience .exp__track {
    left: auto;
    right: 0;
    top: 0;
    width: 2px;
    height: 100%;
  }

  .sec--experience .exp__thumb {
    left: auto;
    right: -1px;
    top: 0;
    width: 4px;
    height: 44px;
  }

  /* Still the flex column from the markup, but on a plain margin rhythm. */
  .sec--experience .exp__items {
    position: relative;
    inset: auto;
    padding-top: 0;
    gap: 0;
  }

  .sec--experience .exp__item {
    align-self: stretch;
    height: auto;
    margin-bottom: 14px;
  }

  .sec--experience .exp__item-btn {
    font-size: 19px;
    line-height: 1.35;
    white-space: normal;
  }

  /* 0.5 is the desktop resting state, where the backdrop is darker and the row
     is larger. At 19px over this render it measured 3.67:1 — below the 4.5:1
     floor for text this size — so the inactive rows sit higher on mobile. */
  .sec--experience .exp__item {
    opacity: 0.72;
  }

  .sec--experience .exp__nav {
    position: relative;
    z-index: 1;
    left: auto;
    top: auto;
    margin-top: 30px;
    gap: 12px;
  }

  .sec--experience .exp__nav-btn,
  .sec--experience .exp__nav-btn img {
    width: 64px;
    height: 65px;
  }

  .sec--experience .exp__nav-frost {
    border-radius: 50%;
    -webkit-backdrop-filter: blur(4px);
    backdrop-filter: blur(4px);
  }
}


/* ===== section: location ===== */
/* ==========================================================================
   Sai Palm View — THE LOCATION
   Figma node 175:3949 ("Component 47"), 1920x1563, page offset y = 9457.
   All coordinates below are Figma page coords MINUS 9457, expressed as
   calc(<figma-px> * var(--u)) per spec/CONTRACT.md §1.

   Bootstrap-first: sections/location.php says everything it can with a utility
   or a component —
     .container-fluid.spv-gutter + .row + .col-12.col-lg-6   the two-column split
     .accordion.accordion-spv + .accordion-item/-header/-button/-collapse/-body
                                                             the whole accordion
     .position-absolute/.position-relative/.top-0/.start-0/.w-100/.h-100/
     .overflow-hidden/.pe-none/.z-0/.z-1/.z-2/.z-3            plates + layering
     .d-flex/.justify-content-between/.align-items-center/.align-items-start/
     .flex-shrink-0/.d-block/.list-unstyled/.m-0/.p-0/.border-0
                                                             rows + chrome reset
     .ff-display/.ff-script/.ff-sans/.fw-light/.fw-normal/.fst-italic/
     .text-primary/.text-body/.text-uppercase/.text-capitalize/.text-nowrap/
     .fs-d80/.fs-d44/.fs-d40/.fs-d20/.fs-d18/.lh-d40/.object-fit-cover/
     .object-fit-contain/.bg-body/.paper-wash                 type + colour
   What is left here is only what Bootstrap has no vocabulary for.
   ========================================================================== */

.sec--location {
  /* The fixed Figma canvas. Bootstrap has no height scale at this size. */
  height: calc(1563 * var(--u));

  /* Section-local colour that is not part of the global token set:
     Figma 175:3959, the open panel's "CONNECTIVITY" label. */
  --loc-quiet: #6e6a6a;
}

/* --------------------------------------------------------------------------
   175:3951 "Background" — cream stucco plate, 1920x1782 @ 0,0. The wash and the
   full-bleed box are Bootstrap (.paper-wash + .position-absolute.top-0.start-0
   .w-100); only the plate's over-height and its inner leaf's Figma geometry
   (h-[99.93%] / top-[0.03%] at 70% opacity) are stated here — Bootstrap's
   .opacity-* scale has no 70 step.
   -------------------------------------------------------------------------- */
.sec--location .loc__plate {
  height: calc(1782 * var(--u));
}

.sec--location .loc__plate-img {
  display: block;
  opacity: 0.7;
}

.sec--location .loc__plate-img > img {
  top: 0.03%;
  height: 99.93%;
}

/* --------------------------------------------------------------------------
   175:3952 "palm-tree-shadows-border-isolated 1" — 1946x832 @ 0,0, opacity 15%.
   The leaf is deliberately larger than its window (114.64% x 178.93%) and
   pinned top-left; the window clips it. Both the over-wide box and the
   over-sized fill are outside any utility scale, and the fill has to opt out of
   the global img max-width so it can exceed its window.
   -------------------------------------------------------------------------- */
.sec--location .loc__shadow {
  width: calc(1946 * var(--u));
  height: calc(832 * var(--u));
  opacity: 0.15;
}

.sec--location .loc__shadow > img {
  width: 114.64%;
  height: 178.93%;
  max-width: none;
}

/* --------------------------------------------------------------------------
   175:4039 "Object" — palm mark, 57x63 @ 154,125, i.e. x 14 inside the 140px
   container gutter. Aspect 227/250, bottom-set.
   -------------------------------------------------------------------------- */
.sec--location .loc__mark {
  left: calc(14 * var(--u));
  top: calc(125 * var(--u));
  width: calc(57 * var(--u));
  height: calc(63 * var(--u));
}

.sec--location .loc__mark > img {
  object-position: bottom;
}

/* --------------------------------------------------------------------------
   175:4033 "Frame 2147224967" — heading column, 639x347 @ 140,213.
   Children sit at the flex-resolved offsets 0 / 122 / 210.
   overflow is left visible so a substituted-font extra line can never clip.
   -------------------------------------------------------------------------- */
.sec--location .loc__intro {
  top: calc(213 * var(--u));
  width: calc(639 * var(--u));
  height: calc(347 * var(--u));
}

/* 175:4036 — "THE location": 80px on a 110px line box (1.375, not the 1.15 of
   .lh-d115), so the leading stays an exact Figma length. */
.sec--location .loc__title {
  line-height: calc(110 * var(--u));
}

/* 175:4037 — Canela Thin Italic 40px in a 48px box. */
.sec--location .loc__sub {
  top: calc(122 * var(--u));
  line-height: calc(48 * var(--u));
}

/* 175:4038 — Haas 55 Roman 20px / leading-[40px], capitalize, 3 lines. */
.sec--location .loc__copy {
  top: calc(210 * var(--u));
}

/* --------------------------------------------------------------------------
   175:3953 "Frame 2147224965" — 802x881 @ 140,620.
   Title at (0, 0.35); the accordion block 175:3956 at (0, 106), width 722.4.
   -------------------------------------------------------------------------- */
.sec--location .loc__acc-wrap {
  top: calc(620 * var(--u));
  width: calc(802 * var(--u));
  height: calc(881 * var(--u));
}

/* 175:4032 — 44px on Figma's own 92.755px leading. */
.sec--location .loc__acc-title {
  top: calc(0.35 * var(--u));
  line-height: calc(92.755 * var(--u));
}

/* 175:3956 — the accordion column. .accordion-spv (theme.css) already carries
   the colours, the transparent chrome and the killed chevron; all that is left
   is the column's Figma origin and its 722.4px measure. */
.sec--location .loc__acc {
  top: calc(106 * var(--u));
  width: calc(722.4 * var(--u));
}

/* 175:3996 stacks the closed panels with a 24px flex gap; 175:3956 puts 42px
   after the open one. Bootstrap's .mb-* scale is rem-based and has neither. */
.sec--location .loc__item {
  margin-bottom: calc(24 * var(--u));
}

.sec--location .loc__item--open {
  margin-bottom: calc(42 * var(--u));
}

.sec--location .loc__item:last-child {
  margin-bottom: 0;
}

/* Header block: the 32px label row + a 19.2px gap, so the rule lands at 51.2. */
.sec--location .loc__head {
  padding-bottom: calc(19.2 * var(--u));
}

/* 175:4004 (Education) / 175:4011 (Lifestyle) are only 37.2px tall in Figma —
   the rule still paints at 51.2 (it is not clipped), so only the flow height
   shrinks. Keyed off the category id the loop already emits. */
.sec--location .loc__item--education .loc__head,
.sec--location .loc__item--lifestyle .loc__head {
  padding-bottom: calc(5.2 * var(--u));
}

/* Theme.css switches Bootstrap's focus box-shadow off, so the keyboard ring is
   drawn here instead — CONTRACT §10 (interactive things stay operable). */
.sec--location .loc__btn:focus-visible {
  outline: max(1px, calc(2 * var(--u))) solid var(--spv-gold-line);
  outline-offset: calc(4 * var(--u));
}

/* 21.6px Haas Light on a 32px line, tracking 0.216px. Neither the size nor the
   tracking is on a utility scale (.fs-d22 would be 0.4px too large), and the
   0.02em of .accordion-spv .accordion-button has to be replaced. */
.sec--location .loc__btn-label {
  font-size: calc(21.6 * var(--u));
  line-height: calc(32 * var(--u));
  letter-spacing: calc(0.216 * var(--u));
}

/* 175:3959 — the open panel's label is Haas Thin in #6e6a6a, not the ink of
   the closed ones. */
.sec--location .loc__btn-label--quiet {
  color: var(--loc-quiet);
}

/* 175:3960 minus (18x2.4) and 175:4000 plus (18x18). */
.sec--location .loc__sign {
  width: calc(18 * var(--u));
  height: calc(18 * var(--u));
}

/* The glyph swap is state-driven, so Bootstrap's !important .opacity-* utilities
   cannot express it. */
.sec--location .loc__sign > img {
  transition: opacity 0.35s ease;
}

.sec--location .loc__sign-minus {
  /* 175:3960 is a zero-height node centred on the row, with its 2.4px stroke
     drawn ABOVE the baseline (inset[-2.4px 0 0 0]) — so the bar sits at
     6.8 inside the 18px sign box, not centred in it. */
  top: calc(6.8 * var(--u));
  height: max(1px, calc(2.4 * var(--u)));
  opacity: 0;
}

.sec--location .loc__btn[aria-expanded="true"] .loc__sign-plus {
  opacity: 0;
}

.sec--location .loc__btn[aria-expanded="true"] .loc__sign-minus {
  opacity: 1;
}

/* 175:3961 / 175:4003 etc. — the 722.404px hairline, drawn at y 51.2 inside
   every header. .accordion-spv's item border-bottom cannot sit there (the open
   panel's body hangs below it, and two headers are only 37.2 tall), which is
   why the markup keeps .border-0 and paints Figma's own asset instead. */
.sec--location .loc__rule {
  top: calc(51.2 * var(--u));
  height: max(1px, calc(1.2 * var(--u)));
}

/* 175:3962 — the open panel. Rows start 42px below the header block, i.e. at
   y = 93.2 inside 175:3956 (theme.css's 18px body padding is the generic one). */
.sec--location .loc__body {
  padding: calc(42 * var(--u)) 0 0;
}

/* Each stop is a 27px row (its label's line box) with a 28.8px gap →
   250.2px for the five rows. Neither value is on a Bootstrap scale. */
.sec--location .loc__stop {
  margin-bottom: calc(28.8 * var(--u));
}

.sec--location .loc__stop:last-child {
  margin-bottom: 0;
}

/* 175:3965 "ion:location-outline" — 24x24, so y = 1.5 in the 27px row. */
.sec--location .loc__stop-icon {
  width: calc(24 * var(--u));
  height: calc(24 * var(--u));
}

/* 18px Haas Light on a 27px line, tracking 0.18px: .fs-d18 carries the size,
   but there is no .lh-d27 and no tracking utility at this value. */
.sec--location .loc__stop-label,
.sec--location .loc__stop-time {
  line-height: calc(27 * var(--u));
  letter-spacing: calc(0.18 * var(--u));
}

/* Label starts at x = 43.2 (24px icon + a 19.2px gap; .gap-* has no 19.2 step). */
.sec--location .loc__stop-label {
  margin-left: calc(19.2 * var(--u));
}

/* --------------------------------------------------------------------------
   175:4040 "Background" — the illustrated location map, 1920x1563 @ 0,0. The
   plate itself is pure Bootstrap (.position-absolute.top-0.start-0.w-100.h-100
   .overflow-hidden.pe-none.z-3 + .object-fit-cover); only Figma's centred crop
   origin is stated here, because the mobile block re-pins it.

   v1 also painted the same WebP as a CSS background on the <img> — a "paint
   fallback" added because Chrome skips rasterising this large alpha WebP in an
   offscreen capture, leaving the <img> blank. At 1920 it is no longer needed
   (the fill is drawn near 1:1 and does rasterise) and it is actively wrong
   there: it composited the map's semi-transparent pixels a SECOND time, which
   darkened ~9k of them by ~33 levels against baseline/location.png. At mobile
   width the fill is a heavy downscale that Chrome still skips, so the fallback
   is kept in the media query below — measured, not assumed: without it the 430
   capture has zero map pixels, with it 8204.
   -------------------------------------------------------------------------- */
.sec--location .loc__map > img {
  object-position: center;
  /* The mobile block over-sizes this fill to 145%, which the global
     img { max-width: 100% } would otherwise clamp. */
  max-width: none;
}

/* --------------------------------------------------------------------------
   Mobile — the section stops being a fixed-height canvas and reflows.
   Raw px allowed here per CONTRACT §11 (note js/main.js rebases --u to /560
   below 992px, so every desktop calc(<figma> * var(--u)) must be overridden).
   Bootstrap ships no responsive variants of position, w or h, so the
   utilities applied in the markup have to be unwound with !important.
   -------------------------------------------------------------------------- */
@media (max-width: 991.98px) {
  .sec--location {
    height: auto;
    padding: 56px 0 64px;
  }

  /* The design gutter is 140 Figma px; rebased to /560 that would be ~107px of
     page margin, so the container falls back to a 24px mobile margin. */
  .sec--location .spv-gutter {
    --bs-gutter-x: 48px;
  }

  .sec--location .loc__plate,
  .sec--location .loc__shadow {
    width: 100%;
    height: 100%;
  }

  .sec--location .loc__shadow > img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: top center;
  }

  /* The blocks go back into normal flow; .loc__col keeps its position-relative
     + z-2, so the text still paints above the plate and the palm shadow. */
  .sec--location .loc__mark,
  .sec--location .loc__intro,
  .sec--location .loc__title,
  .sec--location .loc__sub,
  .sec--location .loc__copy,
  .sec--location .loc__acc-wrap,
  .sec--location .loc__acc-title,
  .sec--location .loc__acc,
  .sec--location .loc__rule {
    position: static !important;
    left: auto !important;
    top: auto !important;
    width: auto !important;
    height: auto;
  }

  .sec--location .loc__mark {
    width: 44px !important;
    height: 49px;
    margin: 0 0 16px;
  }

  .sec--location .loc__intro {
    margin: 0 0 28px;
  }

  .sec--location .loc__title {
    font-size: 44px;
    line-height: 1.15;
  }

  .sec--location .loc__sub {
    font-size: 24px;
    line-height: 1.2;
    margin-top: 8px !important;
  }

  .sec--location .loc__copy {
    font-size: 15px;
    line-height: 26px;
    margin-top: 18px !important;
  }

  /* The map PNG is transparent across its left 31%; on mobile the wrapper is
     given the aspect of the opaque area and the leaf is over-sized and pinned
     right so only the drawn map shows. */
  .sec--location .loc__map {
    position: relative !important;
    left: auto !important;
    top: auto !important;
    width: 100%;
    max-width: 560px;
    height: 0 !important;
    padding-bottom: 105%;
    margin: 0 auto 34px;
  }

  .sec--location .loc__map > img {
    position: absolute;
    top: 0;
    right: 0;
    width: 145% !important;
    height: 100% !important;
    object-position: right center;
    /* Paint fallback (see above): at this scale Chrome leaves the <img> blank in
       an offscreen capture. The background is the same file on the same box, so
       it reproduces object-fit:cover / object-position:right center exactly. */
    background: url("../assets/img/location-map.webp") right center / cover no-repeat;
  }

  .sec--location .loc__acc-title {
    font-size: 28px;
    line-height: 1.25;
    margin-bottom: 20px !important;
    white-space: normal !important;
  }

  .sec--location .loc__item,
  .sec--location .loc__item--open {
    margin-bottom: 14px;
  }

  .sec--location .loc__head,
  .sec--location .loc__item--education .loc__head,
  .sec--location .loc__item--lifestyle .loc__head {
    padding-bottom: 0;
  }

  .sec--location .loc__btn {
    min-height: 44px;
    align-items: center !important;
  }

  .sec--location .loc__btn-label {
    font-size: 14px;
    line-height: 1.35;
    letter-spacing: 0.14px;
    white-space: normal !important;
    padding-right: 14px;
  }

  .sec--location .loc__sign {
    width: 16px;
    height: 16px;
  }

  .sec--location .loc__sign-minus {
    top: 7px;
    height: 2px;
  }

  .sec--location .loc__rule {
    width: 100% !important;
    height: 1px;
    margin-top: 12px;
  }

  .sec--location .loc__body {
    padding: 16px 0 0;
  }

  .sec--location .loc__stop {
    min-height: 24px;
    margin-bottom: 16px;
  }

  .sec--location .loc__stop-icon {
    width: 20px;
    height: 20px;
  }

  .sec--location .loc__stop-label,
  .sec--location .loc__stop-time {
    font-size: 14px;
    line-height: 20px;
    letter-spacing: 0.14px;
  }

  .sec--location .loc__stop-label {
    margin-left: 12px;
    white-space: normal !important;
  }

  .sec--location .loc__stop-time {
    flex: 0 0 auto;
    padding-left: 10px;
  }
}


/* ===== section: tower ===== */
/* ==========================================================================
   Sai Palm View — PARADISE LEGACY / the tower at night
   Figma node 175:4041 ("Frame 2147225017"), frame 1920x2298 at page y = 11020.
   Every coordinate below is frame-relative (page y minus 11020).

   Bootstrap-first: everything this section can say with a utility is said in
   sections/tower.php — .layer/.position-absolute/.top-0/.start-0/.w-100/.h-100/
   .overflow-hidden/.pe-none/.object-fit-cover for the two render plates,
   .d-flex/.flex-column/.align-items-start for each figure stack,
   .text-center/.text-nowrap/.m-0 for the title band,
   .ff-display/.ff-sans/.fw-light/.fw-medium/.fst-italic/.text-uppercase/
   .text-capitalize/.text-white/.fs-d180/.fs-d40/.lh-d115/.bg-body/
   .text-decoration-none for type and colour, and .z-0/.z-1/.z-2/.z-3 for the
   stacking order. What is left here is only what Bootstrap has no vocabulary
   for: exact Figma coordinates, the fixed canvas height, the over-scaled second
   plate, two type sizes off the .fs-d* scale, and the documented
   font-substitution compensations.
   ========================================================================== */

/* The fixed Figma canvas. Bootstrap has no height scale at this size. */
.sec--tower {
  height: calc(2298 * var(--u));
}

/* --------------------------------------------------------------------------
   Second copy of the same render — Figma 175:4045. A 1920x1571 clipping window
   at x=-1 / y=727; the image inside is 146.34% tall, 100% wide and offset to
   left 0.05% / top -46.31%, which lands it exactly on top of the plate above.
   Stacked behind the content (see the note in the partial) so it reads as the
   designed image and nothing is buried. .object-fit-cover does the cropping;
   these five values do the off-centre framing.
   -------------------------------------------------------------------------- */
.sec--tower .twr__bg-lower {
  left: calc(-1 * var(--u));
  top: calc(727 * var(--u));
  width: calc(1920 * var(--u));
  height: calc(1571 * var(--u));
}

.sec--tower .twr__bg-lower-img {
  left: 0.05%;
  top: -46.31%;
  width: 100%;
  height: 146.34%;
  max-width: none;
}

/* --------------------------------------------------------------------------
   PARADISE LEGACY column — Figma 175:4046 "Component 62", 1920x897 at 0,0.
   The 1920 width is .w-100; only the frame height is stated here.
   -------------------------------------------------------------------------- */
.sec--tower .twr__legacy {
  height: calc(897 * var(--u));
}

/* Title — Figma 175:3722 clip box 1726x181 at x=97, text 180px Canela Thin.
   line-height is pinned to the Figma 181px text box, not Cormorant's ~1.3, so
   .lh-d115 cannot stand in for it. */
.sec--tower .twr__title-mask {
  left: calc(97 * var(--u));
  width: calc(1726 * var(--u));
  height: calc(181 * var(--u));
}

.sec--tower .twr__title {
  line-height: calc(181 * var(--u));
}

/* Sub-line — Figma 175:3724 box 523x48 at x=699 / y=216, 40px on 48px leading.
   theme.css ships no .lh-d48, so the leading stays here; the 48px box height is
   exactly the line box, so it needs no rule of its own. */
.sec--tower .twr__sub {
  left: calc(699 * var(--u));
  top: calc(216 * var(--u));
  width: calc(523 * var(--u));
  line-height: calc(48 * var(--u));
}

/* Copy — Figma 175:3726 box 729x300 at x=141 / y=408. Three paragraphs of
   Haas Grot Disp "55 Roman" 24px on 50px leading, capitalize, white, with the
   tail of the middle paragraph in "66 Medium Italic".
   Metric note: Inter sets ~14% wider than Haas Grot Display, so at the Figma
   24px every paragraph spills to a third line and the block overruns its 300px
   box into the ENQUIRE link. Compensated to 20.4 with a hair of negative
   tracking, which restores the designed 2/2/2 line pattern, the 50px leading
   and the exact 300px block height. */
.sec--tower .twr__copy {
  left: calc(141 * var(--u));
  top: calc(408 * var(--u));
  width: calc(729 * var(--u));
  height: calc(300 * var(--u));
  font-size: calc(20.4 * var(--u));
  line-height: calc(50 * var(--u));
  letter-spacing: -0.014em;
}

/* ENQUIRE NOW — Figma 175:3728, 228x134 at x=141 / y=763. Circle group at +72
   (128x134), rule at +149 / +67 (79.158 wide, the exported arrow leaf overhangs
   it by -3.68px vertically and -0.63% horizontally), label at +0 / +55.
   Geometry comes from .know-more in theme.css; only the frame coordinates and
   the horizontal offsets are section-specific. */
.sec--tower .twr__enquire {
  left: calc(141 * var(--u));
  top: calc(763 * var(--u));
  width: calc(228 * var(--u));
}

.sec--tower .twr__enquire .know-more__circle {
  left: calc(72 * var(--u));
}

.sec--tower .twr__enquire .know-more__rule {
  left: calc(149 * var(--u));
}

.sec--tower .twr__enquire .know-more__rule img {
  position: absolute;
  top: calc(-3.68 * var(--u));
  left: -0.63%;
  width: 101.26%;
  height: calc(7.36396 * var(--u));
  max-width: none;
}

.sec--tower .twr__enquire .know-more__label {
  left: 0;
}

/* White focus ring, because the link sits on a night photograph. */
.sec--tower .twr__enquire:focus-visible {
  outline: max(1px, calc(2 * var(--u))) solid var(--bs-white);
  outline-offset: calc(6 * var(--u));
}

/* --------------------------------------------------------------------------
   Track-record figures — Figma 175:4044 "Component 7". The instance sits at
   y=69 inside a wrapper at y=626, so every figure's page-frame y is its
   in-component y + 695. Settled variant "Frame 2147224987" x/y used.
   Number: 100px Canela Thin uppercase. Label: 40px Haas Grot Disp "36 Thin
   Italic", capitalize, wrapping inside the figure's own width.
   The settled variant renders (52:3427 / 52:3431 / 52:3435 / 52:3439) put the
   number ink on a baseline 89px below the figure's top and the label lines on a
   48px pitch with the first cap top at 128 — the values reproduced below.
   -------------------------------------------------------------------------- */

/* 100px is not on theme.css's .fs-d* scale. Cormorant Garamond also defaults to
   old-style figures where Canela's are lining, which no utility can state. */
.sec--tower .twr__stat-num {
  font-size: calc(100 * var(--u));
  font-variant-numeric: lining-nums;
  font-feature-settings: "lnum" 1;
}

/* Label — Figma 40px "36 Thin Italic" on a 48px line pitch (measured between
   the two lines of 52:3431: cap tops at 128 and 176).
   Metric note: Inter Light sets ~21% wider than Haas Grot Disp Thin Italic, so
   at the Figma 40px every label gains a line — the settled variants set
   "Successful Projects", "Glorious Decades" and "Happy Residents" on ONE line
   and "Landmark Projects / Under Construction" on TWO, where Inter at 40 gives
   2/2/3/1 and grows each block by a whole 48px line. Compensated to 31.5, which
   restores the designed 1/1/2/1 pattern and lands the label widths within 2% of
   the Figma ones (288/260/272+286 vs 293/268/285+291). The 48px pitch is kept
   literal so the second line and the block height stay on the Figma grid. */
.sec--tower .twr__stat-label {
  font-size: calc(31.5 * var(--u));
  line-height: calc(48 * var(--u));
}

/* 175:3638 — x 1373 / y 232 (+695 = 927), 294x202 */
.sec--tower .twr__stat--projects {
  left: calc(1373 * var(--u));
  top: calc(927 * var(--u));
  width: calc(294 * var(--u));
}

/* 175:3626 — x 407 / y 374 (+695 = 1069), 272x202 */
.sec--tower .twr__stat--decades {
  left: calc(407 * var(--u));
  top: calc(1069 * var(--u));
  width: calc(272 * var(--u));
}

/* 175:3634 — x 1280 / y 711 (+695 = 1406), 362x250 */
.sec--tower .twr__stat--pipeline {
  left: calc(1280 * var(--u));
  top: calc(1406 * var(--u));
  width: calc(362 * var(--u));
}

/* 175:3630 — x 228 / y 849 (+695 = 1544), 338x202 */
.sec--tower .twr__stat--residents {
  left: calc(228 * var(--u));
  top: calc(1544 * var(--u));
  width: calc(338 * var(--u));
}

/* --------------------------------------------------------------------------
   Mobile — the fixed-height canvas collapses into normal flow. The render
   stays as a fixed background plate behind the stacked copy.
   Stock Bootstrap 5.3 ships no responsive position-* / text-align / white-space
   variants, so the utilities applied in the markup have to be unwound with
   !important here.
   -------------------------------------------------------------------------- */
@media (max-width: 991.98px) {
  /* .bg-body is !important, so the night backdrop has to out-shout it. */
  .sec--tower {
    height: auto;
    padding: 56px 0 64px;
    background: #0b1426 !important;
  }

  .sec--tower .twr__bg {
    position: absolute;
    height: 100%;
  }

  .sec--tower .twr__bg-img {
    object-position: 50% 30%;
  }

  /* Legibility scrim — the render carries the copy on desktop, but at phone
     width the lit tower runs straight through the text column. */
  .sec--tower .twr__bg::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(
      180deg,
      rgba(11, 20, 38, 0.78) 0%,
      rgba(11, 20, 38, 0.66) 55%,
      rgba(11, 20, 38, 0.82) 100%
    );
  }

  .sec--tower .twr__bg-lower {
    display: none;
  }

  .sec--tower .twr__title-mask,
  .sec--tower .twr__sub,
  .sec--tower .twr__copy {
    position: static !important;
    left: auto;
    top: auto;
    width: auto !important;
    height: auto;
  }

  /* Back in normal flow, but still above the absolutely positioned plate. */
  .sec--tower .twr__legacy,
  .sec--tower .twr__stats {
    position: relative !important;
    z-index: 2;
  }

  .sec--tower .twr__legacy {
    height: auto;
    padding: 0 24px;
  }

  .sec--tower .twr__title {
    font-size: 44px;
    line-height: 1.08;
    text-align: left !important;
    white-space: normal !important;
  }

  .sec--tower .twr__sub {
    margin-top: 12px !important;
    font-size: 18px;
    line-height: 1.25;
    text-align: left !important;
    white-space: normal !important;
  }

  .sec--tower .twr__copy {
    margin-top: 24px;
    font-size: 15px;
    line-height: 1.9;
    letter-spacing: 0;
  }

  .sec--tower .twr__copy p + p {
    margin-top: 14px !important;
  }

  .sec--tower .twr__enquire {
    position: relative !important;
    left: auto;
    top: auto;
    display: block;
    width: 186px;
    height: 104px;
    margin: 24px 0 0;
  }

  .sec--tower .twr__enquire .know-more__circle {
    left: 58px;
    top: 0;
    width: 100px;
    height: 104px;
  }

  .sec--tower .twr__enquire .know-more__rule {
    left: 118px;
    top: 52px;
    width: 62px;
  }

  .sec--tower .twr__enquire .know-more__rule img {
    top: -2.85px;
    height: 5.7px;
  }

  .sec--tower .twr__enquire .know-more__label {
    top: 42px;
    font-size: 16px;
  }

  .sec--tower .twr__enquire:focus-visible {
    outline: 2px solid var(--bs-white);
    outline-offset: 6px;
  }

  .sec--tower .twr__stats {
    display: flex;
    flex-wrap: wrap;
    gap: 28px 20px;
    margin-top: 44px;
    padding: 0 24px;
  }

  .sec--tower .twr__stat {
    position: static !important;
    left: auto;
    top: auto;
    width: calc(50% - 10px);
  }

  .sec--tower .twr__stat-num {
    font-size: 44px;
    line-height: 1.1;
  }

  .sec--tower .twr__stat-label {
    font-size: 16px;
    line-height: 1.3;
  }
}


/* ===== section: gallery ===== */
/* ==========================================================================
   Sai Palm View — "The Gallery" (Figma: Component 64)
   Figma node 175:4047, page offset y = 13318, design box 1920 x 1080.
   Every coordinate below is section-local (page y already reduced by 13318 —
   the node's own children are already expressed relative to the section).

   Bootstrap-first: layout, stacking, object-fit, flexbox, colour and casing all
   come from utilities in sections/gallery.php. What is left here is only what
   Bootstrap has no vocabulary for — exact Figma coordinates, the fixed canvas
   height, non-standard opacities, the blur, the inside-stroke photo mount, the
   rotated fan and Swiper's inline-style overrides.
   ========================================================================== */

.sec--gallery {
  height: calc(1080 * var(--u));
}

/* Figma draws every plate at its own explicit box; theme.css clamps images to
   their container, which would crush the over-scanned stucco texture. */
.sec--gallery img {
  max-width: none;
}

/* --------------------------------------------------------------------------
   1. Cream stucco plate + paper wash  (Figma "Background" I175:4047;175:3791)
   Box 1920 x 1782 at (0, -281). Texture fill at 70%, then the 69% paper wash
   (.paper-wash in theme.css). left:0 / pointer-events / the wash come from
   .start-0 .pe-none .paper-wash; only the box is Figma-specific.
   -------------------------------------------------------------------------- */
.sec--gallery .gal__paper {
  top: calc(-281 * var(--u));
  width: calc(1920 * var(--u));
  height: calc(1782 * var(--u));
}

/* Figma's fill opacity — not one of Bootstrap's .opacity-25/50/75 steps. */
.sec--gallery .gal__paper-img-wrap {
  opacity: 0.7;
}

/* Figma: h-[99.93%] top-[0.03%] w-full on the 2000 x 1855 source. */
.sec--gallery .gal__paper-img {
  top: 0.03%;
  height: 99.93%;
}

/* --------------------------------------------------------------------------
   2. Flamingo illustration  (Figma I175:4047;175:3793)
   507 x 760 at (1428, 403); layer opacity 39%; bleeds past the 1920 right edge
   and past the 1080 bottom edge — .sec clips both, exactly as Figma does.
   The 0.39 lives on the <img> so the wrapper's opacity is free for the fade-in.
   -------------------------------------------------------------------------- */
.sec--gallery .gal__flamingo {
  left: calc(1428 * var(--u));
  top: calc(403 * var(--u));
  width: calc(507 * var(--u));
  height: calc(760 * var(--u));
}

.sec--gallery .gal__flamingo-img {
  opacity: 0.39;
}

/* --------------------------------------------------------------------------
   3. Heading block  (Figma I175:4047;175:3878, settled box 647 x 214 at 637,60)
   Captured mid-animation inside a 1px-tall clip window (I175:4047;175:3877) that
   sits on the frame's bottom edge with the content parked at top:-213px — the
   "before" frame of a bottom-anchored wipe. Settled state is authored here.
   Column is centred with no gap: ornament 63 + copy column 151 = 214.
   -------------------------------------------------------------------------- */
.sec--gallery .gal__head {
  left: calc(637 * var(--u));
  top: calc(60 * var(--u));
  width: calc(647 * var(--u));
}

/* Palm crest mark — Figma 57 x 63, bottom-aligned inside its box */
.sec--gallery .gal__ornament {
  width: calc(57 * var(--u));
  height: calc(63 * var(--u));
  object-position: bottom;
}

/* Figma 175:3880 — 12px column gap; Bootstrap's .gap-* scale is rem-based and
   does not land on a Figma pixel. */
.sec--gallery .gal__head-copy {
  gap: calc(12 * var(--u));
}

/* FONT SUBSTITUTION: Figma's auto-height for the mixed Canela + Fleur De Leah
   line is 110px against the 80px type. Cormorant/Fleur stand in for the
   unlicensed originals and their normal leading is shorter, so the 110px is
   pinned here to keep the heading block exactly 214 tall. (No .lh-d* step
   matches 110/80 = 1.375, and .lh-d115 would be the wrong value anyway.) */
.sec--gallery .gal__title {
  line-height: calc(110 * var(--u));
}

/* Figma "Haas Grot Text Trial: 56 Italic" 24px — node height is 29, between
   theme.css's .lh-d30 and .lh-d36 steps. */
.sec--gallery .gal__sub {
  line-height: calc(29 * var(--u));
}

/* --------------------------------------------------------------------------
   4. Fanned photo stack → Swiper  (Figma 175:3800 / 175:3801 / 175:3799)
   The root box is the FRONT plate: 658 x 454 at (632, 443), 11px white mount.
   Centre of that box = (961, 670) in section space.

   The two plates behind it are 660 x 454 mounts rotated about their own centres:
     back-right  rotate( 9.55deg)  centre (945.09, 643.01) -> d(-15.91, -26.99)
     back-left   rotate(-12.38deg) centre (953.01, 670.48) -> d( -7.99,  +0.48)
   (centres recovered from the rotated bounding boxes Figma reports:
    726.173 x 557.203 @ 582,364.4  and  741.965 x 584.901 @ 582,378)

   Those deltas as a share of the 658 x 454 slide box are the percentages handed
   to Swiper's creative effect: next (-2.418%, -5.945%), prev (-1.214%, 0.106%).
   .overflow-visible on the root (Swiper's own .swiper sets overflow:hidden) lets
   the rotated mounts spill out, exactly as Figma draws them.
   -------------------------------------------------------------------------- */
/* The resolved three-up band — Figma's own end-state frame (175:3884,
   "Component 63" of the gallery set): three 976x673 plates on top 334, at
   left -555 / 473 / 1500, i.e. 51px gaps and a 1027 pitch, the outer two
   bleeding off the edges. The centre plate is flat; the side plates carry
   Figma's 2.5px blur. Swiper's own .swiper sets overflow:hidden, so
   .overflow-visible on the root lets the outer frames bleed as drawn. */
.sec--gallery .gal__swiper {
  left: 0;
  top: calc(334 * var(--u));
  width: 100%;
  height: calc(673 * var(--u));
}

/* slidesPerView:'auto' takes the width from here. */
.sec--gallery .gal__slide {
  width: calc(976 * var(--u));
  height: calc(673 * var(--u));
}

.sec--gallery .gal__slide-inner {
  transform-origin: 50% 50%;
  /* Side plates: Figma's 2.5px blur. The side photographs are monochrome in the
     file itself; grayscale is kept as well so that whichever image the arrows
     move out of the centre reads the same way. Bootstrap has no filter
     utility, and the transition has to sit on the element Swiper re-classes. */
  filter: grayscale(1) blur(calc(2.5 * var(--u)));
  transition: filter 0.45s ease;
  will-change: transform, filter;
}

.sec--gallery .swiper-slide-active .gal__slide-inner {
  filter: grayscale(0) blur(0);
}

/* 11px white photo mount. Figma draws it as an INSIDE stroke on the plate, so
   the photograph fills the whole box and the mount is painted over it — a real
   CSS border (or Bootstrap's .border) would shrink the image box and zoom the
   crop. Hence the overlay pseudo-element, which Bootstrap has no utility for. */
.sec--gallery .gal__slide-inner::after {
  content: "";
  position: absolute;
  inset: 0;
  border: max(1px, calc(11 * var(--u))) solid var(--bs-white);
  pointer-events: none;
  z-index: 2;
}

/* The active frame sits above its neighbours. */
.sec--gallery .gal__swiper .swiper-slide { z-index: 1; }
.sec--gallery .gal__swiper .swiper-slide-active { z-index: 3; }

/* No-JS / pre-init fallback: Swiper has not laid the track out yet, so hold the
   three frames in a plain centred row rather than letting them stack at x=0. */
.sec--gallery .gal__swiper:not(.swiper-initialized) .swiper-wrapper {
  display: flex;
  justify-content: center;
  gap: calc(51 * var(--u));
  transform: none;
}

.sec--gallery .gal__swiper:not(.swiper-initialized) .gal__slide {
  flex: 0 0 auto;
}

.sec--gallery .gal__swiper:not(.swiper-initialized) .gal__slide--front .gal__slide-inner {
  filter: grayscale(0);
}

/* --------------------------------------------------------------------------
   5. Prev / next controls  (Figma "Frame 43" I175:4047;175:3802)
   Row 1640 x 122 at (140, 610), space-between; each control 119 x 122.
   Captured at opacity-0 — rendered settled at full opacity.
   The flex row itself is .d-flex .align-items-center .justify-content-between.
   -------------------------------------------------------------------------- */
.sec--gallery .gal__nav {
  left: calc(140 * var(--u));
  top: calc(610 * var(--u));
  width: calc(1640 * var(--u));
  height: calc(122 * var(--u));
}

.sec--gallery .gal__nav-btn {
  width: calc(119 * var(--u));
  height: calc(122 * var(--u));
  transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.3s ease;
}

.sec--gallery .gal__nav-btn--prev:hover {
  transform: translateX(calc(-8 * var(--u)));
}

.sec--gallery .gal__nav-btn--next:hover {
  transform: translateX(calc(8 * var(--u)));
}

.sec--gallery .gal__nav-btn:focus-visible {
  outline: max(1px, calc(2 * var(--u))) solid var(--spv-gold);
  outline-offset: calc(4 * var(--u));
}

/* Swiper adds this class itself; opacity 0.35 is not a Bootstrap step. */
.sec--gallery .gal__nav-btn.swiper-button-disabled {
  opacity: 0.35;
  pointer-events: none;
}

/* --------------------------------------------------------------------------
   6. Mobile — static flow, raw px.
   The desktop composition is absolutely positioned through Bootstrap's
   !important position/overflow utilities, and Bootstrap ships no responsive
   variant of either, so unwinding them here needs !important too. Everything
   Bootstrap CAN vary by breakpoint is already handled in the markup
   (justify-content-center / justify-content-lg-between on the nav row).
   -------------------------------------------------------------------------- */
@media (max-width: 991.98px) {
  .sec--gallery {
    height: auto;
    padding: 48px 24px 56px;
  }

  .sec--gallery .gal__paper {
    top: 0;
    width: 100%;
    height: 100%;
  }

  .sec--gallery .gal__paper-img {
    top: 0;
    height: 100%;
  }

  .sec--gallery .gal__flamingo {
    left: auto;
    right: -10%;
    top: auto;
    bottom: 0;
    width: 46%;
    height: 52%;
  }

  /* Normal flow, but still POSITIONED: the paper plate is an absolutely
     positioned z-index:0 layer, and a static heading would paint underneath it
     and come out washed by the 69% paper wash. */
  .sec--gallery .gal__head {
    position: relative !important;
    left: auto;
    top: auto;
    width: 100%;
    margin: 0 auto 28px;
  }

  .sec--gallery .gal__ornament {
    width: 40px;
    height: 44px;
  }

  .sec--gallery .gal__head-copy {
    gap: 8px;
  }

  .sec--gallery .gal__title {
    font-size: 40px;
    line-height: 1.3;
  }

  .sec--gallery .gal__sub {
    font-size: 15px;
    line-height: 1.35;
  }

  .sec--gallery .gal__swiper {
    position: static !important;
    width: 100%;
    height: auto;
    aspect-ratio: 976 / 673;
    overflow: hidden !important;
  }

  /* One frame at a time on mobile — the three-up band needs the width. */
  .sec--gallery .gal__slide {
    width: 100%;
    height: auto;
    aspect-ratio: 976 / 673;
  }

  .sec--gallery .gal__slide-inner::after {
    border-width: 6px;
  }

  /* No de-saturation on mobile: only one frame is on screen at a time, so a grey
     neighbour would just read as a broken image mid-swipe. */
  .sec--gallery .gal__slide-inner {
    filter: grayscale(0);
  }

  .sec--gallery .gal__swiper:not(.swiper-initialized) .swiper-wrapper {
    gap: 0;
  }

  .sec--gallery .gal__nav {
    position: static !important;
    width: 100%;
    height: auto;
    gap: 20px;
    margin-top: 22px;
  }

  .sec--gallery .gal__nav-btn {
    width: 58px;
    height: 60px;
  }

  .sec--gallery .gal__nav-btn--prev:hover,
  .sec--gallery .gal__nav-btn--next:hover {
    transform: none;
  }

  .sec--gallery .gal__nav-btn:focus-visible {
    outline: 2px solid var(--spv-gold);
    outline-offset: 3px;
  }
}


/* ===== section: footer ===== */
/* ==========================================================================
   Sai Palm View — 10 FOOTER
   Figma node 175:4048 ("Component 33"), page offset y = 14398, box 1920x1142.
   Every coordinate below is the Figma x / (y - 14398) expressed through --u.

   Layer stack (bottom → top):
     image 13            (-3, 361)  1921x1277  faint palm illustration plate
     bg img roman 1 1    (0, 0)     1921x1080  NO FILL EXPORTED — omitted
     Background          (0, -273)  1920x1782  stucco texture @70% + 69% wash
     … content …
     image 11            (710, 623) 1200x600   NO FILL EXPORTED — omitted

   Bootstrap-first: the whole grid is stated in sections/footer.php —
   .container-fluid.spv-gutter-footer for the 112 margin, .row/.col-auto/
   .flex-lg-nowrap/.flex-wrap for the four columns, the social strip and the QR
   row, .d-flex/.flex-column/.flex-lg-row/
   .align-items-start/.align-items-center/.align-items-lg-start/
   .justify-content-center/.text-center/.text-lg-start for every
   stack, .list-unstyled/.mb-0 for the two lists, .position-absolute/.top-0/
   .start-0/.w-100/.h-100/.object-fit-cover/.pe-none/.z-0/.z-1/.z-2 for the
   plates and layering, .rounded-circle/.border for the YouTube ring, .flex-shrink-0 for fixed items,
   .bg-body/.bg-white/.text-primary/.text-body, .ff-display/.ff-sans/.fw-light/
   .fw-medium/.fst-italic/.text-uppercase/.text-capitalize/.text-nowrap/
   .fs-d64/.fs-d48/.fs-d32/.fs-d20/.fs-d16/.lh-d36/.lh-d30/.lh-sm for type,
   .btn.btn-spv for the CTA and <hr class="opacity-100"> for the hairline.

   What is left here is only what Bootstrap has no vocabulary for: the fixed
   1142 canvas, the two decorative plate boxes and their inner geometry, the
   exact Figma column widths / offsets / gaps, leadings that are off the .lh-d*
   scale, and the documented font-substitution compensations.
   ========================================================================== */

.sec--footer {
  /* The fixed Figma canvas. Bootstrap has no height scale at this size. */
  height: calc(1142 * var(--u));

  /* Section-local colours that are not global tokens */
  --ftr-ink-strong: #3a3a3a;         /* "Live the Finest" */
  --ftr-circle: #808080;             /* social circle stroke */
  --ftr-rule: rgba(78, 78, 80, 0.3); /* Figma "Vector 1" stroke */

  /* Every footer link is ink and turns gold on hover. Driving that through
     Bootstrap's own link variables keeps .text-* utilities out of the way and
     means the hover needs no !important to beat them. */
  --bs-link-color-rgb: var(--bs-body-color-rgb);
  --bs-link-hover-color-rgb: var(--bs-primary-rgb);
}

.sec--footer a {
  transition: color 0.35s ease;
}

/* --------------------------------------------------------------------------
   Background plates — free composition, so the boxes are Figma coordinates
   -------------------------------------------------------------------------- */

/* Palm illustration — 1921x1277 at (-3, 361). It sits UNDER the stucco + 69%
   wash, which is what makes it read as a ghosted watermark. */
.sec--footer .ftr__palm {
  left: calc(-3 * var(--u));
  top: calc(361 * var(--u));
  width: calc(1921 * var(--u));
  height: calc(1277 * var(--u));
}

/* Stucco plate: 1920x1782 (the .w-100 covers the width) hung off the top at
   -273. The image inside is inset by Figma's own 0.03% / 99.93% crop and
   painted at 70%, which is off Bootstrap's .opacity-* scale. */
.sec--footer .ftr__paper {
  top: calc(-273 * var(--u));
  height: calc(1782 * var(--u));
}

.sec--footer .ftr__paper-img {
  top: 0.03%;
  height: 99.93%;
  opacity: 0.7;
}

/* --------------------------------------------------------------------------
   The grid — Figma's vertical rhythm down the 1142 canvas.
   Rows are in normal flow; only the Figma gaps between them are stated.
     80    column row (338 tall, the logo lockup sets it)
     444   social strip  (418 + 26)
     518.8 legal strip   (481.8 + 37)
     577.8 hairline      (541.8 + 36)
     623   MahaRERA row  (578.8 + 44.2)
   -------------------------------------------------------------------------- */
.sec--footer .ftr__inner {
  padding-top: calc(80 * var(--u));
}

.sec--footer .ftr__cols {
  height: calc(338 * var(--u));
}

/* Figma column boxes and the gaps between them: brand 710 @ 0, logo 165 @ 766,
   EXPLORE 142 @ 1229, CONTACT 264 @ 1460 (all relative to the 112 margin). */
.sec--footer .ftr__brand   { width: calc(710 * var(--u)); }
.sec--footer .ftr__logo    { width: calc(165 * var(--u)); margin-left: calc(56 * var(--u)); }
.sec--footer .ftr__explore { width: calc(142 * var(--u)); margin-left: calc(298 * var(--u)); }
.sec--footer .ftr__contact { width: calc(264 * var(--u)); margin-left: calc(89 * var(--u)); }

/* --------------------------------------------------------------------------
   Brand block — Figma 175:4052, wordmark @ 0, tagline @ 84, CTA @ 159
   -------------------------------------------------------------------------- */

/* 64px Canela on a 79px text box — not a ratio on theme.css's .lh-d* scale. */
.sec--footer .ftr__wordmark {
  width: calc(452 * var(--u));
  line-height: calc(79 * var(--u));
}

/* 32px on a 38px box; #3A3A3A is darker than the --spv-ink body colour. */
.sec--footer .ftr__tagline {
  margin-top: calc(5 * var(--u));
  line-height: calc(38 * var(--u));
  color: var(--ftr-ink-strong);
}

.sec--footer .ftr__cta {
  margin-top: calc(37 * var(--u));
}

/* --------------------------------------------------------------------------
   Column headings — Canela CONDENSED Light 48 on a 58 box, gold
   -------------------------------------------------------------------------- */
.sec--footer .ftr__col-title {
  width: calc(142 * var(--u));
  line-height: calc(58 * var(--u));
  /* Figma uses Canela CONDENSED here; Cormorant Garamond has no condensed cut,
     so the substitute is narrowed to the exact Figma advance width
     (EXPLORE 202.67 -> 142, CONTACT 227.77 -> 148). */
  transform: scaleX(0.7006);
  transform-origin: left center;
}

.sec--footer .ftr__col-title--contact {
  width: calc(148 * var(--u));
  transform: scaleX(0.6498);
}

/* --------------------------------------------------------------------------
   EXPLORE — Figma 175:4060. 58 heading box + 14 gap puts the list on 72;
   five 36-tall rows on a 10 gap close the column at 292.
   -------------------------------------------------------------------------- */
.sec--footer .ftr__nav {
  margin-top: calc(14 * var(--u));
  gap: calc(10 * var(--u));
}

.sec--footer .ftr__nav-link {
  gap: calc(15 * var(--u));
}

/* Figma used a Font Awesome 6 Solid "angle-right" glyph (14px, gold) in a
   9x14 box; the drawn glyph measures 6x10 inside it. */
.sec--footer .ftr__nav-chev {
  width: calc(9 * var(--u));
  height: calc(14 * var(--u));
}

.sec--footer .ftr__nav-chev svg {
  display: block;
  width: calc(6 * var(--u));
  height: calc(10 * var(--u));
}

/* --------------------------------------------------------------------------
   CONTACT — Figma 175:4078. Same 58 + 14 heading offset; Figma's 17px row gaps
   over the 30 / 24 tall rows set the spacing inside the stack.
   -------------------------------------------------------------------------- */
/* The heading gap matches EXPLORE's 14, so both columns start their content on
   the same line. It was 94 — the +80 the signed-off render (baseline/footer.png)
   used to balance the column against the 338 logo lockup — but that read as a
   hole under the heading, so the columns are aligned to each other instead.
   The three rows themselves keep Figma's 17 gaps over the 30 / 24 tall rows. */
.sec--footer .ftr__info-list {
  margin-top: calc(14 * var(--u));
  gap: calc(17 * var(--u));
}

.sec--footer .ftr__info {
  gap: calc(12 * var(--u));
}

.sec--footer .ftr__info-icon {
  width: calc(18 * var(--u));
  height: calc(18 * var(--u));
}

/* Icon y-offsets inside each row, exactly as Figma parks them */
.sec--footer .ftr__info--phone .ftr__info-icon,
.sec--footer .ftr__info--address .ftr__info-icon {
  margin-top: calc(6 * var(--u));
}

.sec--footer .ftr__info--mail .ftr__info-icon {
  margin-top: calc(3 * var(--u));
}

/* The pin is a 13.5x16.36 drawing inside the 18x18 clip box */
.sec--footer .ftr__pin-img {
  left: calc(2.25 * var(--u));
  top: calc(1.5 * var(--u));
  width: calc(13.5 * var(--u));
  height: calc(16.36 * var(--u));
}

/* Figma declares a 190 measure and renders four lines; Inter is a touch wider
   than Haas Grot, so the measure is opened to 202 to reproduce Figma's exact
   break points ("... Navi Mumbai -" / "400706"). Still inside the 220 row. */
.sec--footer .ftr__address {
  width: calc(202 * var(--u));
}

/* --------------------------------------------------------------------------
   Social strip — Figma 175:4098 @ (112, 444): six 37.8 circles, 10.8 gap
   -------------------------------------------------------------------------- */
.sec--footer .ftr__social {
  margin-top: calc(26 * var(--u));
  gap: calc(10.8 * var(--u));
}

.sec--footer .ftr__social-item {
  width: calc(37.8 * var(--u));
  height: calc(37.8 * var(--u));
}

.sec--footer .ftr__social-link:hover {
  transform: translateY(calc(-4 * var(--u)));
  transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}

/* YouTube is the one composite: the ring is a separate Figma rectangle
   (1.08 stroke, #808080) and only the glyph is exported. .border draws it;
   these two variables give it the Figma stroke rather than the theme rule. */
.sec--footer .ftr__social-link--youtube {
  --bs-border-width: max(1px, calc(1.08 * var(--u)));
  --bs-border-color: var(--ftr-circle);
}

/* 8.49 / 8.68 from the ring's outer edge, less the 1.08 stroke that the
   absolute containing block already excludes. */
.sec--footer .ftr__yt-glyph {
  left: calc(7.41 * var(--u));
  top: calc(7.6 * var(--u));
  width: calc(21.907 * var(--u));
  height: calc(21.907 * var(--u));
}

.sec--footer .ftr__yt-img {
  left: calc(1.5626 * var(--u));
  top: calc(4.2006 * var(--u));
  width: calc(18.7829 * var(--u));
  height: calc(13.508 * var(--u));
}

/* --------------------------------------------------------------------------
   Legal strip — Figma 175:4121 @ (112, 518.8), 1723x23.
   Figma's three stops are 0 / 800 / 1514, which is neither an even
   distribution nor a flush-right credit block, so the flex row carries the
   Figma widths and the two gaps between them.
   -------------------------------------------------------------------------- */
.sec--footer .ftr__legal {
  height: calc(23 * var(--u));
  margin-top: calc(37 * var(--u));
}

.sec--footer .ftr__copyright {
  width: calc(423.974 * var(--u));
  margin-top: calc(2 * var(--u));
}

.sec--footer .ftr__privacy {
  width: calc(123.235 * var(--u));
  margin-top: calc(3 * var(--u));
  margin-left: calc(376.026 * var(--u)); /* 800 - 423.974 */
}

.sec--footer .ftr__credit {
  height: calc(22.581 * var(--u));
  margin-left: calc(590.765 * var(--u)); /* 1514 - (800 + 123.235) */
  gap: calc(12 * var(--u));
}

.sec--footer .ftr__credit-link {
  width: calc(77.209 * var(--u));
  height: calc(22.581 * var(--u));
}

/* The export is a hair wider than its Figma box, so it has to escape the
   global img { max-width: 100% }. */
.sec--footer .ftr__credit-logo {
  left: 0;
  top: calc(-0.0835 * var(--u));
  width: calc(77.2923 * var(--u));
  height: calc(22.664 * var(--u));
  max-width: none;
}

/* Figma "Vector 1": a 1692-long #4E4E50 @30% line. Hairline, so raw 1px. */
.sec--footer .ftr__rule {
  width: calc(1692 * var(--u));
  margin: calc(36 * var(--u)) 0 0;
  color: var(--ftr-rule);
}

/* --------------------------------------------------------------------------
   MahaRERA QR + disclaimer — Figma 175:4169 @ (112, 623), 1723x167
   -------------------------------------------------------------------------- */
.sec--footer .ftr__rera {
  margin-top: calc(44.2 * var(--u)); /* 623 - (577.8 + the 1px rule) */
}

.sec--footer .ftr__qr {
  width: calc(167 * var(--u));
  height: calc(167 * var(--u));
  border: 1px solid rgba(0, 0, 0, 0.1);
  box-shadow: 0 0 calc(10 * var(--u)) 0 rgba(0, 0, 0, 0.05);
}

.sec--footer .ftr__rera-copy {
  width: calc(1516 * var(--u));
  margin-left: calc(40 * var(--u)); /* 207 - 167 */
}

/* Figma pads the registration line with hard spaces instead of tracking. */
.sec--footer .ftr__rera-line {
  white-space: pre-wrap;
}

/* 16px on 29 leading inside a 1452 measure — both off the utility scales. */
.sec--footer .ftr__disclaimer {
  width: calc(1452 * var(--u));
  margin-top: calc(25 * var(--u));
  line-height: calc(29 * var(--u));
}

/* --------------------------------------------------------------------------
   Mobile — the fixed canvas and the pinned Figma offsets are dropped so the
   Bootstrap flow layout can reflow on its own. Wrapping is handled in the
   markup by .flex-lg-nowrap / .flex-column.flex-lg-row, not here.
   Below 992 --u is rebased to clientWidth/560, so the .fs-d* / gap values that
   stay in --u keep scaling; only the ones that come out wrong are restated.
   -------------------------------------------------------------------------- */
@media (max-width: 991.98px) {
  .sec--footer {
    height: auto;
  }

  /* The 112 design margin would eat 40% of a 430 frame. */
  .sec--footer .ftr__inner {
    --bs-gutter-x: 48px;
    padding-top: 48px;
    padding-bottom: 40px;
  }

  /* Both plates cover the section instead of their Figma boxes. */
  .sec--footer .ftr__palm,
  .sec--footer .ftr__paper {
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
  }

  .sec--footer .ftr__paper-img {
    top: 0;
    height: 100%;
  }

  /* The four Figma columns become a centred stack. */
  .sec--footer .ftr__cols {
    height: auto;
  }

  .sec--footer .ftr__brand,
  .sec--footer .ftr__logo,
  .sec--footer .ftr__explore,
  .sec--footer .ftr__contact {
    width: 100%;
    margin-left: 0;
  }

  .sec--footer .ftr__logo {
    width: 116px;
    height: 238px;
    margin: 40px auto 0;
  }

  .sec--footer .ftr__explore { margin-top: 40px; }
  .sec--footer .ftr__contact { margin-top: 32px; }

  .sec--footer .ftr__wordmark {
    width: auto;
    font-size: 38px;
    line-height: 1.2;
    white-space: normal;
  }

  .sec--footer .ftr__tagline {
    margin-top: 6px;
    font-size: 20px;
    line-height: 1.3;
  }

  .sec--footer .ftr__cta {
    margin-top: 24px;
    min-width: 200px;
    min-height: 54px;
    --bs-btn-font-size: 16px;
  }

  .sec--footer .ftr__col-title,
  .sec--footer .ftr__col-title--contact {
    width: auto;
    font-size: 34px;
    line-height: 1.2;
    transform: scaleX(0.78);
  }

  .sec--footer .ftr__nav {
    margin-top: 14px;
    gap: 8px;
  }

  .sec--footer .ftr__nav-link { gap: 12px; }
  .sec--footer .ftr__nav-chev { width: 9px; height: 14px; }
  .sec--footer .ftr__nav-chev svg { width: 6px; height: 10px; }

  .sec--footer .ftr__nav-label {
    font-size: 16px;
    line-height: 32px;
  }

  .sec--footer .ftr__info-list {
    margin-top: 14px;
    gap: 14px;
  }

  .sec--footer .ftr__info { gap: 12px; }

  .sec--footer .ftr__info-icon,
  .sec--footer .ftr__info--phone .ftr__info-icon,
  .sec--footer .ftr__info--mail .ftr__info-icon,
  .sec--footer .ftr__info--address .ftr__info-icon {
    width: 18px;
    height: 18px;
    margin-top: 4px;
  }

  .sec--footer .ftr__pin-img {
    left: 2px;
    top: 1px;
    width: 14px;
    height: 17px;
  }

  .sec--footer .ftr__info-text {
    font-size: 16px;
    line-height: 26px;
    white-space: normal;
  }

  .sec--footer .ftr__address {
    width: auto;
    max-width: 260px;
  }

  .sec--footer .ftr__social {
    margin-top: 36px;
    gap: 11px;
  }

  .sec--footer .ftr__social-item {
    width: 38px;
    height: 38px;
  }

  .sec--footer .ftr__social-link--youtube {
    --bs-border-width: 1px;
  }

  .sec--footer .ftr__yt-glyph {
    left: 8px;
    top: 8px;
    width: 22px;
    height: 22px;
  }

  .sec--footer .ftr__yt-img {
    left: 2px;
    top: 4px;
    width: 19px;
    height: 14px;
  }

  /* The legal row stacks; the Figma stops go with the desktop frame. */
  .sec--footer .ftr__legal {
    height: auto;
    margin-top: 32px;
    gap: 10px;
  }

  .sec--footer .ftr__copyright,
  .sec--footer .ftr__privacy,
  .sec--footer .ftr__credit-label {
    width: auto;
    margin-top: 0;
    margin-left: 0;
    font-size: 13px;
  }

  .sec--footer .ftr__credit {
    height: auto;
    margin-left: 0;
    gap: 10px;
  }

  .sec--footer .ftr__credit-link,
  .sec--footer .ftr__credit-logo {
    width: 72px;
    height: 21px;
  }

  .sec--footer .ftr__credit-logo { top: 0; }

  .sec--footer .ftr__rule {
    width: 100%;
    margin: 24px 0 0;
  }

  /* The QR + copy row stacks instead of overrunning the frame. */
  .sec--footer .ftr__rera {
    margin-top: 24px;
  }

  .sec--footer .ftr__qr {
    width: 128px;
    height: 128px;
  }

  .sec--footer .ftr__rera-copy {
    width: 100%;
    margin-top: 18px;
    margin-left: 0;
  }

  .sec--footer .ftr__rera-line {
    font-size: 12px;
    line-height: 1.5;
    white-space: normal;
  }

  .sec--footer .ftr__disclaimer {
    width: auto;
    margin-top: 14px;
    font-size: 12px;
    line-height: 1.75;
  }
}



/* ==========================================================================
   Pinned sections on viewports taller than 16:9
   The design frame is 1920x1080, so a 1080-unit section is SHORTER than the
   screen at 1440x900 or 1280x900 (u = 0.75 / 0.667). While ScrollTrigger pins
   #residences or #gallery, the strip below the section shows the flat body
   background — a visible seam under the textured plate. Letting the two pinned
   sections fill the viewport keeps their own paper plate (1782u tall, so it
   reaches ~1500u) under the whole screen for the length of the pin. Their
   content is positioned from the top, so the extra height only adds paper at
   the bottom, never moving anything.
   ========================================================================== */
@media (min-width: 992px) {
  .sec--residences,
  .sec--gallery {
    min-height: 100vh;
  }

  /* The Residences paper plate scrolls WITH the horizontal track, so the track
     has to grow with the section too or the extra height shows flat cream. The
     plate inside it starts at -273u and runs 1782u, so it still covers. */
  .sec--residences .res__track {
    min-height: 100vh;
  }
}

/* ==========================================================================
   Mobile menu — hamburger + full-width Bootstrap offcanvas (d-lg-none)
   ========================================================================== */
.spv-burger {
  width: 48px;
  height: 48px;
  margin: 18px 16px 0 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.2);
  box-shadow: inset 0 0 0 1.5px var(--spv-gold-line);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  z-index: 1046; /* above Bootstrap's offcanvas (1045) so it can close it too */
  transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}

.spv-burger__line {
  width: 20px;
  height: 1.5px;
  background: var(--spv-gold);
  transform-origin: center;
  transition: transform 0.45s cubic-bezier(0.77, 0, 0.18, 1), opacity 0.3s ease;
}

/* The trigger morphs into a cross while the menu is open. */
.spv-burger[aria-expanded="true"] .spv-burger__line:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.spv-burger[aria-expanded="true"] .spv-burger__line:nth-child(2) { opacity: 0; transform: scaleX(0); }
.spv-burger[aria-expanded="true"] .spv-burger__line:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

.offcanvas.spv-offcanvas {
  --bs-offcanvas-width: 100%;
  --bs-offcanvas-bg: var(--spv-cream);
  --bs-offcanvas-color: var(--spv-ink);
  --bs-offcanvas-border-width: 0;
  --bs-offcanvas-padding-x: 24px;
  --bs-offcanvas-padding-y: 20px;
  /* Bootstrap's own is 0.3s ease-in-out; a longer, eased slide reads better
     at full width. Same curve on the way out. */
  transition: transform 0.75s cubic-bezier(0.77, 0, 0.18, 1);
}

/* Paper texture wash, same plate the sections use. */
.offcanvas.spv-offcanvas::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("../assets/img/gallery-paper-plate.webp") center / cover no-repeat;
  opacity: 0.35;
  pointer-events: none;
}

.spv-offcanvas__header,
.spv-offcanvas__body {
  position: relative; /* above the ::before wash */
}

.spv-offcanvas__header {
  min-height: 84px;
  border-bottom: 1px solid rgba(169, 131, 74, 0.25);
}

.spv-offcanvas__brand img {
  width: 34px;
  height: auto;
}

.spv-offcanvas__close {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--spv-gold-line);
  /* Hidden: the fixed hamburger (now a cross) is the close control; this one
     stays in the DOM for assistive tech and keyboard dismissal. */
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.spv-offcanvas__body {
  padding-top: 28px;
  padding-bottom: 32px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}

.spv-menu__item {
  border-bottom: 1px solid rgba(169, 131, 74, 0.22);
  /* Staggered rise as the panel opens. */
  opacity: 0;
  transform: translateY(18px);
}

.offcanvas.spv-offcanvas.show .spv-menu__item {
  animation: spv-menu-in 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
  animation-delay: calc(180ms + var(--i) * 65ms);
}

@keyframes spv-menu-in {
  to { opacity: 1; transform: translateY(0); }
}

.spv-menu__link {
  gap: 14px;
  padding: 18px 0;
  color: var(--spv-ink);
  transition: color 0.3s ease, padding-left 0.3s ease;
}

.spv-menu__link:active,
.spv-menu__link:hover {
  color: var(--spv-gold);
  padding-left: 6px;
}

.spv-menu__num {
  font-size: 11px;
  letter-spacing: 0.12em;
  min-width: 22px;
}

.spv-menu__label {
  font-size: 34px;
  line-height: 1.1;
}

.spv-offcanvas__eyebrow {
  font-size: 11px;
  letter-spacing: 0.16em;
}

.spv-offcanvas__contact {
  color: var(--spv-ink);
  font-size: 30px;
  line-height: 1.2;
}

.spv-offcanvas__contact--mail {
  font-size: 14px;
  margin-top: 6px;
  color: var(--spv-gold);
}

@media (prefers-reduced-motion: reduce) {
  .offcanvas.spv-offcanvas { transition: none; }
  .spv-menu__item { opacity: 1; transform: none; animation: none !important; }
  .spv-burger__line { transition: none; }
}

/* Touch devices run without Lenis (see js/main.js), so in-page links use the
   browser's own smooth scrolling. */
@media (hover: none) and (pointer: coarse) {
  html {
    scroll-behavior: smooth;
  }
}

/* ===== enquiry modal ===== */
/* ==========================================================================
   Sai Palm View — enquiry modal

   A stock Bootstrap 5.3 modal + form, re-skinned through Bootstrap's own
   component variables so the markup in index.php stays vanilla (.modal,
   .modal-dialog-centered, .row.g-0, .form-control, .input-group, .form-check,
   .invalid-feedback, .was-validated). Nothing here changes Bootstrap's
   behaviour — only its colours, type and rules.

   Sized in px rather than --u: this is an overlay, not part of the 1920 Figma
   canvas, so it should stay legible at every width the way .spv-offcanvas does.
   ========================================================================== */

.spv-modal {
  /* One column since the render aside came out; 840 was the two-up width. */
  --bs-modal-width: 480px;
  --bs-modal-bg: var(--spv-cream);
  --bs-modal-color: var(--spv-ink);
  --bs-modal-border-width: 0;
  --bs-modal-border-radius: 0;
  --bs-modal-padding: 0;
  --bs-modal-margin: 12px;
  --bs-modal-box-shadow: 0 40px 90px rgba(28, 22, 12, 0.38);
}

/* Bootstrap appends .modal-backdrop to <body>, so it is a SIBLING of .modal and
   never sees variables set on .spv-modal — it has to be retuned on its own. */
.modal-backdrop {
  --bs-backdrop-bg: #17140f;
  --bs-backdrop-opacity: 0.74;
}

.spv-modal .modal-content {
  box-shadow: var(--bs-modal-box-shadow);
}

/* A slightly longer, eased rise — the stock 0.3s translate reads abrupt at
   this size. Bootstrap still owns the .show class that drives it. */
.spv-modal.fade .modal-dialog {
  transform: translateY(34px) scale(0.985);
  transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.6s ease;
  opacity: 0;
}

.spv-modal.show .modal-dialog {
  transform: none;
  opacity: 1;
}

/* --------------------------------------------------------------------------
   Close control — the offcanvas cross, in gold on the photo
   -------------------------------------------------------------------------- */
.spv-modal__close {
  width: 44px;
  height: 44px;
  background: transparent;
  color: var(--spv-gold-line);
  transition: transform 0.4s ease, color 0.3s ease;
}

.spv-modal__close:hover {
  transform: rotate(90deg);
  color: var(--spv-gold);
}

.spv-modal__close-x {
  position: relative;
  width: 18px;
  height: 18px;
  margin: 0 auto;
}

.spv-modal__close-x::before,
.spv-modal__close-x::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  height: 1.5px;
  background: currentColor;
}

.spv-modal__close-x::before { transform: rotate(45deg); }
.spv-modal__close-x::after  { transform: rotate(-45deg); }

/* --------------------------------------------------------------------------
   Panel — the form side
   -------------------------------------------------------------------------- */
/* Flex column + a growing body, so the body spans the dialog's full height
   rather than the form's own — without it the short success panel would sit
   against the top. The hairline is what is left of the render: it was the
   mobile stand-in for the aside, and now the aside is gone at every width. */
.spv-modal__panel {
  display: flex;
  flex-direction: column;
  background: var(--spv-cream);
  border-top: 3px solid var(--spv-gold-line);
}

/* The same paper plate the sections and the offcanvas use. */
.spv-modal__panel::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("../assets/img/gallery-paper-plate.webp") center / cover no-repeat;
  opacity: 0.3;
  pointer-events: none;
  z-index: 1;
}

/* Flex-column so the short success panel centres in the dialog; with the form
   in place the body is what sets the height, so this is a no-op. */
.spv-modal .modal-body {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 30px 34px 32px;
}

.spv-modal__title {
  font-size: 30px;
  line-height: 1.08;
  color: var(--spv-ink);
}

/* .ff-script lowercases; the script face needs a size bump to sit on the
   same optical line as the Cormorant it is set beside. */
.spv-modal__title .ff-script {
  font-size: 1.32em;
  line-height: 0.8;
}

.spv-modal__lede {
  margin-top: 9px;
  max-width: 42ch;
  font-size: 13px;
  line-height: 1.55;
  color: rgba(89, 90, 92, 0.86);
}

/* --------------------------------------------------------------------------
   Fields — Bootstrap form controls as hairline-underlined inputs
   -------------------------------------------------------------------------- */
.spv-form {
  margin-top: 18px;
  /* Tighter than Bootstrap's g-3 row gutter, which is set in the markup. */
  --bs-gutter-y: 0.7rem;
}

.spv-form .form-label {
  margin-bottom: 4px;
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--spv-gold);
}

.spv-form__optional {
  text-transform: none;
  letter-spacing: 0.02em;
  color: rgba(89, 90, 92, 0.6);
}

.spv-form .form-control {
  --bs-body-bg: transparent;
  padding: 7px 2px;
  border: 0;
  border-bottom: 1px solid rgba(169, 131, 74, 0.34);
  border-radius: 0;
  background-color: transparent;
  font-family: var(--spv-font-sans);
  font-size: 14px;
  line-height: 1.4;
  color: var(--spv-ink);
  box-shadow: none;
  transition: border-color 0.3s ease, background-color 0.3s ease;
}

.spv-form .form-control:focus {
  border-bottom-color: var(--spv-gold);
  background-color: rgba(169, 131, 74, 0.05);
  box-shadow: 0 1px 0 0 var(--spv-gold);
  color: var(--spv-ink);
}

.spv-form .form-control::placeholder {
  color: rgba(89, 90, 92, 0.42);
}

.spv-form textarea.form-control {
  resize: none;
  min-height: 62px;
}

/* The +91 prefix — a rule-matched sibling, not a filled pill. */
.spv-form .input-group-text {
  padding: 7px 8px 7px 0;
  border: 0;
  border-bottom: 1px solid rgba(169, 131, 74, 0.34);
  border-radius: 0;
  background: transparent;
  font-size: 14px;
  color: var(--spv-gold);
}

.spv-form .input-group:focus-within .input-group-text {
  border-bottom-color: var(--spv-gold);
}

/* Autofill paints Chrome's own blue; hold the cream. */
.spv-form .form-control:-webkit-autofill,
.spv-form .form-control:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--spv-ink);
  -webkit-box-shadow: 0 0 0 1000px var(--spv-cream) inset;
}

/* Consent checkbox */
.spv-form .form-check {
  padding-left: 1.7em;
}

.spv-form .form-check-input {
  width: 1.05em;
  height: 1.05em;
  margin-left: -1.7em;
  margin-top: 0.18em;
  border: 1px solid var(--spv-gold-line);
  border-radius: 0;
  background-color: transparent;
  box-shadow: none;
}

.spv-form .form-check-input:checked {
  background-color: var(--spv-gold);
  border-color: var(--spv-gold);
}

.spv-form .form-check-input:focus {
  border-color: var(--spv-gold);
  box-shadow: 0 0 0 3px var(--bs-focus-ring-color);
}

.spv-form .form-check-label {
  font-size: 11.5px;
  font-weight: 300;
  line-height: 1.45;
  color: rgba(89, 90, 92, 0.82);
}

/* --------------------------------------------------------------------------
   Validation — Bootstrap's .was-validated states, in the brand's register.
   .was-validated lands on .spv-form itself (main.js adds it on submit), so the
   overrides below are written as .spv-form.was-validated to outrank Bootstrap's
   own two-class .was-validated .form-control:state rules.
   -------------------------------------------------------------------------- */
.spv-form {
  --bs-form-invalid-color: #9d3b2f;
  --bs-form-invalid-border-color: #9d3b2f;
  --bs-form-valid-color: var(--spv-gold);
  --bs-form-valid-border-color: rgba(169, 131, 74, 0.6);
}

/* Bootstrap paints a tick / cross INSIDE the field and reserves ~4rem of
   padding for it. That reads as clutter on hairline inputs and shoves the
   value off-centre, so the border and the message carry the state instead. */
.spv-form.was-validated .form-control:valid,
.spv-form.was-validated .form-control:invalid,
.spv-form.was-validated textarea.form-control:valid,
.spv-form.was-validated textarea.form-control:invalid {
  background-image: none;
  padding-right: 2px;
}

/* Bootstrap's state focus ring is success-green / danger-red; keep the gold. */
.spv-form.was-validated .form-control:valid:focus,
.spv-form.was-validated .form-check-input:valid:focus {
  border-color: var(--spv-gold);
  box-shadow: 0 1px 0 0 var(--spv-gold);
}

.spv-form.was-validated .form-control:invalid:focus,
.spv-form.was-validated .form-check-input:invalid:focus {
  box-shadow: 0 1px 0 0 var(--bs-form-invalid-border-color);
}

/* Only the underline changes colour — the top/side borders are already 0.
   The +91 prefix is a sibling of the input, so it has to be matched separately
   or its rule stays gold while the field's turns red. */
.spv-form.was-validated .form-control:valid,
.spv-form.was-validated .input-group:has(:valid) .input-group-text {
  border-bottom-color: var(--bs-form-valid-border-color);
}

.spv-form.was-validated .form-control:invalid,
.spv-form.was-validated .input-group:has(:invalid) .input-group-text {
  border-bottom-color: var(--bs-form-invalid-border-color);
}

/* The consent row: a gold tick when accepted, brick when not. */
.spv-form.was-validated .form-check-input:valid:checked {
  background-color: var(--spv-gold);
  border-color: var(--spv-gold);
}

.spv-form.was-validated .form-check-input:valid ~ .form-check-label {
  color: rgba(89, 90, 92, 0.82);
}

/* The input-group wraps the field, so the feedback has to clear the whole row. */
.spv-form .input-group .invalid-feedback {
  flex-basis: 100%;
}

.spv-form .invalid-feedback {
  margin-top: 5px;
  font-family: var(--spv-font-sans);
  font-size: 11.5px;
  line-height: 1.4;
}

/* Honeypot — off-screen rather than display:none, which some bots skip. */
.spv-form__trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* --------------------------------------------------------------------------
   Actions
   -------------------------------------------------------------------------- */
/* Sits on top of the row's own 0.7rem gutter, so the submit is held clear of
   the consent block rather than reading as the next field in the stack. */
.spv-form__actions {
  margin-top: 18px;
}

/* .btn-spv sizes itself in --u for the page canvas; the modal pins it. */
.spv-modal .btn-spv {
  --bs-btn-font-size: 13px;
  --bs-btn-padding-x: 24px;
  --bs-btn-padding-y: 11px;
  --bs-btn-border-width: 1.5px;
  min-width: 176px;
  min-height: 46px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.spv-form__submit:disabled {
  opacity: 0.6;
}

/* --------------------------------------------------------------------------
   Success state
   -------------------------------------------------------------------------- */
.spv-modal__done {
  /* Extra top padding stands in for the intro block that showDone() removes,
     so the panel does not jump to a cramped height. */
  padding: 34px 0 26px;
}

.spv-modal__done:focus {
  outline: none;
}

.spv-modal__done-mark {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  border: 1px solid var(--spv-gold-line);
  color: var(--spv-gold);
}

.spv-modal__done-mark svg {
  width: 25px;
  height: 25px;
}

.spv-modal__done-title {
  margin-top: 16px;
  font-size: 30px;
  line-height: 1.1;
  color: var(--spv-ink);
}

.spv-modal__done-title .ff-script {
  font-size: 1.32em;
  line-height: 0.8;
}

/* The script face descends well below its box; the copy needs to clear it. */
.spv-modal__done-copy {
  margin-top: 16px;
  font-size: 13px;
  line-height: 1.6;
  color: rgba(89, 90, 92, 0.86);
}

.spv-modal__done .btn-spv {
  margin-top: 20px;
}

/* The offcanvas foot CTA, matched to the same button metrics. */
.spv-offcanvas__cta {
  --bs-btn-font-size: 13px;
  --bs-btn-padding-y: 13px;
  min-width: 0;
  min-height: 50px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* --------------------------------------------------------------------------
   Below lg — the aside is dropped and the panel becomes the whole sheet
   -------------------------------------------------------------------------- */
@media (max-width: 991.98px) {
  .spv-modal {
    --bs-modal-margin: 10px;
  }

  .spv-modal .modal-body {
    padding: 26px 20px 26px;
  }

  .spv-modal__title {
    font-size: 27px;
  }

  .spv-modal__lede {
    font-size: 12.8px;
  }

  .spv-modal .btn-spv {
    width: 100%;
  }

  .spv-modal__done-title {
    font-size: 26px;
  }
}

@media (max-width: 575.98px) {
  .spv-modal .modal-body {
    padding: 24px 18px 24px;
  }

  .spv-modal__title {
    font-size: 24px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .spv-modal.fade .modal-dialog { transition: none; transform: none; }
  .spv-modal__close { transition: none; }
  .spv-modal__close:hover { transform: none; }
}

/* ===== submit loader ===== */
/* ==========================================================================
 SUBMIT LOADER — #spvLoader

 A full-bleed cream veil that fades in once the enquiry validates and stays up
 through the POST to thank-you.php. It is painted above the modal (1055) and
 its backdrop (1050), so the form dissolves into it rather than sitting under
 it. Hidden by default in both senses — opacity and visibility — so it is out
 of the tab order and out of the accessibility tree until main.js arms it.
 ========================================================================== */
.spv-loader {
  z-index: 2000;
  background: var(--spv-cream);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.5s ease, visibility 0s linear 0.5s;
}

/* The same stucco plate the sections and the thank-you page wear, so the veil
   reads as the site holding its breath rather than a blank browser screen. */
.spv-loader::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("../assets/img/gallery-paper-plate.webp") center / cover no-repeat;
  opacity: 0.38;
  pointer-events: none;
}

.spv-loader.is-on {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.5s ease;
}

/* The mark rises a beat after the veil has arrived, so the two reads are
   sequential: the page goes quiet, then the mark answers. */
.spv-loader__inner {
  z-index: 1;
}

.spv-loader.is-on .spv-loader__inner {
  animation: spv-loader-in 0.7s cubic-bezier(0.22, 1, 0.36, 1) 0.12s both;
}

/* Gold hairline ring around the wordmark — the same atom as .ty-mark, with a
   two-tone arc turning inside it. */
.spv-loader__ring {
  width: 104px;
  height: 104px;
  border: 1px solid var(--spv-rule);
}

.spv-loader__ring::before {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: 50%;
  border: 1px solid transparent;
  border-top-color: var(--spv-gold);
  border-right-color: rgba(207, 168, 84, 0.55);
  animation: spv-loader-spin 1.15s linear infinite;
}

/* The slow halo that breathes outward, borrowed from .ty-ring. */
.spv-loader__ring::after {
  content: "";
  position: absolute;
  inset: -14px;
  border-radius: 50%;
  border: 1px solid rgba(207, 168, 84, 0.4);
  animation: spv-loader-halo 2.6s ease-out infinite;
}

.spv-loader__logo {
  width: 42px;
  height: auto;
}

.spv-loader__text {
  margin-top: 26px;
  font-size: 11.5px;
  letter-spacing: 0.26em;
  color: var(--spv-gold);
}

/* A 1px gold thread with a lit segment sweeping across it — progress without
   pretending to know a percentage. */
.spv-loader__bar {
  width: 128px;
  height: 1px;
  margin: 18px auto 0;
  background: rgba(169, 131, 74, 0.2);
}

.spv-loader__bar::after {
  content: "";
  display: block;
  width: 42%;
  height: 100%;
  background: var(--spv-gold);
  animation: spv-loader-sweep 1.3s ease-in-out infinite;
}

/* Nothing scrolls behind the veil. */
html.spv-loading,
html.spv-loading body {
  overflow: hidden;
}

@keyframes spv-loader-in {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
}

@keyframes spv-loader-spin {
  to { transform: rotate(360deg); }
}

@keyframes spv-loader-halo {
  0% {
    transform: scale(0.88);
    opacity: 0;
  }

  35% { opacity: 1; }

  100% {
    transform: scale(1.2);
    opacity: 0;
  }
}

@keyframes spv-loader-sweep {
  0% { transform: translateX(-110%); }
  100% { transform: translateX(245%); }
}

/* Reduced motion keeps the veil — it is the message — and drops the movement,
   leaving the arc as a static mark. */
@media (prefers-reduced-motion: reduce) {
  .spv-loader,
  .spv-loader.is-on { transition: opacity 0.01s linear; }

  .spv-loader.is-on .spv-loader__inner,
  .spv-loader__ring::before,
  .spv-loader__ring::after,
  .spv-loader__bar::after {
    animation: none;
  }
}

/* ===== fixed CTAs ===== */
/* ==========================================================================
   FIXED CTAs — the two pinned buttons (index.php, "FIXED CTAs").

   Desktop keeps them as two squared buttons in the bottom corners; below
   992px they become one full-width bar split down the middle. Bootstrap's
   display utilities do the swap, so everything here is skin and motion.

   Colour comes straight off the page tokens: solid --spv-gold with a
   --spv-gold-line hairline and --spv-cream type, which is the one pairing
   that stays legible over every plate the buttons float above (cream paper,
   the night tower, the gallery photography).

   z-index sits at 1030 — above the page, below Bootstrap's offcanvas (1045),
   backdrop (1050) and modal (1055), so opening a sheet covers them.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Desktop — bottom-left brochure, bottom-right enquiry
   -------------------------------------------------------------------------- */
.spv-fixed-btn {
  position: fixed;
  bottom: 24px;
  z-index: 1030;
  gap: 9px;
  padding: 13px 22px;
  border: 1px solid var(--spv-gold-line);
  border-radius: 0;
  background-color: var(--spv-gold);
  color: var(--spv-cream);
  font-family: var(--spv-font-sans);
  font-size: clamp(11px, 0.68vw, 13px);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  overflow: hidden;
  box-shadow: 0 6px 22px rgba(89, 90, 92, 0.3);
  transition:
    transform 0.35s cubic-bezier(0.4, 0, 0.2, 1),
    box-shadow 0.35s ease,
    background-color 0.3s ease,
    border-color 0.3s ease;
  /* Slide up into place once the page has settled rather than on first paint. */
  animation: spvFixedBtnIn 0.7s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.spv-fixed-btn--brochure { left: 15px;  animation-delay: 0.5s; }
.spv-fixed-btn--enquire  { right: 15px; animation-delay: 0.65s; }

/* A light sweeps across each button on a slow cycle to draw the eye back. */
.spv-fixed-btn::before {
  content: "";
  position: absolute;
  top: 0;
  left: -120%;
  width: 55%;
  height: 100%;
  background: linear-gradient(120deg, transparent, rgba(255, 253, 237, 0.42), transparent);
  transform: skewX(-20deg);
  pointer-events: none;
  animation: spvFixedBtnShine 5s ease-in-out 2s infinite;
}

/* Offset so the two buttons never sweep together. */
.spv-fixed-btn--enquire::before { animation-delay: 3.5s; }

.spv-fixed-btn:hover,
.spv-fixed-btn:focus-visible {
  background-color: var(--spv-gold-line);
  border-color: var(--spv-gold-line);
  color: var(--spv-cream);
  transform: translateY(-3px);
  box-shadow: 0 14px 32px rgba(89, 90, 92, 0.4);
}

.spv-fixed-btn:active { transform: translateY(-1px); }

/* --------------------------------------------------------------------------
   Mobile / tablet — one bar, split in two
   -------------------------------------------------------------------------- */
.spv-fixed-bar {
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 1030;
  background-color: var(--spv-gold);
  border-top: 1px solid var(--spv-gold-line);
  box-shadow: 0 -6px 22px rgba(89, 90, 92, 0.24);
  /* The inset keeps the row clear of the iOS home indicator. */
  padding: 9px 0 calc(9px + env(safe-area-inset-bottom, 0px));
  animation: spvFixedBarIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.4s both;
}

.spv-fixed-bar__col { flex: 1 1 50%; min-width: 0; }

/* The hairline between the halves, as on the desktop buttons' border. */
.spv-fixed-bar__col--rule { border-right: 1px solid rgba(255, 253, 237, 0.35); }

.spv-fixed-bar__btn {
  gap: 8px;
  padding: 5px 6px;
  color: var(--spv-cream);
  font-family: var(--spv-font-sans);
  font-size: clamp(11px, 3.1vw, 14px);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.4px;
  white-space: nowrap;
  cursor: pointer;
}

.spv-fixed-bar__btn:active { opacity: 0.75; }

/* The bar covers the foot of the page, so give the document the height back. */
@media (max-width: 991.98px) {
  body { padding-bottom: calc(52px + env(safe-area-inset-bottom, 0px)); }
}

/* --------------------------------------------------------------------------
   Icons — shared by both layouts
   -------------------------------------------------------------------------- */
.spv-fixed__icon {
  flex-shrink: 0;
  width: 1.15em;
  height: 1.15em;
  transition: transform 0.35s ease;
}

/* Nudge each icon the way its button reads. */
.spv-fixed-btn--brochure:hover .spv-fixed__icon { transform: translateY(2px); }
.spv-fixed-btn--enquire:hover  .spv-fixed__icon { transform: rotate(-12deg); }

@keyframes spvFixedBtnIn {
  from { opacity: 0; transform: translateY(26px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes spvFixedBarIn {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}

@keyframes spvFixedBtnShine {
  0%, 82% { left: -120%; }
  100%    { left: 130%; }
}

@media (prefers-reduced-motion: reduce) {

  .spv-fixed-btn,
  .spv-fixed-btn::before,
  .spv-fixed-bar {
    animation: none;
  }

  .spv-fixed-btn::before { display: none; }

  .spv-fixed-btn:hover,
  .spv-fixed-btn:focus-visible { transform: none; }
}

/* ===== page: thank-you ===== */
/* ==========================================================================
 Sai Palm View — thank-you.php

 A separate page from the landing page, but it loads the same vendor.css +
 style.css pair so it inherits the palette, the font stacks and .btn-spv.
 Only this page's own geometry lives here; everything above is index.php's.
 ========================================================================== */

/* The landing page scales type by --u against a 1920 canvas. This page is a
 single centred card, so it sizes in px/clamp and pins --u to 1px, which
 keeps the shared atoms (.btn-spv) from collapsing. */
.ty-page {
  --u: 1px;
  min-height: 100svh;
  background: var(--spv-cream);
  /* Arrives out of #spvLoader's cream veil: same background, so only the card
     crossfades in and the hand-off reads as one continuous screen. */
  animation: ty-in 0.55s ease both;
}

@keyframes ty-in {
  from { opacity: 0; }
}

/* The stucco plate the sections use, at the same 69% wash. */
.ty-page::before {
  content: "";
  position: fixed;
  inset: 0;
  background: url("../assets/img/gallery-paper-plate.webp") center / cover no-repeat;
  opacity: 0.38;
  pointer-events: none;
  z-index: 0;
}

/* The ghosted palm illustration from the footer, anchored to the foot. The
 plate has a pale top edge that would otherwise draw a hard horizontal seam
 across the page, so it is masked out over its top third. */
.ty-palm {
  position: fixed;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 52vh;
  background: url("../assets/img/footer-palm-plate.webp") center bottom / cover no-repeat;
  opacity: 0.16;
  pointer-events: none;
  z-index: 0;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 38%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 38%);
}

.ty-shell {
  position: relative;
  z-index: 1;
}

.ty-head {
  padding: 22px 20px;
}

.ty-head img {
  width: 46px;
  height: auto;
  margin: 0 auto;
}

.ty-main {
  padding: clamp(24px, 5vw, 64px) 20px clamp(48px, 7vw, 88px);
}

.ty-card {
  width: min(100%, 620px);
}

/* Gold hairline ring + tick, the same atom as the modal's success mark. */
.ty-mark {
  width: clamp(78px, 9vw, 96px);
  height: clamp(78px, 9vw, 96px);
  border: 1px solid var(--spv-gold-line);
  color: var(--spv-gold);
  animation: ty-pop 0.7s cubic-bezier(0.22, 1, 0.36, 1) 0.1s both;
}

.ty-mark svg {
  width: 38%;
  height: 38%;
}

.ty-ring {
  inset: -13px;
  border: 1px solid rgba(207, 168, 84, 0.5);
  animation: ty-ring 2.8s ease-out 0.6s infinite;
}

@keyframes ty-pop {
  from {
    opacity: 0;
    transform: scale(0.72);
  }
}

@keyframes ty-ring {
  0% {
    transform: scale(0.86);
    opacity: 0;
  }

  35% {
    opacity: 1;
  }

  100% {
    transform: scale(1.22);
    opacity: 0;
  }
}

.ty-eyebrow {
  font-size: 11.5px;
  letter-spacing: 0.24em;
  animation: ty-up 0.8s ease-out 0.2s both;
}

.ty-title {
  margin-top: 14px;
  font-size: clamp(38px, 5.4vw, 62px);
  line-height: 1.06;
  color: var(--spv-ink);
  animation: ty-up 0.8s ease-out 0.28s both;
}

/* .ff-script runs small and low against the Cormorant it sits beside. */
.ty-title .ff-script {
  font-size: 1.3em;
  line-height: 0.8;
}

/* Clears the script 'you', which descends well below its own box. */
.ty-name {
  display: block;
  margin-top: 18px;
  font-size: 0.42em;
  letter-spacing: 0.04em;
  color: var(--spv-gold);
}

.ty-copy {
  margin: clamp(20px, 2.6vw, 30px) auto 0;
  max-width: 44ch;
  font-size: clamp(14px, 1.1vw, 16px);
  line-height: 1.68;
  color: rgba(89, 90, 92, 0.86);
  animation: ty-up 0.8s ease-out 0.4s both;
}

.ty-rule {
  width: 64px;
  height: 1px;
  margin: clamp(26px, 3vw, 34px) auto;
  background: var(--spv-gold-line);
  opacity: 0.6;
  animation: ty-up 0.8s ease-out 0.5s both;
}

.ty-actions {
  gap: 14px;
  animation: ty-up 0.8s ease-out 0.56s both;
}

/* The brochure button leads, so it is filled rather than outlined — the same
   gold-on-cream pairing the fixed CTAs wear. */
.ty-btn--solid {
  --bs-btn-color: var(--spv-cream);
  --bs-btn-bg: var(--spv-gold);
  --bs-btn-border-color: var(--spv-gold);
  --bs-btn-hover-color: var(--spv-cream);
  --bs-btn-hover-bg: var(--spv-gold-line);
  --bs-btn-hover-border-color: var(--spv-gold-line);
}

.ty-page .btn-spv {
  --bs-btn-font-size: 14px;
  --bs-btn-padding-x: 28px;
  --bs-btn-padding-y: 13px;
  --bs-btn-border-width: 1.5px;
  min-width: 200px;
  min-height: 52px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.ty-contact {
  margin-top: clamp(22px, 2.6vw, 30px);
  font-size: 13px;
  color: rgba(89, 90, 92, 0.72);
  animation: ty-up 0.8s ease-out 0.64s both;
}

.ty-contact a {
  color: var(--spv-gold);
}

.ty-foot {
  padding: 18px 20px;
  border-top: 1px solid rgba(169, 131, 74, 0.22);
  font-size: 12px;
  color: rgba(89, 90, 92, 0.7);
}

@keyframes ty-up {
  from {
    opacity: 0;
    transform: translateY(18px);
  }
}

@media (prefers-reduced-motion: reduce) {

  .ty-page,
  .ty-mark,
  .ty-ring,
  .ty-eyebrow,
  .ty-title,
  .ty-copy,
  .ty-rule,
  .ty-actions,
  .ty-contact {
    animation: none;
  }
}
