/* ========================================================================
   Design tokens
   ======================================================================== */
:root {
  --color-bg: #F0F6FA;
  --color-surface: #FFFFFF;
  --color-cream: #FBF7EF;
  --color-text: #1B2130;
  --color-text-muted: #62687A;
  --color-accent: #D5B98D;       /* sandy gold — site-wide palette by request, see Design Direction in CLAUDE.md */
  --color-accent-dark: #A6832E;
  --color-navy: #002D6C;         /* deep Mediterranean navy — site-wide palette by request */
  --color-navy-deep: #001B45;
  --color-navy-light: #123A7A;
  --color-whatsapp: #25D366;
  --color-whatsapp-dark: #1DA851;
  --color-border: #DCE6ED;

  --font-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-heading: 'Poppins', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-display: var(--font-heading); /* big h1 display font — overridden per-page, see "Page-scoped typography" below */

  --space-1: 0.5rem;
  --space-2: 1rem;
  --space-3: 1.5rem;
  --space-4: 2rem;
  --space-5: 3rem;
  --space-6: 4.5rem;

  /* One shared easing curve for every transition/animation on the site —
     a fast-out, slow-settle curve. Keeping motion on a single curve is
     what makes unrelated components feel like one product; don't
     hand-write per-component timing functions. */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --hero-arc: clamp(28px, 5vw, 72px);   /* hero 底部弧线半径 */

  --radius: 10px;
  --radius-pill: 6px;
  --shadow-card: 0 10px 30px rgba(0, 45, 108, 0.08);
  --shadow-card-hover: 0 16px 40px rgba(0, 45, 108, 0.14);
  --container-width: 1140px;
}

/* ========================================================================
   Reset / base
   ======================================================================== */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  font-family: var(--font-base);
  font-weight: 400;
  color: var(--color-text);
  background: linear-gradient(180deg, #FFFFFF 0%, #EAF3FA 100%);
  /* Generous body leading reads calmer and more premium, and matters on
     the long legal/finance copy in the calculator and FAQ. Headings are
     pulled back tight below — inheriting this would make multi-line
     titles look airy and disconnected. */
  line-height: 1.7;
  font-display: swap;
}
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; margin: 0; padding: 0; }
h1, h2, h3, h4, h5 {
  font-family: var(--font-heading);
  font-weight: 600;
  line-height: 1.25;
  margin: 0 0 var(--space-2);
}
h1 { font-family: var(--font-display); }
p { margin: 0 0 var(--space-2); }

.container {
  width: 100%;
  max-width: var(--container-width);
  margin: 0 auto;
  padding: 0 var(--space-3);
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--color-accent);
  color: #fff;
  padding: var(--space-1) var(--space-2);
  z-index: 1000;
}
.skip-link:focus { left: var(--space-2); top: var(--space-2); }

/* Scroll-reveal utilities (.reveal / .line-reveal) live at the very end of
   this file — see "Scroll reveal" there for why. */

/* ========================================================================
   Buttons
   ======================================================================== */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* justify-content centres the flex line, but a label that wraps onto a
     second line still aligns to the start without this. "View Full Details"
     is exactly that case in the card action row. */
  text-align: center;
  padding: 0.85rem 1.6rem;
  border-radius: 4px;
  font-weight: 600;
  font-size: 0.95rem;
  border: 2px solid transparent;
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.15s ease,
    background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.btn:hover {
  transform: translateY(-1px);
  box-shadow: 0 8px 20px rgba(43, 43, 40, 0.16);
}

.btn-whatsapp {
  background: var(--color-whatsapp);
  color: #fff;
}
.btn-whatsapp:hover { background: var(--color-whatsapp-dark); }

.btn-outline {
  background: transparent;
  border-color: var(--color-text);
  color: var(--color-text);
}
.btn-outline:hover { background: var(--color-text); color: #fff; }

.btn-small {
  padding: 0.55rem 1.1rem;
  font-size: 0.85rem;
}

/* ========================================================================
   Header
   ======================================================================== */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: rgba(0, 27, 69, 0.94);
  backdrop-filter: blur(6px);
  border-bottom: 1px solid transparent;
  transition: border-color 0.2s ease, box-shadow 0.2s ease,
    transform 0.4s var(--ease);
}
.site-header.is-scrolled {
  border-bottom-color: rgba(213, 185, 141, 0.35);
  box-shadow: var(--shadow-card);
}
/* Slides out of the way while scrolling down, returns on any scroll up —
   see wireHeaderScrollState() in js/main.js. Never applied near the top of
   the page, and the nav stays open/usable because the class is removed the
   moment the visitor scrolls up. */
.site-header.is-hidden { transform: translateY(-104%); }
.site-header.is-hidden:focus-within { transform: none; }
@media (prefers-reduced-motion: reduce) {
  .site-header { transition: border-color 0.2s ease, box-shadow 0.2s ease; }
  .site-header.is-hidden { transform: none; }
}

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: var(--space-2);
  padding-bottom: var(--space-2);
}

.logo {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 1.1rem;
  letter-spacing: -0.01em;
  display: inline-flex;
  align-items: center;
}
.logo img {
  display: block;
  height: 42px;
  width: auto;
  border-radius: 6px;
}

/* Nav is tucked behind the hamburger toggle at every screen size —
   there is no separate desktop nav bar. */
.main-nav {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  display: none;
  flex-direction: column;
  align-items: flex-start;
  padding: var(--space-2) var(--space-3) var(--space-3);
  gap: var(--space-2);
}
.main-nav.is-open { display: flex; }
.main-nav a {
  font-weight: 500;
  font-size: 0.95rem;
  color: var(--color-text-muted);
}
.main-nav a:hover { color: var(--color-text); }

.nav-whatsapp {
  margin-top: var(--space-1);
}

.lang-switch {
  display: flex;
  gap: var(--space-2);
  padding-top: var(--space-2);
  margin-top: var(--space-1);
  border-top: 1px solid var(--color-border);
  width: 100%;
}
.lang-switch a {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--color-text-muted);
}
.lang-switch a[aria-current="true"] {
  color: var(--color-accent-dark);
  text-decoration: underline;
}
.lang-switch a:hover { color: var(--color-text); }

.nav-toggle {
  display: flex;
  flex-direction: column;
  gap: 5px;
  width: 32px;
  height: 32px;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  cursor: pointer;
}
.nav-toggle span {
  display: block;
  width: 22px;
  height: 2px;
  background: #fff;
  transition: transform 0.2s ease, opacity 0.2s ease;
}
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* ========================================================================
   Hero
   ======================================================================== */
.hero {
  /* 加高 + 底部弧线：530px 撑不起开场，弧线让深色 hero 柔和地流进浅色内容区。
     媒体层切底角、露出页面底色 —— 换成真实照片后弧线依然成立。 */
  position: relative;
  min-height: min(72vh, 660px);
  display: flex;
  align-items: center;
  border-radius: 0 0 var(--hero-arc) var(--hero-arc);
  /* 真照片进来了，装饰性的金色 radial 就撤掉 —— 日落本身就是那个暖色。
     渐变自左向右收：文字那半边压得住，右半边把石灰岩和天空放出来。 */
  background:
    /* 底部压到不透明：照片下缘那块告示牌是 AI 生成的假招牌，
       靠裁切在某些视窗高度下挡不住，用渐变收掉才稳 */
    linear-gradient(180deg, transparent 52%, rgba(0, 27, 69, 0.55) 84%,
      var(--color-navy-deep) 100%),
    linear-gradient(100deg,
      var(--color-navy-deep) 0%,
      rgba(0, 27, 69, 0.90) 26%,
      rgba(0, 45, 108, 0.66) 50%,
      rgba(0, 45, 108, 0.30) 74%,
      rgba(0, 27, 69, 0.30) 100%),
    url('../images/hero/ipoh-skyline.jpg');
  background-size: cover, cover, cover;
  background-position: center, center, center 38%;
  background-repeat: no-repeat;
  color: #fff;
  padding: var(--space-6) 0;
}
/* 手机上文字横跨整幅，横向渐变挡不住 —— 换成整体压深、底部收到不透明 */
@media (max-width: 767px) {
  .hero {
    background:
      linear-gradient(180deg,
        rgba(0, 27, 69, 0.84) 0%,
        rgba(0, 27, 69, 0.74) 34%,
        rgba(0, 27, 69, 0.93) 58%,
        var(--color-navy-deep) 74%),
      url('../images/hero/ipoh-skyline.jpg');
    background-size: cover, cover;
    background-position: center, 68% 36%;
  }
}
.hero-inner { max-width: 720px; }
.eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.22em;
  font-size: 0.8rem;
  font-weight: 600;
  opacity: 0.85;
  margin-bottom: var(--space-2);
}
.hero h1 {
  font-size: clamp(2rem, 4.5vw, 3rem);
  line-height: 1.15;
  letter-spacing: -0.01em;
}
.hero-sub {
  font-size: 1.05rem;
  opacity: 0.92;
  max-width: 560px;
}
.hero-ctas {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.hero .btn-outline { border-color: #fff; color: #fff; }
.hero .btn-outline:hover { background: #fff; color: var(--color-text); }

/* ========================================================================
   Trust bar
   ======================================================================== */
.trust-bar {
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
}
.trust-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-3);
  padding: var(--space-4) var(--space-3);
  text-align: center;
}
@media (min-width: 768px) {
  .trust-grid { grid-template-columns: repeat(4, 1fr); }
}
.trust-number {
  display: block;
  font-size: 1.5rem;
  font-weight: 800;
  color: var(--color-accent-dark);
}
.trust-label {
  display: block;
  font-size: 0.85rem;
  color: var(--color-text-muted);
  margin-top: 0.2rem;
}

/* ========================================================================
   Sections
   ======================================================================== */
.section { padding: var(--space-6) 0; }
.section-alt { background: var(--color-surface); }
.section-title {
  font-size: clamp(1.5rem, 3vw, 2rem);
  letter-spacing: -0.01em;
}
/* Thin accent rule floating to the RIGHT of the title, vertically centred,
   instead of a bar underneath it. Scoped with :not() so it never collides
   with .section-title-underline (the detail pages' gold under-bar, which
   owns its own ::after), and excluded from the one centred title where a
   right-hand rule would read as lopsided. Uses --color-accent-dark, not
   --color-accent: at 2px on our light background the lighter gold is too
   faint to register. */
.section-title:not(.section-title-underline) {
  position: relative;
  width: fit-content;
  max-width: 100%;
}
.section-title:not(.section-title-underline)::after {
  content: "";
  position: absolute;
  right: -90px;
  top: 0;
  bottom: 0;
  margin: auto 0;
  width: 70px;
  height: 2px;
  background: var(--color-accent-dark);
}
.feature-highlights .section-title::after { content: none; }
@media (max-width: 768px) {
  /* Fall back to a normal underline — there is no room beside the title
     on a phone, and a rule hanging off the edge would force a scrollbar. */
  .section-title:not(.section-title-underline)::after {
    right: unset;
    left: 0;
    top: unset;
    bottom: -10px;
    margin: 0;
    width: 64px;
  }
}
.section-sub {
  color: var(--color-text-muted);
  margin-bottom: var(--space-4);
}

/* Placeholder photo blocks — remove once real images are added */
.placeholder-photo {
  display: flex;
  align-items: center;
  justify-content: center;
  background: repeating-linear-gradient(
    45deg, #EDE6DC, #EDE6DC 10px, #E4DACB 10px, #E4DACB 20px
  );
  color: var(--color-text-muted);
  font-size: 0.85rem;
  font-weight: 600;
}

/* ========================================================================
   Listings
   ======================================================================== */
.listing-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
}
@media (min-width: 640px) {
  .listing-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 992px) {
  .listing-grid { grid-template-columns: repeat(3, 1fr); }
}

