/* ============================================================
   KLCC Investor - klccinvestor.com. Started as a standalone
   structural/motion scaffold (originally under a "shelto-scaffold"
   folder name, referencing a Webflow template's layout/spacing/
   interaction patterns - CSS authored from scratch, not copied from
   its source) and has since become the live site itself. Some
   placeholder Unsplash imagery + filler copy may still remain from
   that scaffold phase; swap section by section as real content lands.
   ============================================================ */

/* Inlined from lenis@1.1.20/dist/lenis.css (was an unpkg <link>, folded in
   here so the file has no external stylesheet dependency). */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: clip; }
.lenis.lenis-smooth iframe { pointer-events: none; }

@font-face {
  font-family: 'General Sans';
  src: url('fonts/general-sans-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'General Sans';
  src: url('fonts/general-sans-500.woff2') format('woff2');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'General Sans';
  src: url('fonts/general-sans-600.woff2') format('woff2');
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'General Sans';
  src: url('fonts/general-sans-700.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Corrected from live-render inspection: this is a LIGHT site (cream/
     off-white surfaces, near-black text), not dark - black is reserved for
     the floating nav pill, buttons, and the hero's own photographic tone. */
  --black: #0a0a0a;
  --black-soft: #171717;
  --black-elevated: #1f1f1f;
  --white: #ffffff;
  --cream: #f2efe9;
  --cream-soft: #fafafa;
  --ink: #171717;
  --gray-300: #d4d4d4;
  --gray-400: #6b6b6b;
  --gray-500: #5c5c5c;
  --gray-600: #525252;
  --gray-700: #404040;

  --font: 'General Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;

  --container: min(100% - 3rem, 78rem);
  --radius-sm: 0.75rem;
  --radius-md: 1rem;
  --radius-full: 62.75rem;

  --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
  --duration-fast: 150ms;
  --duration-base: 300ms;
  --duration-slow: 500ms;

  color-scheme: light;
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  *, *::before, *::after {
    animation-duration: 1ms !important;
    transition-duration: 1ms !important;
  }
}

body {
  margin: 0;
  background: var(--cream);
  color: var(--ink);
  font-family: var(--font);
  font-size: 1rem;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

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

a {
  color: inherit;
  text-decoration: none;
}

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1.1;
  text-wrap: balance;
}

p {
  margin: 0;
}

.container {
  width: var(--container);
  margin-inline: auto;
}

/* Scroll reveal - see script.js for the measured timing this implements. */
[data-reveal-item] {
  opacity: 0;
  transform: translateY(80px);
  transition:
    opacity 500ms linear var(--reveal-delay, 0ms),
    transform 500ms linear var(--reveal-delay, 0ms);
}

[data-reveal-item].is-revealed {
  opacity: 1;
  transform: translateY(0);
}

/* Heading char-reveal - see script.js for the char-split this implements.
   Measured from the reference's IX3 data: fade + slide-in-from-right +
   scale-down, ~40ms stagger per character, once on scroll into view. */
[data-heading-reveal] {
  overflow: clip;
}

.heading-word {
  display: inline-block;
}

.heading-char {
  display: inline-block;
  opacity: 0;
  transform: translateX(40%) scale(1.2);
  transition: opacity 0.4s var(--ease-out-quart), transform 0.4s var(--ease-out-quart);
}

[data-heading-reveal].is-revealed .heading-char {
  opacity: 1;
  transform: translateX(0) scale(1);
}

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

.skip-link:focus {
  position: fixed;
  top: 1rem;
  left: 1rem;
  z-index: 100;
  width: auto;
  height: auto;
  padding: 0.75rem 1.25rem;
  margin: 0;
  clip: auto;
  overflow: visible;
  white-space: normal;
  background: var(--black);
  color: var(--white);
  border-radius: var(--radius-sm);
}

.section {
  padding-block: clamp(3.5rem, 3rem + 3vw, 7.5rem);
}

.section--tight {
  padding-block: clamp(2.5rem, 2rem + 2vw, 4.5rem);
}

.section-heading {
  max-width: 42rem;
  margin-bottom: clamp(2rem, 1.5rem + 2vw, 3.5rem);
}

.section-heading h1,
.section-heading h2 {
  font-size: clamp(2rem, 1.6rem + 2vw, 3.25rem);
}

.section-heading p {
  margin-top: 1rem;
  color: var(--gray-400);
  font-size: 1.1rem;
}

.section-heading--center {
  margin-inline: auto;
  text-align: center;
}

/* -------------------- Buttons --------------------
   Default hover treatment for every button on the page, generalized from
   the nav Contact button: a ghost outline that blends into its own
   background at rest, filling solid on hover while its text flips to the
   contrasting color. .btn--on-dark swaps which color is the border/fill
   so the exact same mechanic reads correctly on a photo or a black
   section instead of the site's cream default - add it wherever a
   button sits on a dark background. */
.btn {
  --btn-ink: var(--ink);
  --btn-contrast: var(--white);
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font-weight: 500;
  font-size: 0.95rem;
  padding: 1em 1.85em;
  border-radius: 0;
  overflow: clip;
  isolation: isolate;
  background: transparent;
  color: var(--btn-ink);
  border: 1px solid var(--btn-ink);
  transition: color var(--duration-slow) var(--ease-out-quart);
}

.btn--on-dark {
  --btn-ink: var(--white);
  --btn-contrast: var(--ink);
}

.btn__fill {
  position: absolute;
  inset: -1%;
  z-index: 0;
  background: var(--btn-ink);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--duration-slow) var(--ease-out-quart);
}

.btn:hover .btn__fill,
.btn:focus-visible .btn__fill {
  transform: scaleX(1);
}

.btn:hover,
.btn:focus-visible {
  color: var(--btn-contrast);
}

.btn__label {
  position: relative;
  z-index: 1;
  display: block;
  height: 1.3em;
  overflow: clip;
}

.btn__text {
  display: block;
  transition: transform var(--duration-slow) var(--ease-out-quart);
}

.btn__text:last-child {
  position: absolute;
  inset-block-start: 100%;
  inset-inline-start: 0;
}

.btn:hover .btn__text,
.btn:focus-visible .btn__text {
  transform: translateY(-100%);
}

/* -------------------- Header / nav overlay --------------------
   Nav capsule mechanic + palette matched directly against
   tormuz.webflow.io (its compiled JS/CSS were fetched and read to get
   this exact): a single rectangular shell (small corner radius, not a
   pill) holds both the bar and the dropdown. Opening plays width first
   (expands toward 80% of the viewport) then height (drops down) as two
   sequential GSAP tweens on an expo.inOut timeline; closing is that same
   timeline in reverse (see script.js). Surface/ink colors are Tormuz's
   own neutral-100 / neutral-900 values, kept distinct from this site's
   own --cream/--ink so the nav reads as an intentional match rather
   than a coincidence. Fixed (not the reference's `position: absolute`)
   so navigation stays reachable on scroll. */
/* -------------------- Premium reveal preloader --------------------
   Mechanic matched from girlboss-design.webflow.io's own preloader (its
   compiled CSS/JS were fetched and read directly): a counter + fill bar
   animate together, then the whole overlay fades out and is removed - see
   the comment in index.html above this markup, and the "Premium reveal
   preloader" block at the top of script.js for the fill/counter logic and
   the sessionStorage/reduced-motion gating. */
.site-preloader {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ink);
  transition: opacity var(--duration-slow) var(--ease-out-quart);
}

.site-preloader[data-done='true'] {
  opacity: 0;
  pointer-events: none;
}

.site-preloader__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.5rem;
}