.listing-card {
  background: var(--color-surface);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  border: 1px solid transparent;
  display: flex;
  flex-direction: column;
  transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.listing-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-card-hover);
  border-color: var(--color-accent);
}
.listing-photo { height: 200px; position: relative; }
.listing-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }

.hot-badge {
  position: absolute;
  top: 10px;
  left: 10px;
  background: var(--color-accent);
  /* Navy, not white. White on this gold measures 1.88:1 - far under the 4.5
     a 12px label needs - which is why the badge read as barely there even
     with its background present. Navy on the same gold is 8.98:1. */
  color: var(--color-navy-deep);
  font-size: 0.75rem;
  font-weight: 700;
  padding: 0.3rem 0.7rem;
  border-radius: 999px;
  z-index: 1;
}

/* Dark card body — the reference layout the owner asked for. */
.listing-body {
  padding: var(--space-3);
  background: var(--color-navy-deep);
  color: #fff;
  flex: 1;
  display: flex;
  flex-direction: column;
}
.listing-title {
  font-size: 1.15rem;
  color: #fff;
  margin: 0 0 var(--space-2);
}

.listing-chips {
  list-style: none;
  margin: 0 0 var(--space-2);
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.listing-chips li {
  border: 1px solid rgba(255, 255, 255, 0.24);
  border-radius: 999px;
  padding: 4px 11px;
  font-size: 0.78rem;
  line-height: 1.35;
  color: rgba(255, 255, 255, 0.86);
  white-space: nowrap;
}

.listing-blurb {
  font-size: 0.9rem;
  line-height: 1.6;
  color: rgba(255, 255, 255, 0.72);
  margin: 0 0 var(--space-3);
}

/* Pushes the price and buttons to the bottom so cards of unequal text still
   line their prices up across a row. */
.listing-foot { margin-top: auto; }
.listing-from {
  font-size: 0.68rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
  margin: 0 0 2px;
}
.listing-price {
  font-weight: 800;
  font-size: 1.5rem;
  line-height: 1.25;
  letter-spacing: -0.01em;
  color: #fff;
  margin: 0 0 var(--space-3);
  /* Both price variants share one box, and the label row is always present,
     so prices line up across a row however the copy above them wraps. */
  min-height: 1.875rem;
  display: flex;
  align-items: flex-end;
}
.listing-price.is-status {
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--color-accent);
  letter-spacing: 0;
}

.listing-card-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}
.listing-card-actions .btn { flex: 1 1 140px; border-radius: 999px; }
.listing-card-actions .btn-outline {
  border-color: rgba(255, 255, 255, 0.45);
  color: #fff;
}
.listing-card-actions .btn-outline:hover {
  background: #fff;
  border-color: #fff;
  color: var(--color-navy-deep);
}
.listing-cta { width: 100%; }

/* Restored after the card restyle removed them: the filter bar, the Hot
   Selling heading and the area/type group headings. */
.category-filter {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-bottom: var(--space-4);
}
.filter-btn {
  font-family: inherit;
  font-size: 0.88rem;
  font-weight: 600;
  padding: 0.5rem 1.1rem;
  border-radius: var(--radius-pill);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  color: var(--color-text-muted);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.filter-btn:hover { border-color: var(--color-accent); color: var(--color-text); }
.filter-btn.is-active {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: #fff;
}
.hot-selling { margin-bottom: var(--space-5); }
.hot-selling-title {
  font-size: 1.15rem;
  margin-bottom: var(--space-3);
}
.location-group[hidden] { display: none; }
.location-group + .location-group { margin-top: var(--space-6); }
.location-group-title {
  font-size: 1.35rem;
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-1);
  border-bottom: 2px solid var(--color-accent);
}
.type-group + .type-group { margin-top: var(--space-4); }
.type-group-title {
  font-size: 0.85rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-text-muted);
  margin-bottom: var(--space-2);
}

/* ========================================================================
   Areas
   ======================================================================== */
.areas-grid {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}
.area-chip {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  padding: 0.5rem 1.1rem;
  font-size: 0.9rem;
  font-weight: 500;
}

/* ========================================================================
   Tools
   ======================================================================== */
.service-panel[hidden] { display: none; }

.tool-card-photo {
  height: 180px;
  margin: calc(-1 * var(--space-4)) calc(-1 * var(--space-4)) var(--space-3);
  border-radius: var(--radius) var(--radius) 0 0;
}

.tool-card {
  background: var(--color-surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  border: 1px solid transparent;
  padding: var(--space-4);
  transition: box-shadow 0.2s ease, border-color 0.2s ease;
}
.tool-card:hover {
  box-shadow: var(--shadow-card-hover);
  border-color: var(--color-accent);
}

/* Loan calculator: inputs on the left, cost breakdown on the right, so the
   card reads as two panels side by side instead of one long stacked column. */
.loan-tool-body {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
}
@media (min-width: 760px) {
  .loan-tool-body {
    grid-template-columns: minmax(0, 340px) 1fr;
    align-items: start;
  }
  .loan-tool-costs {
    border-left: 1px solid var(--color-border);
    padding-left: var(--space-4);
  }
}
.tool-title { font-size: 1.15rem; margin-bottom: 0.2rem; }
.tool-desc {
  color: var(--color-text-muted);
  font-size: 0.9rem;
  margin-bottom: var(--space-3);
}
.tool-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}
.tool-form label {
  font-size: 0.85rem;
  font-weight: 600;
  margin-top: var(--space-1);
}
.tool-form input {
  font-family: inherit;
  font-size: 0.95rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: var(--color-bg);
  color: var(--color-text);
}
.form-hint {
  font-size: 0.78rem;
  color: var(--color-text-muted);
  margin: 0.25rem 0 0;
}

/* Schedule-a-viewing form (2026-08-16, by request) — the site's only form.
   It still has no backend: wireViewingForm() in js/main.js assembles the
   fields into a pre-filled wa.me message on submit, per the standing
   "never POST to a server" rule. Reuses .form-hint above for the caption. */
.viewing-form { margin-top: var(--space-4); }
.viewing-form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-3);
}
.viewing-field { display: flex; flex-direction: column; gap: 0.35rem; }
.viewing-field > span {
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}
.viewing-field input,
.viewing-field select {
  font-family: inherit;
  font-size: 0.95rem;
  padding: 0.7rem 0.8rem;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: var(--color-surface);
  color: var(--color-text);
  width: 100%;
  transition: border-color 0.2s ease;
}
.viewing-field input:focus,
.viewing-field select:focus {
  outline: none;
  border-color: var(--color-accent-dark);
}
.viewing-form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
}
.tool-result {
  margin-top: var(--space-3);
  padding: var(--space-2) var(--space-3);
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  display: flex;
  flex-direction: column;
}
.tool-result-label {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.tool-result-value {
  font-size: 1.6rem;
  font-weight: 800;
  color: var(--color-accent-dark);
}
.tool-result-sub {
  font-size: 0.8rem;
  color: var(--color-text-muted);
}
.tool-disclaimer {
  font-size: 0.78rem;
  color: var(--color-text-muted);
  margin: var(--space-2) 0 0;
}

/* ========================================================================
   Cost breakdown (loan calculator) + PDF export
   ======================================================================== */
.cost-breakdown-title {
  font-size: 0.95rem;
  margin: var(--space-3) 0 var(--space-1);
}
.cost-list {
  margin: 0 0 var(--space-2);
  border-top: 1px solid var(--color-border);
}
.cost-row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  padding: 0.45rem 0;
  border-bottom: 1px solid var(--color-border);
  font-size: 0.88rem;
}
.cost-row dt { color: var(--color-text-muted); margin: 0; }
.cost-row dd { margin: 0; font-weight: 600; }
.cost-row-total {
  border-bottom: none;
  padding-top: 0.6rem;
  font-size: 0.98rem;
}
.cost-row-total dt,
.cost-row-total dd { color: var(--color-text); font-weight: 800; }

/* DSR (Debt Service Ratio) check — income/commitment inputs on the left,
   obligation breakdown + result on the right, same 2-column pattern as
   .loan-tool-body so it reads consistently within the same card. */
/* The loan calculator card is split into two sub-tabs — "Loan & Purchase
   Cost Calculator" and "DSR Check" — because stacked in one column the card
   was too long to scan. Both halves stay in the same #loan-tool-card so the
   PDF export can still print them together (see @media print below).
   Reuses the .category-filter / .filter-btn styling of the other filters. */
.tool-subfilter { margin-bottom: var(--space-3); }
.tool-subpanel[hidden] { display: none; }

/* No separator needed on screen — the DSR block now starts its own tab.
   It's restored for print, where both halves stack in one report. */