.site-preloader__logo {
  height: 1.75rem;
  width: auto;
}

.site-preloader__meter {
  display: flex;
  align-items: center;
  gap: 1rem;
  width: min(60vw, 20rem);
}

.site-preloader__bar {
  flex: 1;
  height: 2px;
  background: rgba(255, 255, 255, 0.2);
  overflow: hidden;
}

.site-preloader__bar-fill {
  height: 100%;
  width: 100%;
  transform: scaleX(0);
  transform-origin: left;
  background: var(--white);
  transition: transform 0.2s linear;
}

.site-preloader__counter {
  font-variant-numeric: tabular-nums;
  font-size: 0.85rem;
  color: var(--white);
  min-width: 3ch;
  text-align: right;
}

@media (prefers-reduced-motion: reduce) {
  .site-preloader,
  .site-preloader__bar-fill {
    transition: none;
  }
}

html.is-preloading {
  overflow: hidden;
}

.site-header {
  position: fixed;
  inset-inline: 0;
  inset-block-start: 0;
  z-index: 50;
  display: flex;
  justify-content: center;
  padding: 1.5rem 1.5rem 0;
  pointer-events: none;
}

.site-header__capsule {
  --nav-surface: #eef0f6;
  --nav-ink: #1e1e1e;
  pointer-events: auto;
  width: min(100% - 1rem, 46rem);
  background: var(--nav-surface);
  border-radius: 0;
  /* Collapsed to the bar's own height by default, via CSS alone - script.js's
     GSAP timeline (gsap.set(capsule, { height: navBar.offsetHeight })) only
     re-asserts this once it loads, it doesn't establish it. Without this,
     the capsule sits at its natural content height (bar + the full nav grid
     behind it, which only has opacity:0, not height:0) until GSAP runs,
     rendering as a tall pale-background panel "extended down" the page for
     however long the GSAP CDN script and script.js take to load and run -
     a real flash, not a loading-race illusion. */
  height: 3.75rem;
  overflow: clip;
  box-shadow: 0 12px 30px rgba(10, 10, 10, 0.12);
}

.site-header__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  height: 3.75rem;
  padding: 0.65rem 0.65rem 0.65rem 1.5rem;
  color: var(--nav-ink);
}

.site-header__toggle {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1.5px;
  width: 2.5rem;
  height: 2.5rem;
  background: none;
  border: 0;
  cursor: pointer;
}

/* Each stripe is a duplicate-roll, same mechanic as .nav-link__label: a
   clipped track holds two identical bars, one pre-parked one track-height
   below, and hovering (while closed) shifts both up by that same amount -
   the visible bar rolls away and its twin rolls in to take its place. */
.site-header__toggle-line {
  position: relative;
  width: 1.75rem;
  height: 0.375rem;
  overflow: clip;
  transition: transform var(--duration-base) var(--ease-out-quart);
}

.site-header__toggle-bar {
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: calc(50% - 0.75px);
  width: 100%;
  height: 1.5px;
  background: var(--nav-ink);
  transition: transform var(--duration-base) var(--ease-out-quart);
}

.site-header__toggle-bar:last-child {
  inset-block-start: calc(150% - 0.75px);
}

.site-header__toggle[aria-expanded='false']:hover .site-header__toggle-bar {
  transform: translateY(-0.375rem);
}

.site-header__toggle[aria-expanded='true'] .site-header__toggle-line:first-child {
  transform: translateY(3.75px) rotate(45deg);
}
.site-header__toggle[aria-expanded='true'] .site-header__toggle-line:last-child {
  transform: translateY(-3.75px) rotate(-45deg);
}

.site-header__logo {
  font-weight: 600;
  font-size: 1.1rem;
  letter-spacing: -0.01em;
  display: flex;
  align-items: center;
}

/* Scoped to the image specifically (not .site-header__logo itself) since
   the footer reuses .site-header__logo on a plain text <p> for its own
   wordmark - this rule shouldn't touch that. */
.site-header__logo-img {
  display: block;
  height: 1.75rem;
  width: auto;
}

/* Dropdown content: lives inside .site-header__capsule now (not a
   separate absolutely-positioned panel) - 3 columns (Main Pages /
   CMS+Other Pages / Follow Us + a promo photo card), separated by
   hairline borders. The capsule's height reveals it; this element only
   needs to fade in (handled by the GSAP timeline in script.js). */
.site-header__nav {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  opacity: 0;
}

/* Pages without a "This Property"/page-specific middle column (index, the
   properties/blog hubs, every blog article) - Main Pages + Follow Us only,
   since their old middle column was nothing but dead placeholder links. */
.site-header__nav--2col {
  grid-template-columns: repeat(2, 1fr);
}

/* Dim scrim behind the page while the nav is open. */
.site-header__scrim {
  position: fixed;
  inset: 0;
  z-index: 45;
  background: rgba(10, 10, 10, 0.45);
  opacity: 0;
  visibility: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .site-header__nav {
    transition: opacity var(--duration-base) var(--ease-out-quart);
  }
  .site-header__nav[data-open='true'] {
    opacity: 1;
  }
  .site-header__scrim {
    transition: opacity var(--duration-base) var(--ease-out-quart);
  }
  .site-header__scrim[data-open='true'] {
    opacity: 1;
    visibility: visible;
  }
}

.site-header__nav-col {
  padding: 2rem;
  border-inline-end: 1px solid var(--gray-300);
}

.site-header__nav-col:last-child {
  border-inline-end: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.site-header__nav-col--follow > div:first-child {
  padding: 2rem;
}

.site-header__nav-heading {
  font-weight: 700;
  color: var(--ink);
  margin-block: 1.5rem 0.75rem;
}

.site-header__nav-heading:first-child {
  margin-top: 0;
}

.site-header__links {
  list-style: none;
  margin: 0;
  padding: 0;
}

.site-header__links a,
.site-header__socials a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  font-size: 1rem;
  color: var(--gray-500);
  padding-block: 0.35rem;
  transition: color var(--duration-fast) var(--ease-out-quart);
}

.site-header__nav-col:first-child .site-header__links li:first-child a {
  color: var(--ink);
  font-weight: 600;
}

.site-header__links a:hover,
.site-header__socials a:hover {
  color: var(--ink);
}