.dsr-section { margin-top: 0; }
.dsr-body {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
}
@media (min-width: 760px) {
  .dsr-body {
    grid-template-columns: minmax(0, 340px) 1fr;
    align-items: start;
  }
  .dsr-results {
    border-left: 1px solid var(--color-border);
    padding-left: var(--space-4);
  }
}
.dsr-result { margin-top: var(--space-2); border-width: 2px; }
.dsr-result.status-good { border-color: var(--color-whatsapp); }
.dsr-result.status-good .tool-result-value { color: var(--color-whatsapp-dark); }
.dsr-result.status-warn { border-color: var(--color-accent); }
.dsr-result.status-warn .tool-result-value { color: var(--color-accent-dark); }
.dsr-result.status-bad { border-color: #C0392B; }
.dsr-result.status-bad .tool-result-value { color: #C0392B; }
#send-dsr-whatsapp-btn { margin-top: var(--space-2); width: 100%; }

.pdf-header,
.pdf-footer,
.pdf-summary { display: none; }

@media print {
  @page { margin: 14mm 12mm; }

  /* `visibility: hidden` leaves the hidden page IN FLOW, so the printed
     document stayed as tall as the whole homepage — that is where the six
     trailing blank pages came from. `display: none` on everything that is
     neither the report, an ancestor of it, nor inside it actually collapses
     the document to the report's own height. */
  body *:not(:has(#loan-tool-card)):not(#loan-tool-card):not(#loan-tool-card *) {
    display: none !important;
  }
  html, body { height: auto !important; }
  #loan-tool-card, #loan-tool-card * {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  #loan-tool-card {
    width: 100%;
    box-shadow: none;
    border: none;
    padding: 0;
    /* Loan summary and cost breakdown side by side, so the whole purchase
       estimate lands on page one. `display: contents` on .loan-tool-body
       lifts its two panels into this grid. */
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 20px;
    align-items: start;
  }
  #loan-tool-card > .pdf-header,
  #loan-tool-card > .pdf-footer,
  #loan-tool-card > .dsr-section { grid-column: 1 / -1; }
  #loan-tool-card .loan-tool-body { display: contents !important; }
  #loan-tool-card .loan-tool-inputs { display: none !important; }
  #loan-tool-card > .pdf-summary { grid-column: 1; margin-bottom: 0; }
  #loan-tool-card .loan-tool-costs { grid-column: 2; }
  #loan-tool-card .loan-tool-costs .cost-breakdown-title { margin-top: 0; }

  /* The DSR half starts its own page, and is skipped entirely when the
     visitor never filled the form in — a page of "RM 0" rows is worse than
     no page at all. */
  #loan-tool-card > .dsr-section { break-before: page; page-break-before: always; }
  /* Must out-specify the `.tool-subpanel[hidden] { display: block }`
     rule below, which is the same specificity and declared later. */
  #loan-tool-card[data-dsr="empty"] > .dsr-section.tool-subpanel { display: none !important; }

  /* Take-home pay and existing commitments read side by side too. */
  #loan-tool-card .dsr-results {
    display: grid !important;
    grid-template-columns: 1fr 1fr;
    column-gap: 20px;
    align-items: start;
  }
  #loan-tool-card .dsr-block:nth-of-type(1) { grid-column: 1; }
  #loan-tool-card .dsr-block:nth-of-type(2) { grid-column: 2; }
  #loan-tool-card .dsr-results > :not(.dsr-block) { grid-column: 1 / -1; }
  #loan-tool-card .dsr-block .cost-breakdown-title { margin-top: 0; }
  /* The two DSR verdict boxes pair up as well, which is what keeps the
     whole DSR half inside a single printed page. */
  #loan-tool-card #dsr-result { grid-column: 1; }
  #loan-tool-card #dsr-result-net { grid-column: 2; }
  #loan-tool-card .tool-form,
  #loan-tool-card .tool-desc,
  #loan-tool-card .tool-title,
  #loan-tool-card .tool-card-photo,
  #loan-tool-card #tool-subfilter,
  #loan-tool-card #send-loan-whatsapp-btn,
  #loan-tool-card #send-dsr-whatsapp-btn {
    display: none !important;
  }

  /* The two sub-tabs are a screen affordance only — the printed report
     must contain BOTH halves, so force the inactive one visible and give
     the DSR block back its separator rule. */
  #loan-tool-card .tool-subpanel[hidden] { display: block !important; }
  #loan-tool-card .dsr-section {
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--color-border);
  }

  /* Report reads as one full-width column instead of the on-screen
     2-column layout — the side-by-side panels made sense for an
     interactive card, not a printed page. */
  #loan-tool-card .dsr-body { display: block !important; }
  #loan-tool-card .loan-tool-costs,
  #loan-tool-card .dsr-results {
    border-left: none !important;
    padding-left: 0 !important;
  }
  /* The on-screen "Est. Monthly Installment" box is superseded by
     .pdf-summary below — hide it to avoid showing the number twice.
     Scoped to .loan-tool-inputs only so it doesn't catch the DSR result
     boxes further down, which also carry the shared .tool-result class. */
  #loan-tool-card .loan-tool-inputs .tool-result { display: none !important; }

  .pdf-header,
  .pdf-footer,
  .pdf-summary { display: block !important; }

  .pdf-header {
    display: flex !important;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    background: linear-gradient(135deg, var(--color-navy) 0%, var(--color-navy-deep) 100%);
    color: #fff;
    padding: 16px 20px;
    margin: 0 0 16px;
    border-radius: 8px;
    border-bottom: 3px solid var(--color-accent);
  }
  .pdf-header-brand { display: flex; align-items: center; gap: 12px; }
  .pdf-header-logo {
    width: 42px;
    height: 42px;
    border-radius: 6px;
    object-fit: cover;
  }
  .pdf-business { font-weight: 700; font-size: 1.05rem; margin: 0; color: #fff; }
  .pdf-header-ren { font-size: 0.72rem; color: var(--color-accent); margin: 2px 0 0; letter-spacing: 0.02em; }
  .pdf-header-doc { text-align: right; }
  .pdf-header h3 { margin: 0; font-size: 1rem; color: #fff; }
  .pdf-date { color: rgba(255, 255, 255, 0.75); margin: 2px 0 0; font-size: 0.78rem; }

  .pdf-summary {
    border: 1px solid var(--color-border);
    border-left: 4px solid var(--color-accent-dark);
    border-radius: 8px;
    padding: 14px 18px;
    margin: 0 0 18px;
    background: #FAFCFE;
  }
  .pdf-summary-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 2px 24px;
  }
  .pdf-summary-item {
    display: flex;
    justify-content: space-between;
    gap: var(--space-2);
    border-bottom: 1px dotted var(--color-border);
    padding: 4px 0;
    font-size: 0.85rem;
  }
  .pdf-summary-item span { color: var(--color-text-muted); }
  .pdf-summary-item strong { font-weight: 700; text-align: right; }
  .pdf-summary-monthly {
    margin-top: 12px;
    padding-top: 12px;
    border-top: 2px solid var(--color-navy);
    display: flex;
    align-items: baseline;
    justify-content: space-between;
  }
  .pdf-summary-monthly span {
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--color-text-muted);
  }
  .pdf-summary-monthly strong { font-size: 1.4rem; color: var(--color-navy); font-weight: 800; }

  #loan-tool-card .cost-breakdown-title {
    color: var(--color-navy);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: 0.8rem;
    border-bottom: 2px solid var(--color-accent);
    padding-bottom: 4px;
    margin-top: 22px;
  }
  #loan-tool-card .cost-list { border-top: none; }
  #loan-tool-card .cost-row {
    padding: 6px 8px;
    margin: 0;
    border-bottom: 1px solid var(--color-border);
  }
  #loan-tool-card .cost-row:nth-child(even) { background: #F2F7FB; }
  #loan-tool-card .cost-row-total {
    background: var(--color-navy) !important;
    color: #fff;
    border-radius: 6px;
    margin-top: 6px;
    padding: 8px;
  }
  #loan-tool-card .cost-row-total dt,
  #loan-tool-card .cost-row-total dd { color: #fff; }

  #loan-tool-card .dsr-result { border-width: 1px; margin-top: 10px; }
  #loan-tool-card .listing-note { margin-top: var(--space-2); }

  .pdf-footer {
    margin-top: 22px;
    padding-top: 10px;
    border-top: 1px solid var(--color-border);
    font-size: 0.72rem;
    color: var(--color-text-muted);
    text-align: center;
  }
}

/* ========================================================================
   FAQ
   ======================================================================== */
.faq-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: 760px;
}
.faq-item {
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-3);
}
.faq-item summary {
  cursor: pointer;
  font-family: var(--font-heading);
  font-weight: 600;
  list-style: none;
  padding: 0.3rem 1.6rem 0.3rem 0;
  position: relative;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
  content: "+";
  position: absolute;
  right: 0;
  top: 0.3rem;
  font-size: 1.2rem;
  color: var(--color-accent-dark);
}
.faq-item[open] summary::after { content: "\2212"; }
.faq-item p {
  color: var(--color-text-muted);
  margin: var(--space-1) 0 0.3rem;
}

/* ========================================================================
   Listing detail page (e.g. santorini-botani.html) — a deeper single-project
   spotlight page, distinct from the grid-card pattern used on the homepage:
   a navy detail hero, a spec table, and grouped facility chips instead of
   another card grid.
   ======================================================================== */
.detail-hero {
  /* Palette tokens only. This rule used to carry hardcoded pre-pivot
     colours (#0B1220 / #16223D / #1F2E4D and a rgba(201,162,74) gold) left
     over from before the site-wide Santorini palette pivot. Every detail
     page currently also carries .detail-hero-photo, which sets
     `background: none`, so the stale gradient was masked and invisible —
     but any future detail page using plain .detail-hero would have
     rendered off-palette. */
  background:
    radial-gradient(ellipse 900px 500px at 12% -10%, rgba(213, 185, 141, 0.22), transparent 60%),
    linear-gradient(135deg, var(--color-navy-deep), var(--color-navy) 55%, var(--color-navy-light));
  color: #fff;
  padding: var(--space-5) 0 var(--space-6);
}
.detail-breadcrumb { margin: 0 0 var(--space-2); }
.detail-breadcrumb a {
  font-size: 0.85rem;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.75);
}
.detail-breadcrumb a:hover { color: #fff; }
.detail-hero h1 {
  font-size: clamp(2rem, 4.5vw, 2.75rem);
  line-height: 1.15;
}
.detail-quickfacts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin: var(--space-3) 0;
  font-size: 0.9rem;
  font-weight: 600;
}
.detail-quickfacts li {
  padding-left: var(--space-2);
  position: relative;
}
.detail-quickfacts li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.5em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-accent);
}

.detail-photo {
  height: 280px;
  border-radius: var(--radius);
  margin-bottom: var(--space-4);
  overflow: hidden;
}
.detail-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Embedded location map (2026-08-14, by request) — a keyless Google Maps
   embed (no API key/billing needed, unlike the official Maps Embed API),
   used where the page has a real, developer-confirmed address to show.
   Followed by a "Get Directions" link using Google's plain Maps URL
   scheme (also keyless). First used on klemeru-township.html's Location
   section, replacing what was previously an all-placeholder Lifestyle
   Gallery. */
.detail-map {
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-card);
  margin-bottom: var(--space-3);
}
.detail-map iframe { width: 100%; height: 400px; display: block; border: 0; }

.listing-note {
  font-size: 0.88rem;
  color: var(--color-text-muted);
  border-left: 3px solid var(--color-accent);
  padding-left: var(--space-2);
}

/* Answer-first lede + fact sheet (web-design-skills AEO pattern) — a
   one-paragraph answer to "what is this" up top, then key numbers as a
   spec sheet (hairline rows, no card chrome) rather than another row of
   boxed stats. First used on taman-lapangan-indah.html's Overview
   section; reuse for any future detail page's opening section instead of
   a plain paragraph. */
.answer-first {
  border-left: 2px solid var(--color-accent-dark);
  padding-left: 1.4rem;
  max-width: 68ch;
}
.answer-first .lede { font-size: 1.05rem; margin-bottom: 0; }

.factgrid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 0;
  border-top: 1px solid var(--color-border);
  margin: var(--space-4) 0;
}
.fact {
  padding: var(--space-3) var(--space-3) var(--space-3) 0;
  border-bottom: 1px solid var(--color-border);
}
.fact dt {
  font-size: 0.66rem;
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: 0.3rem;
}
.fact dd {
  margin: 0;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: clamp(1.05rem, 1rem + 0.5vw, 1.3rem);
  color: var(--color-navy);
}
.fact dd em {
  display: block;
  font-style: normal;
  font-family: var(--font-base);
  font-weight: 400;
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--color-text-muted);
  margin-top: 0.2rem;
}

/* Dark-hero outline button needs its own contrast fix — the base
   .btn-outline is dark-on-transparent, unreadable on a navy hero. Mirrors
   the same fix already applied to .hero .btn-outline. */
.detail-hero .btn-outline { border-color: rgba(255, 255, 255, 0.85); color: #fff; }
.detail-hero .btn-outline:hover { background: #fff; color: var(--color-navy); }

.facility-chip {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  padding: 0.5rem 1.1rem;
  font-size: 0.9rem;
  font-weight: 500;
}

.detail-cta-inner { max-width: 560px; margin: 0 auto; text-align: center; }
.detail-cta-inner .hero-ctas { justify-content: center; }

/* Lifestyle gallery — Swiper carousel (see santorini-botani.html), themed
   to the site's navy/gold palette. Swap each .gallery-slide-photo
   placeholder for a real <img> once photography exists. */
.gallery-swiper { padding-bottom: var(--space-5); }
.gallery-slide-photo { height: 320px; border-radius: var(--radius); overflow: hidden; }
.gallery-slide-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gallery-swiper .swiper-button-next,
.gallery-swiper .swiper-button-prev {
  color: var(--color-navy);
  background: var(--color-surface);
  width: 40px;
  height: 40px;
  border-radius: 50%;
  box-shadow: var(--shadow-card);
}
.gallery-swiper .swiper-button-next::after,
.gallery-swiper .swiper-button-prev::after { font-size: 1rem; font-weight: 700; }
.gallery-swiper .swiper-pagination-bullet-active { background: var(--color-accent-dark); }

/* Feature icon strip (3 short highlights under the overview paragraph) */
.feature-strip {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
  margin: var(--space-4) 0 0;
}
@media (min-width: 640px) {
  .feature-strip { grid-template-columns: repeat(3, 1fr); }
}
.feature-item { text-align: center; }
.feature-icon {
  width: 56px;
  height: 56px;
  margin: 0 auto var(--space-2);
  border-radius: 50%;
  background: var(--color-navy);
  display: flex;
  align-items: center;
  justify-content: center;
}
.feature-icon svg {
  width: 26px;
  height: 26px;
  fill: none;
  stroke: #fff;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.feature-item h3 { font-size: 1rem; margin-bottom: 0.2rem; }
.feature-item p { color: var(--color-text-muted); font-size: 0.88rem; margin: 0; }

/* Photo flip-cards (2026-08-14, by request, referencing eadepro.com's own
   "Key Features" photo grid) — front face shows a real photo, hover/focus
   flips the card 180° to reveal the heading+description on the back. A
   separate opt-in component from .feature-strip above (which stays plain
   icon+text and is still used site-wide on the homepage, Santorini, Taman
   Lapangan Indah, and Anderson Residences) — don't merge the two or this
   change ripples into pages that have no photo to show on the front face.
   First used on klemeru-township.html's Overview section. */
.feature-flip-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3);
  margin: var(--space-4) 0 0;
}
@media (min-width: 640px) {
  .feature-flip-grid { grid-template-columns: repeat(2, 1fr); }
}
.feature-flip-card { perspective: 1200px; height: 260px; }
@media (min-width: 640px) {
  .feature-flip-card { height: 320px; }
}
.feature-flip-inner {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: var(--radius);
  transition: transform 0.7s var(--ease);
  transform-style: preserve-3d;
}
.feature-flip-card:hover .feature-flip-inner,
.feature-flip-card:focus-within .feature-flip-inner {
  transform: rotateY(180deg);
}
.feature-flip-front, .feature-flip-back {
  position: absolute;
  inset: 0;
  overflow: hidden;
  border-radius: var(--radius);
  backface-visibility: hidden;
}
.feature-flip-front img { width: 100%; height: 100%; object-fit: cover; display: block; }
.feature-flip-label {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  margin: 0;
  padding: var(--space-3) var(--space-3) var(--space-2);
  background: linear-gradient(0deg, rgba(0, 27, 69, 0.9) 0%, rgba(0, 27, 69, 0.55) 60%, rgba(0, 27, 69, 0) 100%);
  color: #fff;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: 0.01em;
}
.feature-flip-back {
  transform: rotateY(180deg);
  background: linear-gradient(135deg, var(--color-navy) 0%, var(--color-navy-deep) 100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--space-3);
}
.feature-flip-back h3 { color: #fff; font-size: 1rem; margin-bottom: 0.3rem; }
.feature-flip-back p { color: rgba(255, 255, 255, 0.85); font-size: 0.88rem; margin: 0; }
@media (prefers-reduced-motion: reduce) {
  .feature-flip-inner { transition: none; }
}

.feature-highlights { padding-top: var(--space-5); padding-bottom: var(--space-5); }
.feature-highlights .section-title { text-align: center; }

/* Unit type cards — replaces a plain comparison table with a per-type
   card (closer to how the developer's own site presents each floor plan) */
.unit-type-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3);
}
@media (min-width: 640px) {
  .unit-type-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 992px) {
  .unit-type-grid { grid-template-columns: repeat(3, 1fr); }
}
.unit-type-card {
  background: var(--color-cream);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.unit-type-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-card-hover);
}
.unit-type-card .placeholder-photo { height: 170px; }
/* Stable wrapper class (2026-08-14) — carries the size regardless of
   placeholder/real-photo state. When swapping in a real photo, add this
   class to the existing photo div, drop .placeholder-photo, and nest an
   <img>; the height keeps applying with no further CSS changes needed.
   Bumped from 130px to 170px by request — still landscape/wide, not a
   square, matching this site's photography-is-always-landscape
   convention (see Design Direction in CLAUDE.md). */
.unit-type-photo { height: 170px; overflow: hidden; }
.unit-type-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.unit-type-card-body { padding: var(--space-3); }
.unit-type-name {
  font-size: 1.1rem;
  margin-bottom: 0.2rem;
  color: var(--color-navy);
}
.unit-type-size {
  font-weight: 700;
  color: var(--color-accent-dark);
  margin-bottom: var(--space-2);
}
.unit-type-specs {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  font-size: 0.88rem;
  color: var(--color-text-muted);
}
.unit-type-highlights {
  margin: var(--space-1) 0 0;
  padding-top: var(--space-1);
  border-top: 1px solid var(--color-border);
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

/* Facility accordion — same native <details> pattern as the FAQ section,
   styled as a gold bar per category instead of a plain card. */
.facility-accordion {
  border: none;
  border-radius: 8px;
  background: var(--color-accent);
  margin-bottom: var(--space-1);
  overflow: hidden;
}
.facility-accordion summary {
  cursor: pointer;
  list-style: none;
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-heading);
  font-weight: 600;
  color: var(--color-navy-deep);
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.facility-accordion summary::-webkit-details-marker { display: none; }
.facility-accordion summary::after { content: "+"; font-size: 1.2rem; }
.facility-accordion[open] summary::after { content: "\2212"; }
.facility-accordion-body {
  padding: 0 var(--space-3) var(--space-3);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}
.facility-accordion .facility-chip {
  background: rgba(255, 255, 255, 0.65);
  border-color: transparent;
}

/* ========================================================================
   Reference-style detail-page enhancements — scoped via modifier classes
   (.detail-hero-photo, .hero-highlight-card, .amenity-row, .split-feature-row,
   .icon-grid, .benefit-grid) so they only affect pages that opt in. Added
   2026-08-08 for santorini-botani.html (matching santorinibotaniipoh.com.my:
   photo-led hero, floating stat card, arch-topped location cards,
   alternating lifestyle rows, icon grid, investment-benefit cards) and
   reused 2026-08-09 for klemeru-township.html (matching eadepro.com) —
   both keep our own original copy and no developer name/logo. The plain
   .detail-hero (no modifier) is still available for any future detail
   page that doesn't want this treatment.
   ======================================================================== */
.detail-hero-photo { position: relative; overflow: hidden; background: none; isolation: isolate; }
.detail-hero-photo::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    radial-gradient(ellipse 900px 500px at 12% -10%, rgba(213, 185, 141, 0.25), transparent 60%),
    linear-gradient(135deg, rgba(0, 27, 69, 0.9), rgba(0, 45, 108, 0.74) 55%, rgba(18, 58, 122, 0.62));
}
.detail-hero-photo .detail-hero-bg { position: absolute; inset: 0; z-index: 0; }
.detail-hero-photo .container { position: relative; z-index: 2; }
/* Slow settle-in on the hero photo. Deliberately scoped to a real <img>,
   so it stays inert while .detail-hero-bg is still a placeholder block
   (animating a diagonal-stripe placeholder would just look broken) and
   switches itself on the moment real hero photography drops in. */