.site-header__socials {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Hover roll, matched from tormuz.webflow.io's real IX3 timeline
   (t-a613ca81, fetched and read from their compiled JS): both duplicate
   label lines translate up together, and both duplicate arrows translate
   diagonally together - since each pair is identical content, the
   before/after states look the same but the 0.4s motion between them is
   the whole point. Markup for .nav-link__* is generated in script.js so
   every nav link doesn't need to be hand-duplicated in the HTML. */
.nav-link__text {
  display: flex;
  flex-direction: column;
  height: 1.4em;
  overflow: clip;
}

.nav-link__label {
  display: block;
  line-height: 1.4;
  transition: transform 0.4s var(--ease-out-quart);
}

.site-header__links a:hover .nav-link__label,
.site-header__socials a:hover .nav-link__label {
  transform: translateY(-100%);
}

.nav-link__arrow {
  position: relative;
  flex: none;
  width: 1rem;
  height: 1rem;
  overflow: clip;
}

.nav-link__arrow-icon {
  position: absolute;
  inset: 0;
  transition: transform 0.4s var(--ease-out-quart);
}

.nav-link__arrow-icon:last-child {
  transform: translate(-100%, 100%);
}

.site-header__links a:hover .nav-link__arrow-icon:first-child,
.site-header__socials a:hover .nav-link__arrow-icon:first-child,
.service-card__title:hover .nav-link__arrow-icon:first-child {
  transform: translate(100%, -100%);
}

.site-header__links a:hover .nav-link__arrow-icon:last-child,
.site-header__socials a:hover .nav-link__arrow-icon:last-child,
.service-card__title:hover .nav-link__arrow-icon:last-child {
  transform: translate(0, 0);
}

@media (prefers-reduced-motion: reduce) {
  .nav-link__label,
  .nav-link__arrow-icon,
  .site-header__toggle-bar {
    transition: none;
  }
}

/* WeChat QR card: a plain (non-link) block - the QR is meant to be
   scanned by a second device's camera pointed at this screen, not
   clicked, so unlike the property-photo promo this replaced, there's no
   meaningful click destination. The QR image sits fixed-width and
   centered (not stretched to the column's full width like the old
   property photos) since a nav-dropdown card only needs it legible, not
   maximized; caption sits below on the same solid dark band as before. */
.site-header__nav-promo {
  position: relative;
  flex: 1;
  overflow: clip;
  display: flex;
  flex-direction: column;
}

.site-header__nav-promo img {
  align-self: center;
  width: 9rem;
  margin-top: 1.5rem;
}

.site-header__nav-promo-overlay {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 1.25rem 1.5rem;
  color: var(--white);
  background: var(--ink);
  text-align: center;
}

.site-header__nav-promo-overlay p {
  font-weight: 600;
  font-size: 1.15rem;
  line-height: 1.3;
}

@media (max-width: 720px) {
  .site-header__nav {
    grid-template-columns: 1fr;
    max-height: 80vh;
    overflow-y: auto;
  }

  .site-header__nav-col {
    border-inline-end: 0;
    border-bottom: 1px solid var(--gray-300);
  }
}

/* -------------------- Hero --------------------
   Corrected from live render: full-bleed photo hero with a giant
   background wordmark the photo visually breaks through, headline
   anchored bottom-left, a floating agent card bottom-right. This section
   carries its own dark photographic tone against the site's light body.
   The background gradient is sampled from hero-poster.jpg's own top/bottom
   tones (dusk sky -> dark skyline), not an arbitrary color - .hero__photo
   sits on top of this via absolute positioning, so this gradient is what
   briefly shows through on any reload/paint before that layer renders. An
   earlier version used a cold blue-grey gradient left over from the
   original Webflow template this site was rebuilt from, which is what
   caused the long-reported "blue-grey flash" on reload - it clashed with
   the actual photo instead of blending into it. */
.hero {
  position: relative;
  min-height: 100svh;
  overflow: clip;
  display: flex;
  align-items: flex-end;
  color: var(--white);
  background: linear-gradient(180deg, #cacfe0 0%, #8b8792 55%, #4d4044 100%);
  padding-block-end: clamp(2rem, 1.5rem + 3vw, 4rem);
}

.hero__wordmark {
  position: absolute;
  inset-inline: 0;
  inset-block-start: clamp(4.5rem, 4rem + 3vw, 7rem);
  z-index: 2;
  text-align: center;
  font-size: clamp(3.5rem, 4rem + 6vw, 11rem);
  font-weight: 700;
  letter-spacing: -0.03em;
  line-height: 1;
  color: var(--white);
  text-shadow: 0 4px 30px rgba(0, 0, 0, 0.35);
  pointer-events: none;
  user-select: none;
}

/* Full-bleed: the photo now covers the entire hero section edge to edge,
   not a smaller floating inset image. A scrim keeps the wordmark and
   headline legible against whatever the photo looks like underneath. */
.hero__photo {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}

.hero__photo img,
.hero__photo-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.hero__photo::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0.15) 40%, rgba(0, 0, 0, 0.6) 100%);
}

.hero__content {
  position: relative;
  z-index: 3;
}

.hero__headline {
  font-size: clamp(2.25rem, 2vw + 1.75rem, 3.75rem);
  max-width: 13ch;
  color: var(--white);
}

.hero__actions {
  margin-top: 1.5rem;
}

.hero__image {
  border-radius: var(--radius-md);
  overflow: clip;
}

.hero__image img {
  width: 100%;
  height: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
}

@media (min-width: 900px) {
  .hero__inner {
    grid-template-columns: 0.85fr 1fr;
    gap: 4rem;
  }
}

/* -------------------- About: heading+eyebrow, 3-col intro, stats, logos --
   Measured from the reference's live site: a heading+eyebrow row, then a
   3-column row (CTA / photo / copy), then a SEPARATE 3-column stats row
   below it (not interleaved with the CTA/photo the way an earlier pass
   had it), then the logo strip. */
.about__top {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: flex-start;
  gap: 1rem;
  margin-bottom: clamp(2rem, 1.5rem + 2vw, 3.5rem);
}

.about__heading {
  font-size: clamp(1.75rem, 1.4rem + 1.75vw, 2.5rem);
  font-weight: 700;
  text-transform: uppercase;
  max-width: 34ch;
  line-height: 1.15;
}

.about__eyebrow {
  color: var(--gray-500);
  font-size: 0.9rem;
  white-space: nowrap;
}

.about__intro {
  padding-bottom: clamp(2.5rem, 2rem + 2vw, 4rem);
}

/* One paragraph per founder, so a 2-column split (matching the founder
   cards' own side-by-side layout above) fills the row instead of leaving
   the single 42rem-capped block from an earlier draft stranded on the
   left with a large empty gap to its right. */
@media (min-width: 900px) {
  .about__intro {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 2rem;
  }
}

.about__founders {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 2rem;
  margin-bottom: clamp(2rem, 1.5rem + 2vw, 3rem);
}

.about__founder-card {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  padding: 1.5rem;
  border: 1px solid var(--gray-300);
  border-radius: var(--radius-md);
}

.about__founder-card img {
  width: 300px;
  height: 300px;
  border-radius: var(--radius-full);
  object-fit: cover;
  flex-shrink: 0;
}

.about__founder-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.4rem;
}

.about__founder-name {
  font-weight: 600;
  font-size: 1.1rem;
}

.about__founder-title {
  font-size: 0.9rem;
  color: var(--gray-500);
}

.about__founder-linkedin {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  margin-top: 0.3rem;
  color: var(--gray-500);
  transition: color var(--duration-base) var(--ease-out-quart);
}

.about__founder-linkedin:hover {
  color: var(--ink);
}

.about__founder-linkedin svg {
  width: 100%;
  height: 100%;
}

.about__copy {
  color: var(--ink);
  font-size: 0.95rem;
  line-height: 1.7;
}

.about__stats {
  display: grid;
  gap: 2rem;
  padding-top: clamp(1.5rem, 1rem + 1.5vw, 2.5rem);
  border-top: 1px solid var(--gray-300);
}

.about__stat-label {
  color: var(--gray-500);
}

.about__stat-value {
  margin-top: 0.5rem;
  font-size: clamp(2.5rem, 2rem + 2vw, 4rem);
  font-weight: 600;
}

.about__stat-rule {
  margin: 1rem 0;
  border: 0;
  border-top: 1px solid var(--gray-300);
}

.about__stat-body {
  color: var(--gray-500);
}

/* Static, not a marquee - a single real credential (PropNex logo + REN
   status) reads as serious licensing info; scrolling it would make a lone
   item look like a fast, oddly-looping ticker instead. Marquees stay
   reserved for content with genuine plurality (see .hm-marquee in the
   hero-marquee-mockup.html concept). */
.about__credentials {
  margin-top: clamp(2.5rem, 2rem + 2vw, 4.5rem);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.25rem;
  flex-wrap: wrap;
  text-align: center;
}

.about__credentials-logo {
  height: 2.75rem;
  width: auto;
  display: block;
}

.about__credentials-divider {
  width: 1px;
  height: 1.75rem;
  background: var(--gray-300);
}

.about__credentials-text {
  font-size: 0.95rem;
  font-weight: 500;
  color: var(--gray-500);
}

@media (max-width: 480px) {
  .about__credentials-divider {
    display: none;
  }
}

@media (min-width: 900px) {
  .about__stats {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* -------------------- Properties showcase (hero slideshow) --------------------
   Reused verbatim from properties.html's own hero slider (auto-advancing
   cross-fade between full-bleed slides, modeled on noteen.webflow.io's
   hero slider after reading its markup/class structure directly - fresh,
   original CSS/JS, not their code or visual theme). Moved here from that
   page's own inline <style> once index.html started using the same
   component too, so both pages share one definition instead of two
   drifting copies. */
.hm-slider {
  position: relative;
  height: min(94vh, 920px);
  min-height: 560px;
  overflow: clip;
  background: var(--ink);
}

.hm-slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity 900ms ease;
}

.hm-slide.is-active {
  opacity: 1;
  visibility: visible;
  z-index: 1;
}

/* The photo stays partially visible (never fully transparent) above
   --reveal-end, tuned per slide to roughly where that photo's own
   building mass begins - its real sky color shows the whole way up
   instead of a flat black gap, while staying sheer enough that the
   name behind it (z-index below) still reads. The photo then returns
   to fully opaque right at the building, covering the name there. An
   honest tonal cut across the whole frame, not a fake trace of the
   tower's actual silhouette. */
.hm-slide__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1);
  transition: transform 8000ms linear;
  -webkit-mask-image: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0.34) 0%,
    rgba(0, 0, 0, 0.34) calc(var(--reveal-end, 36%) - 14%),
    #000 var(--reveal-end, 36%)
  );
  mask-image: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0.34) 0%,
    rgba(0, 0, 0, 0.34) calc(var(--reveal-end, 36%) - 14%),
    #000 var(--reveal-end, 36%)
  );
}

.hm-slide.is-active .hm-slide__img {
  transform: scale(1.08);
}

/* Bottom-up only - a top-down gradient here would dim the giant name
   sitting in that exact region, undoing "sharp, full opacity" text. */
.hm-slide__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(0deg, rgba(10, 8, 6, 0.6) 0%, transparent 34%);
}

.hm-slide__cta {
  position: absolute;
  z-index: 3;
  left: clamp(1.25rem, 3vw, 3rem);
  /* Clears the full-width ticker bar sitting at the very bottom edge. */
  bottom: clamp(4.5rem, 4rem + 2vw, 5.5rem);
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 700ms ease 500ms, transform 700ms ease 500ms;
}

.hm-slide.is-active .hm-slide__cta {
  opacity: 1;
  transform: translateY(0);
}

/* -------------------- Giant layered building name --------------------
   Sits behind the photo (z-index below .hm-slide__img), upper-center, so
   the sky lets it show through and the building itself - wherever it
   actually falls in that photo's own composition - covers it once the
   image returns to full opacity. This IS the hero content now; no
   eyebrow, tagline, or button. */
.hm-slide__bigname {
  position: absolute;
  inset: 0;
  z-index: 0;
  margin: 0;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: calc(var(--site-header-h, 92px) + clamp(1rem, 1.5vw, 1.75rem));
  padding-inline: 1.5rem;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 900ms ease 200ms, transform 900ms ease 200ms;
}

/* Font-size here is a MAX (JS shrinks it per name via inline style so
   longer names like "Imperial Residences" still land on one line instead
   of wrapping into the fully-opaque part of the photo). */
.hm-slide__bigname-text {
  display: block;
  max-width: 100%;
  font-size: clamp(3rem, 1.2rem + 8vw, 8rem);
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 0.94;
  text-transform: uppercase;
  text-align: center;
  color: var(--white);
  text-shadow: 0 2px 28px rgba(0, 0, 0, 0.4);
  white-space: nowrap;
}

.hm-slide.is-active .hm-slide__bigname {
  opacity: 1;
  transform: translateY(0);
}

/* -------------------- Slim tick indicator --------------------
   Three thin vertical rules, bottom-center, instead of round dots - reads
   closer to the sharp uppercase display type used for the building names
   than a generic carousel dot. All three share a bottom baseline
   (align-items: flex-end on the row) so the active tick grows taller
   upward rather than scaling from its center. */
.hm-ticker {
  position: absolute;
  z-index: 4;
  left: 0;
  right: 0;
  bottom: clamp(1.25rem, 2.5vw, 2rem);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: 0.75rem;
  height: 24px;
}

.hm-ticker__zone {
  width: 2px;
  height: 16px;
  padding: 0;
  margin: 0;
  border: none;
  border-radius: 1px;
  background: rgba(255, 255, 255, 0.45);
  cursor: pointer;
  transition: background 300ms ease, height 300ms ease;
}

.hm-ticker__zone[aria-current='true'] {
  background: var(--white);
  height: 24px;
}

.hm-ticker__zone:hover {
  background: rgba(255, 255, 255, 0.75);
}

/* -------------------- Logo countdown -------------------- */
/* The KLCC Investor mark itself is the countdown: a dim 40%-opacity base
   copy sits still, while a full-opacity copy of the same mark wipes in
   left-to-right over the slide's 4s dwell (clip-path, not opacity, so the
   reveal has a direction instead of just brightening uniformly). At 0s
   nothing is revealed, so the mark reads at its 40% floor; at 4s the wipe
   has crossed the whole mark, it reads fully bright, and the slide
   advances. Sits directly above the ticks, on the same center axis. */
.hm-logo-countdown {
  position: absolute;
  z-index: 4;
  left: 50%;
  bottom: clamp(3.25rem, 2.5rem + 2.5vw, 4.5rem);
  transform: translateX(-50%);
  width: clamp(72px, 7vw, 104px);
  pointer-events: none;
}

.hm-logo-countdown__base,
.hm-logo-countdown__fill {
  display: block;
  width: 100%;
  height: auto;
}

.hm-logo-countdown__base {
  opacity: 0.4;
}

.hm-logo-countdown__fill {
  position: absolute;
  inset: 0;
  -webkit-clip-path: inset(0 100% 0 0);
  clip-path: inset(0 100% 0 0);
}

@media (max-width: 640px) {
  .hm-logo-countdown {
    bottom: 2.75rem;
    width: 64px;
  }
}

/* -------------------- Properties showcase: pinned hold + curtain handoff --------------------
   .showcase-trigger is a PLAIN wrapper (no height of its own - it's just
   the natural sum of its 3 children) holding two sticky top:0 siblings,
   .showcase-stage and #how-we-work, with a spacer between them. This
   "stacked sticky siblings" arrangement is what makes the handoff a
   curtain instead of a plain scroll-past:

   - .showcase-stage (100vh) sticks first. Because a sticky element's
     valid "stuck" range extends through however much of ITS OWN
     container's remaining height isn't yet claimed by later content, it
     stays frozen not just for its own height but all the way through
     .showcase-dwell AND #how-we-work's height too - moot for most of
     that, since #how-we-work will already be covering it by then, but
     it's what keeps it motionless (not sliding away) through the dwell.
   - .showcase-dwell (200vh, empty) is what the slideshow autoplays
     through before anything moves - the exact scroll distance
     .showcase-stage spends simply sitting still, unchanged from the
     flat pinned hold this used to be.
   - #how-we-work gets its own real "stuck" duration from
     .how-we-work-dwell (a 4th sibling, empty spacer) directly after it -
     a sticky element's valid range is bounded by its containing block
     (.showcase-trigger), so without something after #how-we-work
     claiming extra height, the containing block would end exactly at
     its own bottom edge and it would pass through top:0 in a single
     instant (this was the original, dwell-less behavior). What makes
     the reveal itself read as a curtain, rather than a plain handoff, is
     the one-viewport-height of ordinary scroll immediately BEFORE that:
     #how-we-work is still a normal in-flow block whose top tracks
     scroll position 1:1 as it approaches the top of the viewport from
     below, while .showcase-stage is STILL actively stuck (frozen, fully
     covering the viewport) for that exact same stretch - since
     #how-we-work is later in the DOM with an opaque background, the two
     rectangles genuinely overlap on screen and #how-we-work paints over
     it, rising from the bottom. Once #how-we-work reaches top:0 it then
     HOLDS there, motionless, for .how-we-work-dwell's height, before
     releasing into plain, uninterrupted scrolling through its own (much
     taller) real content, exactly like a normal section. */