.detail-hero-bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  animation: hero-settle 2.4s var(--ease) forwards;
}
@keyframes hero-settle {
  from { transform: scale(1.07); }
  to   { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .detail-hero-bg img { animation: none; transform: none; }
}

/* Auto-sliding hero photo carousel (2026-08-16, by request) — reuses the
   Swiper instance already loaded on the page for the unit-type photo
   galleries rather than hand-rolling slide logic. Fills .detail-hero-bg
   exactly like a plain <img> did before; autoplay only, no nav/pagination
   since this is an ambient background, not an interactive gallery — see
   the .hero-swiper init alongside the .unit-swiper one in the page's
   inline script. First (and currently only) use: klemeru-township.html. */
.hero-swiper, .hero-swiper .swiper-wrapper, .hero-swiper .swiper-slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.hero-swiper .swiper-slide img { width: 100%; height: 100%; object-fit: cover; display: block; }

.detail-hero-tagline {
  font-family: var(--font-heading);
  font-size: clamp(1.5rem, 3.2vw, 2.15rem);
  font-weight: 600;
  line-height: 1.25;
  margin: var(--space-1) 0 var(--space-2);
}
.detail-hero-tagline span { display: block; color: var(--color-accent); }

/* Hero text underlay (web-design-skills technique) — a blurred navy glow
   sitting behind the hero text block, on top of .detail-hero-photo's own
   global scrim. Tinted navy rather than black per the palette rule. Only
   pays off over real photography, so it was left unused until a detail
   page shipped with a real hero photo — see taman-lapangan-indah.html,
   the first to use it. Safe to reuse on any future real-photo hero. */
.hero-text-glow { position: relative; }
.hero-text-glow::before {
  content: "";
  position: absolute;
  left: -10%;
  top: -20%;
  width: 120%;
  height: 140%;
  background: var(--color-navy-deep);
  opacity: 0.55;
  filter: blur(60px);
  z-index: -1;
}

/* Floating highlight card that overlaps the hero's bottom edge — a sibling
   of the hero section (not nested inside it), so the negative margin pulls
   it up over the hero without being clipped by the hero's own overflow:hidden. */
.hero-highlight-card {
  position: relative;
  z-index: 1;
  background: var(--color-surface);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card-hover);
  margin-top: calc(-1 * var(--space-4));
  padding: var(--space-5) var(--space-3) var(--space-1);
}
.hero-highlight-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3);
}
@media (min-width: 640px) {
  .hero-highlight-grid { grid-template-columns: repeat(2, 1fr); }
  .hero-highlight-grid .hero-highlight-item:nth-child(-n+2) {
    border-bottom: 1px solid var(--color-border);
    padding-bottom: var(--space-3);
  }
  .hero-highlight-grid .hero-highlight-item:nth-child(odd) {
    border-right: 1px solid var(--color-border);
  }
}
.hero-highlight-item { text-align: center; padding: 0 var(--space-2) var(--space-3); }
.hero-highlight-icon {
  width: 48px;
  height: 48px;
  margin: 0 auto var(--space-1);
  border-radius: 50%;
  background: var(--color-navy);
  display: flex;
  align-items: center;
  justify-content: center;
}
.hero-highlight-icon svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: #fff;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.hero-highlight-item h3 { font-size: 0.95rem; margin-bottom: 0.2rem; }
.hero-highlight-item p { font-size: 0.82rem; color: var(--color-text-muted); margin: 0; }

/* Photo-band amenity showcase (2026-08-16, by request) — a full-bleed photo
   band with a centered heading/subtitle, and a floating white card
   overlapping its bottom edge (photo left, checklist right). Reuses the
   same negative-margin-overlap + isolation pattern as .hero-highlight-card
   above (including the same GPU-compositing fix) so an animated background
   never bleeds into the card. Replaces .facility-accordion for facts that
   earn full visual treatment — currently Park and Solar on
   klemeru-township.html. Reuse for any future page that wants to spotlight
   one amenity/feature set this way instead of a chip accordion. */
.showcase-band {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  padding: var(--space-6) 0 calc(var(--space-6) + 60px);
  text-align: center;
  color: #fff;
}
.showcase-band-bg { position: absolute; inset: 0; z-index: 0; }
.showcase-band-bg img { width: 100%; height: 100%; object-fit: cover; display: block; }
.showcase-band::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(135deg, rgba(0, 27, 69, 0.85), rgba(0, 45, 108, 0.7) 55%, rgba(18, 58, 122, 0.55));
}
.showcase-band .container { position: relative; z-index: 2; }
.showcase-band h2, .showcase-band h3 { color: #fff; }
.showcase-band .section-title { text-align: center; margin-left: auto; margin-right: auto; }
.showcase-band .section-title::after { content: none; }
.showcase-band .section-sub {
  color: rgba(255, 255, 255, 0.85);
  max-width: 640px;
  margin-left: auto;
  margin-right: auto;
}
.showcase-band + .container { margin-top: calc(-1 * (var(--space-6) + 20px)); }
.showcase-card {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
  background: var(--color-surface);
  border: 1px solid var(--color-accent);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card-hover);
  padding: var(--space-4);
  margin-bottom: var(--space-6);
}
@media (min-width: 760px) {
  .showcase-card { grid-template-columns: 320px 1fr; }
}
.showcase-card-photo { border-radius: var(--radius); overflow: hidden; min-height: 240px; }
.showcase-card-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.showcase-card-title {
  font-family: var(--font-heading);
  font-weight: 600;
  color: var(--color-accent-dark);
  margin-bottom: var(--space-2);
}
.showcase-checklist { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--space-2); }
.showcase-checklist li { display: flex; gap: var(--space-2); align-items: flex-start; }
.showcase-checklist svg {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  margin-top: 0.2em;
  stroke: var(--color-accent-dark);
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.showcase-checklist strong { color: var(--color-navy); }

/* ========================================================================
   Per-phase unit subpages (klemeru-2/3/4.html + ms/ + zh/, 2026-08-16 by
   request). Each phase moved off the township page onto its own page:
   dark-gradient photo hero with a spec rail, then Details / Location /
   Site Plan / Floor Plan / Showroom / FAQ, alternating white and cream.
   ======================================================================== */

/* Hero scrim variant: a bottom-weighted dark gradient rather than the
   navy diagonal used on .detail-hero-photo. Built from --color-navy-deep
   fading into a near-black neutral so it reads as the requested "black
   gradient" without introducing a 4th palette colour. */
.detail-hero-dark::before {
  background:
    linear-gradient(180deg, rgba(0, 27, 69, 0.3) 0%, rgba(6, 12, 20, 0.55) 45%, rgba(6, 12, 20, 0.92) 100%);
}

/* Hero spec rail — the four numbers a buyer checks first, on the first
   screen. Adopted from the klemeruipoh.com audit (its .rail). */
.spec-rail {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1px;
  margin-top: var(--space-4);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: var(--radius);
  overflow: hidden;
  background: rgba(255, 255, 255, 0.16);
}
.spec-rail div {
  background: rgba(6, 12, 20, 0.42);
  padding: var(--space-2) var(--space-3);
}
.spec-rail dt {
  font-size: 0.62rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.7);
  margin-bottom: 0.25rem;
}
.spec-rail dd {
  margin: 0;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 1.02rem;
  color: #fff;
}

/* Section 1 — full spec sheet, laid out horizontally so it stays scannable
   on a phone (auto-fit collapses it to fewer columns, never a long list). */
.spec-block + .spec-block { margin-top: var(--space-5); }
.spec-block-title { font-size: 1.15rem; margin-bottom: var(--space-1); }
.spec-block-size {
  font-family: var(--font-base);
  font-weight: 700;
  color: var(--color-accent-dark);
  margin-bottom: var(--space-3);
}
.spec-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(155px, 1fr));
  gap: var(--space-2);
}
.spec-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-3);
}
.spec-card dt {
  font-size: 0.62rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--color-text-muted);
  margin-bottom: 0.3rem;
}
.spec-card dd {
  margin: 0;
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: 1.05rem;
  color: var(--color-navy);
}

/* Alternating section bands on the subpages: white / cream. */
.band-white { background: var(--color-surface); }
.band-cream { background: var(--color-cream); }

/* Framed media block used by Location / Site Plan / Floor Plan. Holds a
   real <img> once photography is supplied; until then it carries
   .placeholder-photo, per the standing "labelled placeholder, never a
   misleading stock photo" rule. */
.media-frame {
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-card);
  margin: var(--space-4) 0 var(--space-3);
  min-height: 300px;
}
.media-frame img { width: 100%; height: 100%; object-fit: cover; display: block; }
.media-frame.placeholder-photo { display: flex; align-items: center; justify-content: center; }

/* Showroom carousel on the subpages — full-width Swiper, reusing the
   proven .unit-swiper-nav arrow buttons (which are unscoped) rather than
   Swiper's own .swiper-button-* classes, so multiple instances can never
   collide. */
.show-gallery { position: relative; margin-top: var(--space-4); }
.show-swiper { border-radius: var(--radius); overflow: hidden; }
.show-swiper .swiper-slide img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  display: block;
}
@media (min-width: 760px) {
  .show-swiper .swiper-slide img { aspect-ratio: 16 / 9; }
}

/* Gold eyebrow for light-background sections (the base .eyebrow is white,
   sized for the navy hero) + a short gold underline under section titles */
.eyebrow-gold {
  color: var(--color-accent-dark);
  text-transform: uppercase;
  /* 0.22em, not the old 0.08em — a wide-tracked uppercase micro-label
     against tight (negative-tracked) headings is what reads as "premium".
     Applied to both eyebrow variants site-wide, main pages and subpages. */
  letter-spacing: 0.22em;
  font-size: 0.8rem;
  font-weight: 700;
  margin-bottom: var(--space-1);
}
.section-title-underline { position: relative; padding-bottom: var(--space-2); }
.section-title-underline::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 56px;
  height: 3px;
  background: var(--color-accent);
}

/* Horizontal-scroll row of arch-topped placeholder photo cards (nearby
   amenities). The arch shape comes from a top radius wider than half the
   card width, which clamps to a semicircle. */
.amenity-row {
  display: flex;
  gap: var(--space-3);
  overflow-x: auto;
  padding: var(--space-2) 0 var(--space-3);
  scroll-snap-type: x proximity;
}
.amenity-item { flex: 0 0 150px; text-align: center; scroll-snap-align: start; }
.amenity-arch {
  width: 150px;
  height: 170px;
  border-radius: 999px 999px 8px 8px;
  margin: 0 auto var(--space-1);
  overflow: hidden;
}
.amenity-arch img { width: 100%; height: 100%; object-fit: cover; display: block; }
.amenity-item p { font-size: 0.85rem; font-weight: 600; margin: 0; }