.showcase-trigger {
  position: relative;
}

.showcase-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
}

/* Overrides the base .hm-slider height (an ID beats a class regardless of
   source order, so this doesn't depend on cascade position) - properties.html's
   own hero keeps the base rule's fixed height untouched. Flex instead of a
   flat 100% since .showcase-stage now also holds .showcase-cta below it;
   min-height: 0 is required for a flex child to actually shrink to fit
   rather than overflowing on its own content size. */
#properties-showcase {
  flex: 1;
  min-height: 0;
}

/* Sits below the slider inside the same sticky stage, so it's visible for
   the section's entire pinned duration rather than a slide the autoplay
   cycles past or a plain element scrolled past right after the slider. */
.showcase-cta {
  flex: none;
  padding-block: 1.5rem;
  text-align: center;
  background: var(--cream);
}

.showcase-dwell {
  height: 200vh;
}

/* The slideshow itself still autoplays under reduced motion (matching
   properties.html's own hero, which has never special-cased it either) -
   only the pinning/curtain mechanics are undone, so a visitor isn't held
   in a fixed position (or shown a section rising to cover another) they
   didn't choose to scroll into - just a plain, ordinary section order. */
@media (prefers-reduced-motion: reduce) {
  .showcase-stage {
    position: static;
    height: auto;
    display: block;
  }

  #properties-showcase {
    height: min(94vh, 920px);
  }

  .showcase-dwell {
    height: 0;
  }

  #how-we-work {
    position: static;
    min-height: 0;
  }

  .how-we-work-dwell {
    height: 0;
  }
}

/* .section has no background of its own (it relies on body's --cream
   showing through) - fine everywhere else, but this section needs to be
   opaque so it actually covers the still-frozen properties/CTA section
   during the curtain-reveal overlap above, instead of that content
   showing through a transparent gap. min-height: 100vh matters just as
   much as the background: with every phase accordion collapsed (the
   default state), the section's natural content (heading + 4 collapsed
   headers) is shorter than one viewport - without this, the stuck box
   stops short of the viewport's bottom edge and leaves the "View All
   Properties" CTA sitting in the still-frozen stage behind it exposed
   (and clickable) below the visible curtain instead of fully covered. */
#how-we-work {
  position: sticky;
  top: 0;
  min-height: 100vh;
  background: var(--cream);
}

/* The actual pin: how long #how-we-work sits frozen at top:0, fully
   covering .showcase-stage, before releasing to normal scroll. */
.how-we-work-dwell {
  height: 100vh;
}

/* -------------------- How we work (numbered phase list) --------------------
   A vertical list of phases - "(01) Understand" etc - each pairing body
   copy with a photo and a checklist, separated by hairlines. Replaces an
   earlier 5-column grid that didn't match the reference's real layout.
   Reveal-on-scroll (fade up + stagger) is matched from tormuz.webflow.io's
   real IX3 timeline (t-0d5d201c, fetched and read from their compiled JS)
   for [data-animation-2="slide-up-reveal-2"] - the mechanic behind their
   "Real Stories From Successful Startup Leaders" testimonial cards: y
   60px->0, opacity 0->100%, 0.75s, a 150ms base delay then 200ms stagger
   per card (see script.js). Kept separate from the site's other
   [data-reveal-item] system (agent/insight grids) rather than changing
   its shared values, since those use a different, already-established
   80px/500ms/190ms measurement from a different section. */
.phase-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--gray-300);
}

.phase {
  padding-block: clamp(1.5rem, 1rem + 2vw, 2.5rem);
  border-bottom: 1px solid var(--gray-300);
}

[data-phase-reveal] {
  opacity: 0;
  transform: translateY(60px);
  transition:
    opacity 0.75s var(--ease-out-quart) var(--phase-reveal-delay, 0ms),
    transform 0.75s var(--ease-out-quart) var(--phase-reveal-delay, 0ms);
}

[data-phase-reveal].is-revealed {
  opacity: 1;
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  [data-phase-reveal] {
    transition: none;
  }
}

.phase__title {
  margin: 0;
  font-size: clamp(1.5rem, 1.2rem + 1.5vw, 2.25rem);
}

/* Accordion trigger - opens on HOVER (same mechanic as .faq-item), mouseleave
   closes it. Click/keyboard [data-open] below remains as the accessible
   fallback for touch and keyboard users, who have no hover state to trigger
   it. Each phase opens/closes independently (siblings aren't forced shut)
   since these are steps to browse side by side, not competing FAQ answers. */
.phase__head {
  width: 100%;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 0;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.phase__index {
  color: var(--gray-400);
}

/* Grid-rows 0fr -> 1fr collapse trick (same as .faq-item__answer): animates
   height without JS measuring scrollHeight. Deliberately slower than the
   FAQ's 500ms - this one opens on hover as you scroll/move through the
   phase list, so a snappier speed read as flickery; 850ms gives it a more
   deliberate, considered feel. */
.phase__collapse {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 850ms var(--ease-out-quart);
}

.phase[data-open='true'] .phase__collapse {
  grid-template-rows: 1fr;
}

@media (hover: hover) and (pointer: fine) {
  .phase:hover .phase__collapse {
    grid-template-rows: 1fr;
  }
}

.phase__collapse-inner {
  overflow: hidden;
  min-height: 0;
}

.phase__body {
  margin-top: 1.5rem;
  padding-bottom: 0.5rem;
  display: grid;
  gap: 1.5rem;
  align-items: start;
}

.phase__text {
  color: var(--ink);
  max-width: 42ch;
}

.phase__photo {
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-md);
  overflow: clip;
}

.phase__photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.phase__list {
  margin: 0;
  padding-left: 1.1rem;
  color: var(--gray-500);
}

.phase__list li {
  margin-bottom: 0.5rem;
}

@media (min-width: 800px) {
  .phase__body {
    grid-template-columns: 1fr 1.1fr 1fr;
  }
}

/* -------------------- Our Service (scroll-scrubbed card row) --------------------
   Mechanic + card design matched from tormuz.webflow.io's "Services We
   Provide" section (its compiled JS/CSS were fetched and read directly) -
   see the comment in index.html above this section for the full source
   note. Same tall-wrapper-plus-sticky-stage pattern as Testimonials. */
.service-section {
  background: var(--cream);
}

.service-trigger {
  position: relative;
  /* 300vh drives the 0->1 scrub itself; the extra 50vh is a dwell - once
     progress clamps to 1 (script.js) the last card sits flush and pinned
     for this whole stretch before the section releases, so a reader
     doesn't get swept past the final MM2H card the instant it arrives. */
  height: 350vh;
}