/* Alternating image/text lifestyle rows */
.split-feature-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3);
  align-items: center;
  margin-bottom: var(--space-5);
}
.split-feature-row:last-child { margin-bottom: 0; }
@media (min-width: 760px) {
  .split-feature-row { grid-template-columns: 1fr 1fr; gap: var(--space-4); }
  .split-feature-row-reverse .split-feature-media { order: 2; }
}
.split-feature-media { height: 260px; border-radius: var(--radius); }
.split-feature-text h3 { color: var(--color-navy); font-size: 1.3rem; }

/* Small icon + caption grid (facilities teaser) */
.icon-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-3);
}
@media (min-width: 640px) {
  .icon-grid { grid-template-columns: repeat(4, 1fr); }
}
.icon-grid-item { text-align: center; }
.icon-grid-icon {
  width: 52px;
  height: 52px;
  margin: 0 auto var(--space-1);
  border-radius: 50%;
  background: var(--color-cream);
  border: 1px solid var(--color-border);
  display: flex;
  align-items: center;
  justify-content: center;
}
.icon-grid-icon svg {
  width: 24px;
  height: 24px;
  fill: none;
  stroke: var(--color-navy);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.icon-grid-item h4 { font-size: 0.9rem; margin: 0 0 0.1rem; }
.icon-grid-item p { font-size: 0.78rem; color: var(--color-text-muted); margin: 0; }

/* Investment/benefit cards */
.benefit-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-3);
}
@media (min-width: 640px) {
  .benefit-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 992px) {
  .benefit-grid { grid-template-columns: repeat(4, 1fr); }
}
.benefit-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  padding: var(--space-3);
  box-shadow: var(--shadow-card);
}
.benefit-card h3 { color: var(--color-navy); font-size: 1rem; margin-bottom: 0.3rem; }
.benefit-card p { font-size: 0.85rem; color: var(--color-text-muted); margin: 0; }

/* ========================================================================
   Floating WhatsApp button — the whole site's conversion goal is a
   WhatsApp message, so the CTA follows the visitor instead of only
   existing at section boundaries. Present on every page; the pre-filled
   message lives in each page's data-wa-message (see wireStaticWhatsAppLinks).
   ======================================================================== */
.fab-whatsapp {
  position: fixed;
  right: 1.15rem;
  bottom: 1.15rem;
  z-index: 90;
  width: 58px;
  height: 58px;
  border-radius: 50%;
  background: var(--color-whatsapp);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 8px 26px rgba(0, 45, 108, 0.28);
  transition: transform 0.3s var(--ease), box-shadow 0.3s var(--ease);
}
.fab-whatsapp:hover,
.fab-whatsapp:focus-visible {
  transform: scale(1.08);
  box-shadow: 0 12px 32px rgba(0, 45, 108, 0.36);
}
.fab-whatsapp svg { width: 30px; height: 30px; fill: #fff; position: relative; z-index: 1; }
/* Expanding ring — draws the eye without moving the button itself. */
.fab-whatsapp::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--color-whatsapp);
  animation: fab-pulse 2.6s var(--ease) infinite;
}
@keyframes fab-pulse {
  0%   { transform: scale(0.86); opacity: 0.9; }
  70%  { transform: scale(1.18); opacity: 0; }
  100% { transform: scale(1.18); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .fab-whatsapp::after { animation: none; opacity: 0; }
  .fab-whatsapp { transition: none; }
}

/* ========================================================================
   Footer
   ======================================================================== */
.site-footer {
  background: var(--color-navy);
  color: #E7E2DA;
  padding: var(--space-4) 0;
  font-size: 0.9rem;
}
/* Clear the fixed WhatsApp button so it never covers the footer's last
   line on narrow screens, where the two overlap. */
.site-footer { padding-bottom: calc(var(--space-4) + 58px); }
@media (min-width: 768px) {
  .site-footer { padding-bottom: var(--space-4); }
}
.footer-inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: space-between;
  align-items: center;
}
.footer-name {
  font-family: var(--font-heading);
  font-weight: 600;
  color: #fff;
  margin-bottom: 0.2rem;
}
.footer-copy { width: 100%; opacity: 0.7; margin-top: var(--space-2); }

/* ========================================================================
   Unit photo showcase — floor-plan/spec panel + swipeable photo gallery
   per unit type, added 2026-08-09 by request for klemeru-township.html
   (owner shared a screenshot of this pattern from a reference site: a
   cream info card beside a full-bleed photo carousel with round,
   semi-transparent arrow buttons overlaid directly on the photo — a
   different treatment from .gallery-swiper's nav, which floats below the
   image). Replaces .unit-type-grid/.unit-type-card there; Santorini
   keeps the plain grid-card style for now. Each .unit-swiper gets its
   own scoped Swiper instance (see the inline script in
   klemeru-township.html) — .unit-swiper-prev/next are plain buttons, not
   Swiper's default nav classes, so multiple instances on one page never
   collide via a shared global selector.
   ======================================================================== */
/* Vertical unit-type cards, 3 across (2026-08-16, by request) — was a
   2-column row (info left, photo right) stacked vertically. Now photo on
   top, specs below, laid out horizontally in a grid. auto-fill (not
   auto-fit) is deliberate: a phase with only one unit type keeps a normal
   card width instead of stretching across the full row, so Klemeru 2 / 3 /
   4 all render at the same card size despite having 1 / 3 / 1 unit types. */
.unit-photo-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--space-4);
  align-items: start;
}
.unit-photo-row {
  display: flex;
  flex-direction: column;
  background: var(--color-cream);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  transition: box-shadow 0.25s var(--ease);
}
/* Shadow only, no lift: .unit-photo-row is in wireScrollReveal()'s selector
   list, so it also carries .reveal.is-visible { transform: none } — equal
   specificity, declared later in the file (where it must stay), so any
   transform here would be silently dead. */
.unit-photo-row:hover,
.unit-photo-row:focus-within { box-shadow: var(--shadow-card-hover); }
.unit-photo-info { padding: var(--space-3); display: flex; flex-direction: column; }
.unit-photo-info .unit-type-name { font-size: 1.15rem; margin-bottom: 0.2rem; }
/* order: -1 puts the photo above the specs without reordering the markup
   in all 3 language files (specs stay first in the DOM, which also keeps
   the unit name as the first thing a screen reader reaches). */
.unit-photo-gallery { position: relative; min-width: 0; overflow: hidden; order: -1; }
.unit-photo-gallery .swiper,
.unit-photo-gallery .swiper-wrapper,
.unit-photo-gallery .swiper-slide { height: 100%; }
.unit-swiper-photo { height: 230px; }
.unit-swiper-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Phase picker cards (2026-08-16, by request) — one card per phase
   (Klemeru 2 / 3 / 4) laid out horizontally at the top of the Unit Types
   section; each is a whole-card anchor jumping to that phase's own detail
   section further down the page. Distinct from .unit-photo-row below,
   which presents a single unit type's specs and photo gallery. */
.phase-card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: var(--space-4);
  margin-top: var(--space-4);
}
.phase-card {
  display: flex;
  flex-direction: column;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  transition: box-shadow 0.25s var(--ease), border-color 0.25s var(--ease);
}
.phase-card:hover,
.phase-card:focus-visible {
  box-shadow: var(--shadow-card-hover);
  border-color: var(--color-accent);
}
.phase-card-photo { height: 200px; overflow: hidden; }
.phase-card-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.phase-card-body { padding: var(--space-3); display: flex; flex-direction: column; flex: 1; }
.phase-card-eyebrow {
  color: var(--color-accent-dark);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.72rem;
  font-weight: 600;
  margin-bottom: 0.2rem;
}
.phase-card-title { font-size: 1.2rem; margin-bottom: 0.2rem; }
.phase-card-sub { font-size: 0.9rem; color: var(--color-text-muted); margin-bottom: var(--space-2); }
.phase-card-facts {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-3);
  display: grid;
  gap: 0.3rem;
  font-size: 0.88rem;
}
.phase-card-link {
  margin-top: auto;
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--color-navy);
}
.phase-card:hover .phase-card-link { color: var(--color-accent-dark); }
.phase-card-link::after { content: " \2192"; }

/* Collapsed unit specs (2026-08-16, by request) — the unit name and size
   stay visible as the card's identity; bed/bath/land and the highlights
   line fold into a native <details> so the cards scan short and only
   expand when a visitor wants to read them. */
.unit-details { margin-top: var(--space-2); border-top: 1px solid var(--color-border); }
.unit-details > summary {
  cursor: pointer;
  list-style: none;
  padding: var(--space-2) 0 0;
  font-weight: 600;
  font-size: 0.88rem;
  color: var(--color-accent-dark);
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.unit-details > summary::-webkit-details-marker { display: none; }
.unit-details > summary::after {
  content: "+";
  margin-left: auto;
  font-size: 1.1rem;
  line-height: 1;
  color: var(--color-navy);
}
.unit-details[open] > summary::after { content: "\2212"; }
.unit-details > summary:hover { color: var(--color-navy); }
.unit-details .unit-type-specs { margin-top: var(--space-2); }

/* Hover reveal on the unit photo: a dark scrim fades in first, then the
   CTA slides up just behind it (transition-delay on the button only). The
   scrim is pointer-events:none so it never swallows a click meant for the
   swiper's prev/next buttons; only the CTA itself takes pointer events.
   :focus-within mirrors :hover so keyboard and touch users get it too. */
.unit-photo-overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: var(--space-3);
  background: linear-gradient(0deg, rgba(0, 27, 69, 0.82) 0%, rgba(0, 27, 69, 0.35) 55%, rgba(0, 27, 69, 0) 100%);
  opacity: 0;
  transition: opacity 0.25s var(--ease);
}
.unit-photo-overlay .btn {
  pointer-events: auto;
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.3s var(--ease) 0.12s, transform 0.3s var(--ease) 0.12s,
    background-color 0.15s ease;
}
.unit-photo-row:hover .unit-photo-overlay,
.unit-photo-row:focus-within .unit-photo-overlay { opacity: 1; }
.unit-photo-row:hover .unit-photo-overlay .btn,
.unit-photo-row:focus-within .unit-photo-overlay .btn {
  opacity: 1;
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .unit-photo-row, .unit-photo-overlay, .unit-photo-overlay .btn { transition: none; }
  .unit-photo-overlay .btn { transform: none; }
}
.unit-swiper-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: rgba(11, 18, 32, 0.55);
  color: #fff;
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
  transition: background 0.15s ease;
}
.unit-swiper-nav:hover { background: rgba(11, 18, 32, 0.8); }
.unit-swiper-prev { left: var(--space-2); }
.unit-swiper-next { right: var(--space-2); }