.service-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(1.5rem, 1rem + 2vw, 2.5rem);
  overflow: clip;
}

.service-track {
  display: flex;
  gap: 1.5rem;
  padding-inline-start: max(1.5rem, calc((100vw - 82.25rem) / 2 + 1.5rem));
  will-change: transform;
}

.service-card {
  flex: 0 0 min(78vw, 22rem);
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 1.75rem;
  border: 1px solid var(--gray-300);
  border-radius: var(--radius-md);
  background: var(--white);
  transition: box-shadow var(--duration-slow) var(--ease-out-quart);
}

.service-card:hover {
  box-shadow: 0 20px 50px rgba(10, 10, 10, 0.12);
}

.service-card__title {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-size: 1.4rem;
  font-weight: 600;
  color: var(--ink);
}

.service-card__desc {
  color: var(--gray-600);
  font-size: 0.95rem;
  line-height: 1.6;
}

/* margin-top: auto pushes the media (and the tags below it) down to the
   card's bottom edge regardless of how many lines the description above
   it wraps to - .service-card is already a flex column stretched to the
   row's tallest card, so without this, a card with a short description
   leaves its image sitting higher than a card with a long one instead of
   every card's image lining up at the same height. */
.service-card__media {
  margin-top: auto;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-sm);
  overflow: clip;
}

.service-card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.service-card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

.service-card__tags li {
  padding: 0.35rem 0.9rem;
  border: 1px solid var(--gray-300);
  border-radius: var(--radius-full);
  font-size: 0.8rem;
  color: var(--gray-600);
}

.service-liner-wrap {
  height: 1px;
  background: var(--gray-300);
}

.service-liner {
  display: block;
  width: 0%;
  height: 1px;
  background: var(--ink);
}

@media (prefers-reduced-motion: reduce) {
  .service-trigger {
    height: auto;
  }

  .service-stage {
    position: static;
    height: auto;
  }

  .service-track {
    overflow-x: auto;
    transform: none !important;
  }

  .service-liner {
    display: none;
  }
}

/* -------------------- Note from Kay (sellers & landlords CTA) --------------------
   Small, personal, and centered - a real photo, a first name, one direct
   CTA - rather than another feature card or a left-aligned block, since
   the point is that this reads like a single centered note, not another
   section of the page. padding-top: 0 (the section is otherwise a plain
   .section) so the whitespace above matches what .service-section's own
   bottom padding already provides, instead of stacking both and leaving
   an oversized gap above this section relative to the one below it. */
.note-cta {
  padding-top: 0;
}

.note-cta__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.25rem;
  max-width: 38rem;
  margin-inline: auto;
  text-align: center;
}

.note-cta__avatar {
  flex-shrink: 0;
  width: clamp(88px, 7vw, 116px);
  height: clamp(88px, 7vw, 116px);
  border-radius: 50%;
  object-fit: cover;
}

/* Pull-quote row: the big mark hangs on the left of the paragraph instead
   of sitting above the photo, same device as .testimonial-card__mark
   elsewhere on the site but scaled up and darkened for the cream
   background. align-items: flex-start plus a negative top margin on the
   mark tucks its glyph into the text's own cap-height instead of the
   mark's full line-height pushing the paragraph down. */
.note-cta__quote {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  margin-top: 0.5rem;
  text-align: left;
}

.note-cta__mark {
  flex-shrink: 0;
  margin-top: -0.75rem;
  font-family: Georgia, 'Times New Roman', serif;
  font-size: clamp(3.5rem, 2.75rem + 3vw, 5rem);
  line-height: 1;
  color: var(--gray-300);
}

.note-cta__eyebrow {
  margin: 0;
  color: var(--gray-500);
  font-size: 0.85rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.note-cta__text {
  margin: 0;
  color: var(--gray-700);
  font-size: 1.05rem;
  line-height: 1.65;
}

.note-cta__btn {
  margin-top: 0.5rem;
}

/* -------------------- Showreel (scroll-scrubbed zoom into a video mosaic)
   Measured from the reference's actual IX3 interaction data: a 3-column
   grid of square tiles (each tile the SAME footage panned to a different
   crop) starts scaled to 3.25x - filling the screen with one zoomed-in
   view - and scales down to 1x as you scroll through a tall pinned
   wrapper. A centered title fades in, then out, partway through; a play
   control fades/scales in near the end. See script.js for the scroll
   -progress calculation driving grid scale + title/play opacity. */
.showreel-wrap {
  position: relative;
  height: 400vh;
}

.showreel-sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: clip;
  background: var(--cream);
  display: flex;
  align-items: center;
  justify-content: center;
}

.showreel__grid {
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(2, 1fr);
  gap: 0.4rem;
  padding: 0.4rem;
  transform: scale(3.25);
  will-change: transform;
}

.showreel__tile {
  position: relative;
  overflow: clip;
}

.showreel__tile img,
.showreel__tile video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.showreel__control {
  position: absolute;
  inset-block-end: 1rem;
  inset-inline-end: 1rem;
  width: 2.25rem;
  height: 2.25rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  background: rgba(0, 0, 0, 0.4);
  backdrop-filter: blur(6px);
  color: var(--white);
  border: 0;
  cursor: pointer;
  font-size: 0.65rem;
  letter-spacing: 0.05em;
}

@media (max-width: 720px) {
  .showreel__grid {
    grid-template-columns: repeat(2, 1fr);
    grid-template-rows: repeat(3, 1fr);
  }
}

@media (prefers-reduced-motion: reduce) {
  .showreel-wrap {
    height: auto;
  }

  .showreel-sticky {
    position: static;
    height: auto;
  }

  .showreel__grid {
    transform: none;
  }
}

/* -------------------- Testimonials (click-to-expand card deck) --------------------
   Cards rest in a "hand of cards" fan: each .testimonial-card-slot carries
   its own deterministic rotation + vertical offset via CSS custom
   properties, overlapping the next via negative margin. The rotation/lift
   lives on the SLOT, not the card itself, so script.js can drive the
   card's own `transform` purely for the FLIP expand/collapse animation
   without the two fighting over one property. */
/* Was `min-height: 100vh` + flex-centering to read as its own full-page
   "chapter". In context between Agents and FAQ that backfired: the deck's
   actual content is only ~450-560px tall, so centering it in a hard 100vh
   box left ~170px of dead flex slack on EACH side on top of the section's
   own padding-block - stacking up to a ~260px gap on both sides (checked
   via getBoundingClientRect), noticeably more than the ~180px gap between
   any other two sections on the page. A generous-but-bounded padding
   keeps this section feeling a little more spacious than its neighbors
   without manufacturing empty space just to hit a fixed viewport height. */
.testimonials-section {
  padding-block: clamp(4.5rem, 3.5rem + 4vw, 8rem);
}

.testimonial-deck {
  position: relative;
  padding-block: clamp(3rem, 2rem + 4vw, 6rem) clamp(2rem, 1rem + 3vw, 4rem);
}

.testimonial-deck__backdrop {
  position: fixed;
  inset: 0;
  background: rgba(10, 10, 10, 0.6);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--duration-slow) var(--ease-out-quart);
  z-index: 40;
}

.testimonial-deck[data-deck-open='true'] .testimonial-deck__backdrop {
  opacity: 1;
  pointer-events: auto;
}

.testimonial-deck__fan {
  list-style: none;
  margin: 0;
  padding-inline: 1.5rem;
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  row-gap: 2rem;
}