/* ========================================================================
   Photo lightbox — click-to-enlarge gallery viewer, added 2026-08-09 for
   klemeru-township.html to match the reference site's real PhotoSwipe
   lightbox behavior (see wireGalleryLightbox() in js/main.js). The
   overlay markup + JS only activate on pages with a
   [data-lightbox-gallery] element, so this is inert everywhere else.
   ======================================================================== */
.lightbox-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  background: rgba(11, 18, 32, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.2s ease;
}
.lightbox-overlay.is-open { opacity: 1; visibility: visible; }
.lightbox-content { position: relative; width: 100%; max-width: 720px; }
.lightbox-photo { height: min(70vh, 480px); border-radius: var(--radius); overflow: hidden; }
.lightbox-photo img { width: 100%; height: 100%; object-fit: contain; display: block; }
.lightbox-caption {
  color: rgba(255, 255, 255, 0.8);
  text-align: center;
  font-size: 0.85rem;
  margin-top: var(--space-2);
}
.lightbox-close,
.lightbox-nav {
  position: absolute;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: rgba(255, 255, 255, 0.15);
  color: #fff;
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s ease;
}
.lightbox-close:hover,
.lightbox-nav:hover { background: rgba(255, 255, 255, 0.3); }
.lightbox-close { top: -52px; right: 0; width: 36px; height: 36px; }
.lightbox-nav { top: 50%; transform: translateY(-50%); }
.lightbox-prev { left: -52px; }
.lightbox-next { right: -52px; }
@media (max-width: 760px) {
  .lightbox-prev { left: var(--space-1); }
  .lightbox-next { right: var(--space-1); }
}
@media (prefers-reduced-motion: reduce) {
  .lightbox-overlay { transition: none; }
}
button.gallery-slide-photo {
  border: none;
  width: 100%;
  padding: 0;
  cursor: pointer;
  font-family: inherit;
}

/* ========================================================================
   Facility photo grid — added 2026-08-09 by request for
   santorini-botani.html, replacing the small circular-icon .icon-grid
   for the "Facilities Designed Around Modern Living" section with real
   photo cards (owner shared a screenshot of the reference site's actual
   version of this section: a photo bleeding to the card's top edge,
   navy serif title, muted description below — not small icons). Card
   titles use var(--font-display) specifically because this section's
   real headings on santorinibotaniipoh.com.my are verified Cormorant
   Garamond, unlike some other h2/h3 elements on that site which use
   Outfit — don't assume one heading font covers every section there.
   ======================================================================== */
.facility-photo-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-3);
}
@media (min-width: 640px) {
  .facility-photo-grid { grid-template-columns: repeat(4, 1fr); }
}
.facility-photo-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.facility-photo-card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-card-hover);
}
.facility-photo-card .placeholder-photo { height: 200px; }
/* Stable wrapper class (2026-08-14) — same pattern as .unit-type-photo
   above: carries the size regardless of placeholder/real-photo state, so
   swapping in a real photo is just "drop .placeholder-photo, add this
   class + <img>", no CSS changes needed at that point. Bumped from
   160px to 200px by request — still landscape, not a square. */
.facility-photo { height: 200px; overflow: hidden; }
.facility-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.facility-photo-card-body { padding: var(--space-3); }
.facility-photo-card-body h4 {
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--color-navy);
  font-size: 1.15rem;
  margin-bottom: 0.3rem;
}
.facility-photo-card-body p { font-size: 0.88rem; color: var(--color-text-muted); margin: 0; }

/* ========================================================================
   Page-scoped typography — by request 2026-08-09, listing detail pages
   for a specific external project intentionally use that project's own
   real font pairing (verified from the developer's own site's computed
   styles) instead of the site-wide Poppins/Inter system, so each
   spotlight page reads as visually distinct rather than uniform. Scoped
   via a body.<project>-page modifier class overriding --font-display /
   --font-heading / --font-base — every component already reads these
   custom properties (see font-family declarations throughout this file),
   so nothing else needs to change. A page without the modifier class is
   unaffected: the vars fall back to the :root Poppins/Inter values. Each
   page's own <head> swaps its Google Fonts <link> to match (no need to
   also load Poppins/Inter there — they're never referenced once the
   vars are overridden). Follow this same pattern — verify the reference
   site's real computed font-family/weight first, never guess — for any
   future project page that should have its own font identity.
   ======================================================================== */
body.santorini-page {
  /* verified from santorinibotaniipoh.com.my: h1 Cormorant Garamond 700,
     h2/h3/body Outfit 500/400 */
  --font-display: 'Cormorant Garamond', Georgia, serif;
  --font-heading: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-base: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
body.klemeru-page {
  /* verified from eadepro.com: h1/h2 Playfair Display 600, body Quicksand 400.
     --font-display swapped to Cormorant Garamond 2026-08-13 by request, after
     the owner referenced eadepro.com's own H1 treatment — mirrors exactly how
     body.santorini-page already gives its H1 a distinct display face while
     h2-h4 keep the page's regular heading font. --font-heading (h2-h4) and
     --font-base (body) are unchanged. */
  --font-display: 'Cormorant Garamond', Georgia, serif;
  --font-heading: 'Playfair Display', Georgia, serif;
  --font-base: 'Quicksand', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

/* Detail-page in-page section nav (2026-08-13, by request) — a slim,
   elegant jump-nav sitting right after the floating .hero-highlight-card,
   using the page's own real section names as links (not a literal copy of
   any reference site's top-nav labels). Uses --font-display so it reads
   with the same serif elegance as the H1 above it. Page-scoped opt-in via
   .detail-section-nav on the wrapping element — add to any future detail
   page that wants the same treatment. */
.detail-section-nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) 0;
  margin: var(--space-4) 0 0;
  border-top: 1px solid var(--color-border);
  border-bottom: 1px solid var(--color-border);
}
.detail-section-nav a {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-navy);
  transition: color 0.25s var(--ease);
}
.detail-section-nav a:hover { color: var(--color-accent-dark); }

/* Hero-top variant (2026-08-16, by request) — the nav moved from below the
   floating .hero-highlight-card to the very top of the hero, above the
   breadcrumb. It now sits on the dark hero scrim instead of the page
   background, so it needs light-on-dark colours and a translucent rule.
   Scoped to .detail-hero so the navy-on-light styling above still applies
   to any future page that keeps the nav in normal page flow. */
.detail-hero .detail-section-nav {
  margin: 0 0 var(--space-3);
  padding: 0 0 var(--space-3);
  border-top: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0.28);
}
.detail-hero .detail-section-nav a { color: rgba(255, 255, 255, 0.92); }
.detail-hero .detail-section-nav a:hover { color: var(--color-accent); }
/* Tighten the hero's own top padding so the nav reads as sitting at the
   hero's top edge. :has() degrades harmlessly — without support the nav
   simply keeps the hero's normal top padding above it. */
.detail-hero:has(.detail-section-nav) { padding-top: var(--space-3); }

/* ========================================================================
   Scroll reveal — .reveal and .line-reveal are only ever added by JS (see
   wireScrollReveal in js/main.js), never present in the markup itself, so
   content is never gated on JS running or the animation completing.

   These MUST stay last in the file. They are utility classes applied on
   top of components, and most cards (.listing-card, .tool-card,
   .unit-photo-row, .facility-photo-card …) declare their own `transition`
   for hover. Since those rules have equal specificity, whichever comes
   later wins — with this block earlier in the file the cards' hover
   transitions silently replaced the reveal transition and the cards
   snapped into view instead of fading. Keep utilities after components.
   ======================================================================== */
.reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.85s var(--ease), transform 0.85s var(--ease);
}
.reveal.is-visible {
  opacity: 1;
  transform: none;
}

/* Section-heading rules draw themselves in from the left rather than
   just appearing with the heading. Scoped to .line-reveal (JS-added) so
   the underline is fully drawn for no-JS / reduced-motion visitors. */
.line-reveal::after {
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.9s var(--ease);
}
.line-reveal.is-visible::after {
  transform: scaleX(1);
}


/* ========================================================================
   Areas We Serve — interactive Voronoi map (#areas)
   区域数据来自 HTML 的 #areaList，JS 只负责画。令牌全部沿用上面已有的。
   ======================================================================== */
.areamap {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  gap: clamp(1.5rem, 3vw, 3rem);
  align-items: center;
}
.areamap-board {
  position: relative;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 14px;
  padding: clamp(0.75rem, 1.6vw, 1.25rem);
  overflow: hidden;
}
.areamap-board svg { display: block; width: 100%; height: auto; }

.imap-r { cursor: pointer; opacity: 0; animation: imapIn 0.5s var(--ease) forwards; }
/* 抬起的是整块 .tile（面 + 描边 + 字），不是只有面。
   只抬 .top 时字和描边留在原地，读起来像错位，不像一块地抬起来。 */
.imap-r .tile { transition: transform 0.3s var(--ease); }
.imap-r .top { transition: filter 0.3s var(--ease); }
/* 描边宽度固定，hover 只换颜色 —— 动 stroke-width 会重算描边几何 */
.imap-r .rim { fill: none; stroke: rgba(255,255,255,0.55); stroke-width: 1.8;
               transition: stroke 0.2s var(--ease); }
.imap-r .lbl { font-family: var(--font-heading); font-weight: 600;
               fill: var(--color-navy-deep); text-anchor: middle; pointer-events: none;
               paint-order: stroke; stroke: rgba(255,255,255,0.7); stroke-width: 2.5px; }
/* .is-lift 由 pointer 事件挂上，鼠标和触摸走同一条路；:hover / :focus-visible
   留着做无 JS 时的兜底 */
.imap-r:hover .tile, .imap-r:focus-visible .tile, .imap-r.is-lift .tile {
  transform: translateY(-9px); }
.imap-r:hover .top, .imap-r:focus-visible .top, .imap-r.is-lift .top {
  filter: drop-shadow(0 6px 10px rgba(0, 27, 69, 0.28)); }
.imap-r:hover .rim, .imap-r:focus-visible .rim, .imap-r.is-lift .rim {
  stroke: var(--color-accent-dark); }
@keyframes imapIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

.imap-tip {
  position: absolute; z-index: 5; pointer-events: none;
  opacity: 0; transform: translateY(4px);
  transition: opacity 0.18s var(--ease), transform 0.18s var(--ease);
  background: var(--color-navy-deep); color: #fff;
  border-radius: 8px; padding: 0.55rem 0.8rem; max-width: 15rem;
  box-shadow: var(--shadow-card-hover);
}
.imap-tip.on { opacity: 1; transform: none; }
.imap-tip b { display: block; font-family: var(--font-heading); font-size: 0.95rem; font-weight: 600; }
.imap-tip span { display: block; font-size: 0.78rem; line-height: 1.45;
                 color: rgba(255,255,255,0.8); margin-top: 0.15rem; }