.testimonial-card-slot {
  --deck-rotate: 0deg;
  --deck-lift: 0px;
  position: relative;
  transform: rotate(var(--deck-rotate)) translateY(var(--deck-lift));
  transition: transform var(--duration-slow) var(--ease-out-quart);
}

/* Fan overlap/rotation only kicks in once there's enough width to spread
   five cards sideways; narrower screens keep the slot's default 0/0 and
   fall back to a lightly-overlapped vertical stack instead (below). */
@media (min-width: 860px) {
  .testimonial-card-slot + .testimonial-card-slot {
    margin-inline-start: -5.5rem;
  }

  .testimonial-card-slot:nth-child(1) { --deck-rotate: -9deg; --deck-lift: 10px; z-index: 1; }
  .testimonial-card-slot:nth-child(2) { --deck-rotate: -4deg; --deck-lift: -10px; z-index: 2; }
  .testimonial-card-slot:nth-child(3) { --deck-rotate: 2deg; --deck-lift: 14px; z-index: 3; }
  .testimonial-card-slot:nth-child(4) { --deck-rotate: 6deg; --deck-lift: -8px; z-index: 4; }
  .testimonial-card-slot:nth-child(5) { --deck-rotate: -2deg; --deck-lift: 4px; z-index: 5; }

  .testimonial-card-slot:hover,
  .testimonial-card-slot:focus-within {
    --deck-rotate: 0deg;
    --deck-lift: -30px;
    z-index: 30;
  }
}

@media (max-width: 859px) {
  .testimonial-deck__fan {
    flex-direction: column;
    align-items: center;
  }

  .testimonial-card-slot + .testimonial-card-slot {
    margin-top: -3.75rem;
  }

  .testimonial-card-slot:hover,
  .testimonial-card-slot:focus-within {
    --deck-lift: -14px;
    z-index: 30;
  }
}

/* Per-card accent color, set as an inline `--deck-bg` custom property
   directly on the .testimonial-card element in index.html (not on the
   slot, and not via nth-child) - it needs to survive the card being
   reparented out of its slot when expanded, and nth-child positions would
   break the moment that reparenting changes its sibling index. Palette:
   five saturated "sweet" jewel tones - raspberry, teal, violet, tangerine,
   amber - each still dark enough to clear 4.5:1 contrast against the
   existing white card text (checked against WCAG's contrast formula, not
   eyeballed) without a second text-color rule. Each avatar photo also gets
   a duotone filter (see the <svg><defs> near the top of index.html) tinted
   to a dark/light pair of that same card's color, so the photo and the
   card color read as one deliberate, matching pair rather than a random
   collage. */
.testimonial-card {
  position: relative;
  width: min(62vw, 18.5rem);
  padding: 2rem;
  background: var(--deck-bg, var(--black-soft));
  color: var(--white);
  border-radius: var(--radius-md);
  display: flex;
  flex-direction: column;
  gap: 1rem;
  cursor: pointer;
  box-shadow: 0 10px 30px rgba(10, 10, 10, 0.25);
  transition: box-shadow var(--duration-base) var(--ease-out-quart);
}

.testimonial-card-slot:hover .testimonial-card,
.testimonial-card-slot:focus-within .testimonial-card {
  box-shadow: 0 24px 55px rgba(10, 10, 10, 0.4);
}

@media (max-width: 859px) {
  .testimonial-card {
    width: min(85vw, 22rem);
  }
}

.testimonial-card__mark {
  font-family: Georgia, 'Times New Roman', serif;
  font-size: 3rem;
  line-height: 1;
  /* Purely decorative, so a dim wash is fine here - but the vibrant
     per-card colors below are close enough in lightness to the site's
     --gray-600/500/400 that those would sit at ~1.1-3.3:1 contrast (checked
     against WCAG's formula) and nearly vanish; translucent white scales
     with whatever the card's background is instead. */
  color: rgba(255, 255, 255, 0.35);
}

.testimonial-card__quote {
  font-size: 1.05rem;
  line-height: 1.6;
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Revealed only once the card is expanded - a small reward for clicking
   through, not shown on the resting fan card. */
.testimonial-card__badge {
  display: none;
  align-self: flex-start;
  padding: 0.3rem 0.75rem;
  border-radius: var(--radius-full);
  border: 1px solid rgba(255, 255, 255, 0.4);
  font-size: 0.75rem;
  /* 90% white clears 4.5:1 against every card color in the palette above
     (checked); the site's own --gray-400 would not (as low as ~2:1 on
     some of them). */
  color: rgba(255, 255, 255, 0.9);
}

.testimonial-card__person {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: auto;
}

.testimonial-card__avatar {
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius-full);
  object-fit: cover;
}

.testimonial-card__name {
  font-weight: 600;
  font-size: 0.9rem;
}

.testimonial-card__role {
  /* Same reasoning as .testimonial-card__badge above - --gray-500 would
     drop to ~1.1-1.8:1 against these saturated card colors. */
  color: rgba(255, 255, 255, 0.9);
  font-size: 0.8rem;
}

.testimonial-card__close {
  display: none;
  position: absolute;
  top: 1rem;
  right: 1rem;
  width: 2.25rem;
  height: 2.25rem;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  border: 1px solid rgba(255, 255, 255, 0.3);
  background: transparent;
  color: var(--white);
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
}

/* Expanded state: the card leaves the fan, centers itself over the
   viewport, and reveals its full quote + badge. script.js drives the
   grow-from-the-fan motion via an inline FLIP transform on top of this. */
.testimonial-card[data-open='true'] {
  position: fixed;
  inset: 0;
  margin: auto;
  width: min(90vw, 34rem);
  max-height: min(82vh, 34rem);
  height: fit-content;
  z-index: 50;
  cursor: default;
}

.testimonial-card[data-open='true'] .testimonial-card__quote {
  -webkit-line-clamp: unset;
  overflow: visible;
}

.testimonial-card[data-open='true'] .testimonial-card__badge,
.testimonial-card[data-open='true'] .testimonial-card__close {
  display: flex;
}

@media (prefers-reduced-motion: reduce) {
  .testimonial-card-slot {
    transition: none;
  }
}

/* -------------------- FAQ accordion -------------------- */
/* Was `align-items: flex-end` with the intro block first in the DOM: since
   the intro (h3 + paragraph + button) is taller than the bare "FAQs"
   heading, bottom-aligning them left the big h2 floating noticeably lower
   than the small intro heading above it - a disconnected, diagonal-looking
   header instead of one composition. Reordered so "FAQs" (the section's
   real h2) leads on the left like every other section's heading on this
   page, with the "can't find your answer" prompt as a smaller supporting
   block beside it, both starting from the same top line. */
.faq-header {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: flex-start;
  gap: 2rem;
  margin-bottom: 2.5rem;
}

.faq-header__intro h3 {
  font-size: 1.1rem;
  margin-bottom: 0.5rem;
}

.faq-header__intro p {
  color: var(--gray-500);
  max-width: 22rem;
  margin-bottom: 1.25rem;
}

.faq-header__title {
  font-size: clamp(2rem, 1.4rem + 3vw, 3.5rem);
}

/* Measured: each row cascades further right than the one above it,
   forming a staircase rather than a flush-left list. Previously also had
   `margin-inline-start: auto`, pushing the whole list to the right on TOP
   of that per-row cascade - between the two, almost the entire left half
   of the section sat empty all the way down. Starting the list flush-left
   instead lets the staircase do its job by itself, using the width the
   section actually has. */
.faq-list {
  border-top: 1px solid var(--gray-700);
  max-width: 48rem;
}

.faq-item {
  border-bottom: 1px solid var(--gray-700);
}