.areamap-side { display: grid; gap: var(--space-3); align-content: center; }
.areamap-cycler { font-family: var(--font-heading); font-weight: 700;
                  color: var(--color-navy); font-size: clamp(1.8rem, 1.2rem + 2.4vw, 3rem);
                  line-height: 1.1; }
/* 只淡地名，且不淡到全透明 —— 整块闪成空白会像坏掉 */
.areamap-cycler span { display: block; transition: opacity 0.22s var(--ease); }
/* 小标签用 muted 不用 accent-dark：#A6832E 在 cream 上只有 3.55，
   11.5px 的文字过不了 AA。跟 .trust-label 保持一致（accent-dark 只用在大号数字上） */
.areamap-cycler small { display: block; font-family: var(--font-base); font-weight: 500;
                        font-size: 0.72rem; letter-spacing: 0.18em; text-transform: uppercase;
                        color: var(--color-text-muted); margin-bottom: 0.5rem; }
/* 排除 cycler —— .areamap-side > p 的权重(0-1-1)会盖过 .areamap-cycler(0-1-0)，
   不排除的话那行最大的地名标题会被染成灰色 */
.areamap-side > p:not(.areamap-cycler) { color: var(--color-text-muted); }

.area-list { display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0; margin: 0; list-style: none; }
.area-list a { display: inline-block; padding: 0.4rem 0.8rem; border-radius: 999px;
               border: 1px solid var(--color-border); background: var(--color-surface);
               font-size: 0.85rem; color: var(--color-text); text-decoration: none;
               transition: border-color 0.2s var(--ease), color 0.2s var(--ease), transform 0.2s var(--ease); }
.area-list a:hover { border-color: var(--color-accent); color: var(--color-navy); transform: translateY(-1px); }
.area-list a[data-loc]:not([data-loc=""])::after {
  content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  background: var(--color-accent-dark); margin-left: 0.45rem; vertical-align: middle;
}
.area-note { font-size: 0.8rem; color: var(--color-text-muted); }
.area-note i { display: inline-block; width: 6px; height: 6px; border-radius: 50%;
               background: var(--color-accent-dark); margin-right: 0.35rem; }

@media (max-width: 860px) {
  .areamap { grid-template-columns: 1fr; }
  .areamap-side { order: -1; }
}
@media (prefers-reduced-motion: reduce) {
  .imap-r { opacity: 1; animation: none; }
  /* 不动位置，但描边和阴影照常换 —— 反馈还在，只是不用位移表达 */
  .imap-r .tile { transition: none; }
  .imap-r:hover .tile, .imap-r:focus-visible .tile, .imap-r.is-lift .tile {
    transform: none; }
}


/* ========================================================================
   Recent Handovers (#handovers) — 深色签约照片墙
   全站唯一的深色段：打断房源卡墙 + 补上深浅节奏 + 真实摄影
   ======================================================================== */
.section-dark {
  background: var(--color-navy-deep);
  color: rgba(255, 255, 255, 0.86);
}
.section-dark .section-title { color: #fff; }
.section-dark .section-sub { color: rgba(255, 255, 255, 0.74); }

.handover-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-block: 1px solid rgba(255, 255, 255, 0.16);
  margin-bottom: var(--space-4);
  text-align: center;
}
.section-dark .trust-number { color: var(--color-accent); }
.section-dark .trust-label { color: rgba(255, 255, 255, 0.68); }

/* 正方裁切：签约照来源杂乱（竖拍、手机画质、日光灯），统一裁方是让一堆
   素材看起来整齐的唯一办法。图片已在构建时裁好并转 WebP。 */
/* Two rows drifting in opposite directions. See patch_marquee.py for why the
   spacing is margin-right on the items rather than gap on the track. */
.handover-marquee { display: grid; gap: 0.6rem; }
.handover-row {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 5%, #000 95%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 5%, #000 95%, transparent);
}
.handover-track {
  display: flex;
  /* Without this the flex line stretches each item to the row height, which
     overrides aspect-ratio:1 and the squares come out as tall rectangles. */
  align-items: flex-start;
  width: max-content;
  animation: handover-drift 54s linear infinite;
}
.handover-row--reverse .handover-track { animation-direction: reverse; }
.handover-marquee:hover .handover-track,
.handover-marquee:focus-within .handover-track { animation-play-state: paused; }
@keyframes handover-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
.handover-marquee .gallery-slide-photo {
  flex: 0 0 clamp(148px, 17vw, 212px);
  margin-right: 0.6rem;
  /* .gallery-slide-photo carries height:320px for the Klemeru slider, and an
     explicit height beats aspect-ratio — the old grid cells were 148x320, not
     square. Release the height so the 1:1 ratio actually applies here. */
  height: auto;
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1;
  padding: 0;
  border: 0;
  border-radius: 8px;
  overflow: hidden;
  cursor: zoom-in;
  background: rgba(255, 255, 255, 0.06);
  transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease);
}
.handover-marquee .gallery-slide-photo img {
  width: 100%; height: 100%; object-fit: cover; display: block;
}
.handover-marquee .gallery-slide-photo:hover,
.handover-marquee .gallery-slide-photo:focus-visible {
  transform: translateY(-3px);
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.38);
}

.handover-policy {
  margin-top: var(--space-3);
  padding: 0.75rem 0.95rem;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 10px;
  font-size: 0.85rem;
  color: rgba(255, 255, 255, 0.74);
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
}
.handover-policy svg {
  flex: none; width: 18px; height: 18px; margin-top: 0.15rem;
  stroke: var(--color-accent); fill: none; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
.handover-policy b { color: #fff; font-weight: 600; }

@media (max-width: 600px) {
  .handover-marquee { gap: 0.45rem; }
  .handover-marquee .gallery-slide-photo { flex-basis: 40vw; margin-right: 0.45rem; }
  .handover-stats { grid-template-columns: 1fr; gap: 0.9rem; }
}


/* DSR: income earned outside Malaysia.
   The checkbox sits in the same .tool-form column flow as the number fields,
   so it needs its own row rule rather than the label/input pair styling.
   Hiding the statutory block is done with the hidden attribute in JS; this
   only handles the print stylesheet, which force-shows a lot of the card. */
.field-check {
  display: flex;
  align-items: flex-start;
  gap: 0.55rem;
  margin-top: 0.35rem;
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--color-text);
  cursor: pointer;
}
.field-check input[type="checkbox"] {
  flex: none;
  width: 1.05rem;
  height: 1.05rem;
  margin-top: 0.12rem;
  accent-color: var(--color-accent-dark);
  cursor: pointer;
}
#loan-tool-card[data-overseas="yes"] #dsr-deductions-block,
#loan-tool-card[data-overseas="yes"] #dsr-result-net { display: none !important; }
#dsr-overseas-note[hidden] { display: none; }


/* No drift under reduced motion — the clones come out too, leaving a plain
   horizontal scroller of the eight real photographs per row. */
@media (prefers-reduced-motion: reduce) {
  .handover-track { animation: none; width: auto; }
  .handover-row { overflow-x: auto; -webkit-mask-image: none; mask-image: none; }
  .handover-marquee [data-clone] { display: none; }
}


/* ============================================================
   WHO IT SUITS
   Buyer-persona block. Same component as the project
   stylesheets carry, mapped onto this sheet's own tokens.
   ============================================================ */
.suits-section { padding-block: var(--space-7, 4rem); }
.suits-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 1.4rem;
  margin-top: 2.2rem;
}
.suits-card {
  position: relative;
  padding: 1.9rem 1.6rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  box-shadow: var(--shadow-card);
  transition: transform .3s var(--ease), border-color .3s var(--ease),
              box-shadow .3s var(--ease);
}
.suits-card:hover {
  transform: translateY(-3px);
  border-color: var(--color-accent);
  box-shadow: var(--shadow-card-hover);
}
.suits-card b {
  display: block;
  font-family: var(--font-heading);
  font-size: 1.2rem;
  font-weight: 600;
  line-height: 1.25;
  color: var(--color-navy);
  margin-bottom: 0.7rem;
  padding-right: 2.4rem;
}
.suits-card p {
  font-size: 0.95rem;
  line-height: 1.7;
  color: var(--color-text-muted);
  margin: 0;
}
.suits-card i {
  position: absolute;
  top: 1.5rem;
  right: 1.2rem;
  font-style: normal;
  font-family: var(--font-heading);
  font-size: 1.9rem;
  font-weight: 700;
  line-height: 1;
  color: var(--color-accent-dark);
  opacity: .45;
}


/* Four-card persona grids were leaving an orphan on a second row at tablet
   widths. Pick the counts rather than letting auto-fit pick them. */
.suits-grid { grid-template-columns: repeat(3, 1fr); }
.suits-grid:has(> :nth-child(4)) { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 1080px) {
  .suits-grid:has(> :nth-child(4)) { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 720px) {
  .suits-grid,
  .suits-grid:has(> :nth-child(4)) { grid-template-columns: 1fr; }
}

/* Rolling price counters. .price-from span and td.lot-price span already style
   the small sub-line that sits beside a price, and a bare counter <span> was
   inheriting that - a 26px "RM" with an 11.5px figure dropped underneath it.
   The counter carries its own class and resets those properties so no
   container rule can capture it. */
.cnum{display:inline!important;font:inherit!important;font-size:inherit!important;
  font-weight:inherit!important;font-family:inherit!important;color:inherit!important;
  letter-spacing:inherit!important;text-transform:none!important;
  margin:0!important;padding:0!important;opacity:1!important}

/* ---------------------------------------------------------------------------
   WhatsApp button shine — a light sweeping left to right.
   ::after is used for the sweep; pointer-events:none keeps it from swallowing
   the tap. Disabled under prefers-reduced-motion.
   --------------------------------------------------------------------------- */
.btn-whatsapp{position:relative;overflow:hidden}
.btn-whatsapp::after{content:"";position:absolute;top:0;bottom:0;left:-140%;width:55%;
  background:linear-gradient(100deg,rgba(255,255,255,0) 0%,rgba(255,255,255,.45) 50%,rgba(255,255,255,0) 100%);
  transform:skewX(-18deg);pointer-events:none;
  animation:waShine 3.8s cubic-bezier(.22,1,.36,1) infinite}
@keyframes waShine{0%{left:-140%}55%,100%{left:140%}}
@media (prefers-reduced-motion:reduce){.btn-whatsapp::after{animation:none;opacity:0}}