/* nth-child(5n+k) repeats the staircase every 5 rows instead of stopping
   after item 5 - needed once a FAQ list runs past 5 items (e.g. the
   property-detail pages), so the last few rows don't fall back flush-left
   and break the cascade. */
@media (min-width: 640px) {
  .faq-item:nth-child(5n+2) { margin-inline-start: 2rem; }
  .faq-item:nth-child(5n+3) { margin-inline-start: 4rem; }
  .faq-item:nth-child(5n+4) { margin-inline-start: 6rem; }
  .faq-item:nth-child(5n) { margin-inline-start: 8rem; }
}

.faq-item__question {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 1.5rem;
  background: none;
  border: 0;
  color: inherit;
  font-family: inherit;
  font-size: 1.1rem;
  text-align: left;
  cursor: pointer;
}

.faq-item__icon {
  flex-shrink: 0;
  transition: transform var(--duration-base) var(--ease-out-quart);
}

.faq-item[data-open='true'] .faq-item__icon {
  transform: rotate(45deg);
}

.faq-item__answer {
  display: grid;
  grid-template-rows: 0fr;
  /* 500ms measured from the reference's live open/close motion (polled
     computed height every ~30ms during the transition) - notably slower
     than this file's default 300ms UI-transition speed. */
  transition: grid-template-rows 500ms var(--ease-out-quart);
}

.faq-item[data-open='true'] .faq-item__answer {
  grid-template-rows: 1fr;
}

.faq-item__answer-inner {
  overflow: hidden;
  min-height: 0;
}

.faq-item__answer p {
  padding-bottom: 1.5rem;
  color: var(--gray-400);
  max-width: 42rem;
}

/* Contextual links inline in an answer (e.g. "read our full guide") -
   the global `a { color: inherit; text-decoration: none; }` reset leaves
   these indistinguishable from surrounding text otherwise. Darker ink
   alone against this paragraph's --gray-400 body text still read as too
   subtle to register as a link at a glance, so weight carries the
   distinction too - same ink + 600-weight combination already used for
   the nav's own emphasized links (.site-header__links li:first-child a),
   rather than reaching for an accent color on this grayscale-only site. */
.faq-item__answer a {
  color: var(--ink);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: var(--gray-500);
  text-underline-offset: 0.15em;
  transition: text-decoration-color var(--duration-base) var(--ease-out-quart);
}

.faq-item__answer a:hover,
.faq-item__answer a:focus-visible {
  text-decoration-color: currentColor;
}

/* Measured from the reference's actual interaction data: this accordion
   opens on HOVER (mouseleave closes it), not click - click/keyboard
   [data-open] above remains as the accessible fallback for touch and
   keyboard users, who have no hover state to trigger it. */
@media (hover: hover) and (pointer: fine) {
  .faq-item:hover .faq-item__answer {
    grid-template-rows: 1fr;
  }

  .faq-item:hover .faq-item__icon {
    transform: rotate(45deg);
  }
}

/* -------------------- Insights / blog teaser -------------------- */
/* Heading + "View All Articles" link row, same flex/space-between device
   as .about__top - a real link to the blog hub, since this section only
   ever shows 3 teaser cards and had no way to reach the rest of the
   articles otherwise. */
.insights__heading {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  max-width: none;
}

.insights__view-all {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--ink);
  font-weight: 500;
  white-space: nowrap;
}

.insights__view-all:hover .nav-link__arrow-icon:first-child {
  transform: translate(100%, -100%);
}

.insights__view-all:hover .nav-link__arrow-icon:last-child {
  transform: translate(0, 0);
}

.insight-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 1.5rem;
}

/* Measured: the middle card sits lower than its neighbors, matching the
   Properties grid's stagger. */
@media (min-width: 900px) {
  .insight-card--offset {
    margin-top: 3rem;
  }
}

.insight-card__image {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-md);
  overflow: clip;
  background: var(--black-elevated);
}

.insight-card__image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Measured: a dark overlay scales in from the center and fades in on
   hover (scale 0 -> 1, opacity 0 -> 100%), not a plain fade. */
.insight-card__overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.35);
  opacity: 0;
  transform: scale(0);
  transition: opacity var(--duration-base) var(--ease-out-quart), transform var(--duration-base) var(--ease-out-quart);
}

.insight-card:hover .insight-card__overlay {
  opacity: 1;
  transform: scale(1);
}

.insight-card__tag {
  margin-top: 1rem;
  color: var(--gray-500);
  font-size: 0.8rem;
  letter-spacing: 0.04em;
}

.insight-card__title {
  margin-top: 0.5rem;
  font-size: 1.15rem;
}

/* A card linking to a real article wraps its image/tag/title in this so
   the whole card is clickable; placeholder cards (no article yet) skip it. */
.insight-card__link {
  display: block;
  color: inherit;
  text-decoration: none;
}

/* -------------------- Footer -------------------- */
.site-footer {
  padding-block: 3rem;
}

/* Scoped to this exact footer (the only one carrying #enquiry, now that the
   newsletter section it moved from is gone) rather than to .site-footer
   itself, so the other 23 pages sharing this stylesheet - whose footers
   never had a section above them supplying this separator - keep their
   current look. */
.site-footer#enquiry {
  border-top: 1px solid var(--gray-300);
}

.site-footer__inner {
  display: grid;
  gap: 2rem;
}

/* Logo pairing echoes .about__credentials's logo + divider + text
   pattern above, so the "two marks side by side" idiom reads as one
   system rather than a one-off footer treatment. */
.site-footer__brand-logos {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.site-footer__brand-logo {
  height: 1.6rem;
  width: auto;
  display: block;
}

/* The PropNex mark specifically is enlarged (matching .about__credentials-logo's
   size, since both are the same real logo swapped in from a higher-res source)
   without touching the KLCC Investor logo it sits beside, which keeps
   .site-footer__brand-logo's own height. Two classes on one selector for
   higher specificity than the base rule above, so this isn't dependent on
   cascade order. */
.site-footer__brand-logo.site-footer__brand-logo--propnex {
  height: 2.75rem;
}

.site-footer__brand-divider {
  width: 1px;
  height: 1.5rem;
  background: var(--gray-300);
}

.site-footer__motto {
  margin-top: 0.85rem;
  max-width: 26rem;
  color: var(--gray-400);
  line-height: 1.5;
}

/* Matched to .site-header__nav-heading's weight/color so a heading sitting
   above a link list reads the same way here as it does in the nav's own
   mega-menu columns. */
.site-footer__heading {
  margin-bottom: 0.85rem;
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--ink);
}

/* A second heading stacked in the same footer column (e.g. "WeChat" below
   "Address") needs breathing room from the content above it. */
.site-footer__heading:not(:first-child) {
  margin-top: 1.5rem;
}

.site-footer__qr {
  display: block;
  width: 7rem;
}

.site-footer__legal {
  margin-top: 2rem;
  text-align: center;
  color: var(--gray-600);
  font-size: 0.8rem;
}

/* Measured: a giant brand wordmark bleeds off the bottom edge of the
   footer, mirroring the hero's oversized wordmark treatment. */
.site-footer__wordmark {
  margin-top: 1.5rem;
  overflow: clip;
  color: var(--ink);
  opacity: 0.06;
  font-size: clamp(4rem, 2rem + 14vw, 11rem);
  font-weight: 700;
  line-height: 0.8;
  text-align: center;
  white-space: nowrap;
  transform: translateY(28%);
}

@media (min-width: 700px) {
  .site-footer__inner {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* -------------------- Counter number tabular alignment -------------------- */
.tabular-nums {
  font-variant-numeric: tabular-nums;
}
