/* Mirna Luka theme bundle — concatenated from src/ token layer + globals + component CSS. Generated for the WordPress port. */

/* ===== styles\00-primitives.css ===== */
/* ===========================================================================
   LAYER 1 — PRIMITIVES

   Raw values only. Never references Layer 2 (01-theme.css) or any vendor token.
   Component CSS must NOT consume these directly — it consumes Layer 2 semantics
   so that everything inverts inside a .theme-always-dark / .theme-invert island.

   Declared on a bare `:root` because none of these flip per theme and none of
   these names exist in the vendor stylesheet, so there is nothing to out-specify.
   (Layer 2 and Layer 3 DO need extra specificity — see their headers.)

   Every value below was derived by collapsing the real inventory: 48 hex + 48
   rgba in src/, 40 distinct font-sizes, 21 line-heights, 16 letter-spacings.
   The "absorbs" comments record what each token replaced, so a future reader can
   tell a deliberate merge from an accident.
   =========================================================================== */

:root {
  /* ---- Palette: teal (brand) ---------------------------------------------
     4 near-identical teals in src/ → 2 steps + 1 deep. */
  --ml-teal-900: #0b1a17;   /* VideoScroll canvas + section black-teal */
  --ml-teal-700: #17332e;   /* THE brand ink/accent. 25 uses. Value unchanged. */
  --ml-teal-600: #223f39;   /* hover/lifted. absorbs #22403a #223c36 #34493f */

  /* ---- Palette: sage greys ----------------------------------------------
     9 muted greys doing 2 jobs → 3 text steps.
     6 off-whites differing by 1–2 per channel → 3 surface/line steps. */
  /* THE SAGE RAMP IS FOR SURFACES ONLY — no step of it may back text.
     A tint on a background is atmosphere; the same tint on a glyph is a defect.
     The text ramp lives in the ink block below. Three sage steps used to back
     text (600 body, 500 label, 400 muted) and every one of them carried the same
     ~14/255 green shift, so paragraphs and captions read murky. Corrected after
     review; do not route text back through here. */
  --ml-sage-100: #e7eae8;   /* hairline.    absorbs #e6eae8 #e7ece9 #e8ece9 */
  --ml-sage-050: #f2f5f3;   /* tinted surface */

  /* ---- Palette: inks ----------------------------------------------------
     11 near-blacks → 5, plus the neutral body grey.

     BODY TEXT IS NEUTRAL, NOT SAGE — this was corrected after review and must
     not drift back. The first pass merged the project's three body colours
     (#5f726c, #5a635f, #6c7d77) into their average #5d6b65 and bridged the
     vendor's --secondary into it, which tinted 624 paragraphs green: all three
     originals carry a ~14/255 green shift, so averaging them preserved the cast
     instead of cancelling it. On long-form text that reads as murky and is
     noticeably harder to scan.
     #555555 is exactly the vendor's own --secondary, so vendor paragraphs are
     back to the colour they had before the migration, and it measures 7.46:1
     (AAA) against 5.59:1 for the sage.
     Sage belongs on SURFACES and LINES, where a tint is atmosphere. On text it
     is a defect. */
  --ml-ink-600: #555555;   /* body copy.   7.46:1 on white */

  /* Label text: spec keys, figcaptions, room labels. Small uppercase counts as
     SMALL text under WCAG, so it needs 4.5:1 — and it was deliberately taken
     past that to 5.74:1 rather than parked on the line, because at 11–12px the
     minimum is uncomfortable to read even when it technically passes.
     Replaces #8a938f (3.16:1), #96a49f (2.59:1), #8fa199 (2.72:1) and
     #8aa39a (2.70:1) — all of which failed AA outright. */
  --ml-ink-500: #666666;   /* labels.      5.74:1 */

  /* Decorative muted tier. Intentionally NOT AA: only for text a reader can miss
     without losing anything — counters, placeholders, map attribution. Anything
     that must be read uses --ml-ink-500 or darker.
     Also happens to be the vendor's own grey (71 uses in style.css), so this
     tier now agrees with the template instead of competing with it. */
  --ml-ink-400: #999999;   /* muted.       2.85:1 */
  --ml-ink-300: #b9b9b9;   /* ornamental: rules, dividers, watermarks. 1.96:1 */
  --ml-ink-950: #0a0d0c;    /* deepest. MapSection ×7, ExploreMap ×2 */
  --ml-ink-900: #111111;    /* == vendor --primary / --black / --bg. Keep this
                               value EXACT: bridged dark bands must match the
                               hundreds of vendor bands we never touch. */
  --ml-ink-850: #1a1a1c;    /* absorbs #1a1a1a #141917 */
  --ml-ink-800: #232326;    /* absorbs #222 */
  --ml-ink-700: #2c2c2f;    /* absorbs #2a2a2e #2d2d2d #333337 */

  /* ---- Palette: gold (warm accent — map marker, focus ring) -------------- */
  --ml-gold-950: #1a140a;
  --ml-gold-700: #9c7723;
  --ml-gold-500: #c8a96a;
  --ml-gold-100: #f4f1ea;
  --ml-gold-050: #fbf4e6;

  --ml-white: #ffffff;
  --ml-black: #000000;

  /* ---- Channel triplets that NEVER flip ---------------------------------
     For the cases where you genuinely mean white or black regardless of theme:
     photo scrims, shadow colour. The flipping pair (--fg-rgb / --bg-rgb) lives
     in 01-theme.css because it is theme state, not a primitive. */
  --white-rgb: 255 255 255;
  --black-rgb: 0 0 0;
  --teal-rgb: 23 51 46;
  --gold-rgb: 200 169 106;

  /* ---- FLUID TYPE SCALE -------------------------------------------------
     Slope range 320px → 1790px. 1790 is the real site width
     (.rr-container-1790, called "the site width" in globals.css), NOT the 1366
     of the pxpace reference — a token that stops growing at 1366 would leave
     the widest 24% of the layout with undersized type.

       slope_vw      = (max - min) / 14.7
       intercept_rem = (min - (max - min) * 320/1470) / 16

     The small steps deliberately span only ~1px. An 11px eyebrow label must not
     become 15px on a 1790px screen — labels are meant to stay quiet. This is
     also why pxpace's --text-xs floor of 0.97rem (~15.5px) is unusable here:
     62 of the 97 real font-size declarations in src/ are between 10 and 17px. */
  --text-3xs:  clamp(0.625rem,  calc(0.07vw + 0.611rem), 0.6875rem);  /* 10→11 */
  --text-2xs:  clamp(0.6875rem, calc(0.07vw + 0.674rem), 0.75rem);    /* 11→12 */
  --text-xs:   clamp(0.75rem,   calc(0.07vw + 0.736rem), 0.8125rem);  /* 12→13 */
  --text-s:    clamp(0.8125rem, calc(0.07vw + 0.799rem), 0.875rem);   /* 13→14 */
  --text-m:    clamp(0.875rem,  calc(0.07vw + 0.861rem), 0.9375rem);  /* 14→15 */
  --text-base: clamp(0.9375rem, calc(0.07vw + 0.924rem), 1rem);       /* 15→16 */
  /* BODY COPY. Decided: a fixed 16px in the old CSS maps HERE, not to
     --text-base. It keeps 16px on phones and grows to 18px at the site width,
     which is the normal measure for the ~900px reading column (a 90-character
     line lands near 72 characters at 18px). --text-base would have preserved
     desktop exactly at the cost of shrinking phones to 15px, and phones are
     where the extra size is worth more. Apply the same mapping in every file. */
  --text-l:    clamp(1rem,      calc(0.14vw + 0.973rem), 1.125rem);   /* 16→18 */
  --text-xl:   clamp(1.125rem,  calc(0.27vw + 1.071rem), 1.375rem);   /* 18→22 */
  --text-2xl:  clamp(1.3125rem, calc(0.41vw + 1.231rem), 1.6875rem);  /* 21→27 */
  --text-3xl:  clamp(1.5rem,    calc(0.68vw + 1.364rem), 2.125rem);   /* 24→34 */
  --text-4xl:  clamp(1.75rem,   calc(1.09vw + 1.532rem), 2.75rem);    /* 28→44 */
  --text-5xl:  clamp(2.125rem,  calc(2.31vw + 1.662rem), 4.25rem);    /* 34→68 */
  --text-6xl:  clamp(2.5rem,    calc(3.54vw + 1.793rem), 5.75rem);    /* 40→92 */
  --text-7xl:  clamp(3.5rem,    calc(4.9vw + 2.52rem),   8rem);       /* 56→128 */
  /* Art-directed one-off, NOT a scale step: the MapIntro numeral, previously
     clamp(64px, 16vw, 260px). Kept as a token so it is findable, not so it is
     reusable — if a second element wants this, it wants a different design. */
  --text-mega: clamp(4rem, calc(13.33vw + 1.333rem), 16.25rem);       /* 64→260 */

  /* ---- FLUID SPACE SCALE ------------------------------------------------
     Same 320→1790 range. src/ currently has no 4px or 8px grid at all:
     8,9,10,14,16,18,20,22,24,26,28,30,34,38,40,44,48 all appear as both gaps
     and paddings. These 11 steps replace that. */
  --space-4xs: 0.25rem;                                            /* 4, static */
  --space-3xs: clamp(0.375rem, calc(0.14vw + 0.348rem), 0.5rem);   /* 6→8   */
  --space-2xs: clamp(0.5rem,   calc(0.14vw + 0.473rem), 0.625rem); /* 8→10  */
  --space-xs:  clamp(0.625rem, calc(0.27vw + 0.571rem), 0.875rem); /* 10→14 */
  --space-s:   clamp(0.875rem, calc(0.27vw + 0.821rem), 1.125rem); /* 14→18 */
  --space-m:   clamp(1rem,     calc(0.54vw + 0.891rem), 1.5rem);   /* 16→24 */
  --space-l:   clamp(1.25rem,  calc(0.68vw + 1.114rem), 1.875rem); /* 20→30 */
  --space-xl:  clamp(1.5rem,   calc(1.09vw + 1.282rem), 2.5rem);   /* 24→40 */
  --space-2xl: clamp(1.875rem, calc(1.22vw + 1.63rem),  3rem);     /* 30→48 */
  --space-3xl: clamp(2.5rem,   calc(1.63vw + 2.173rem), 4rem);     /* 40→64 */
  --space-4xl: clamp(3.75rem,  calc(2.72vw + 3.206rem), 6.25rem);  /* 60→100 */

  /* ---- MEASURE (reading widths) -----------------------------------------
     15 distinct values in 3 unit systems (px, rem, ch) for what is
     conceptually the same thing → 6 steps, all rem. */
  --measure-xs:  30rem;   /* 480  — absorbs 30rem, 40ch */
  --measure-s:   33rem;   /* 528  — absorbs 520px, 560px */
  --measure-m:   40rem;   /* 640  — absorbs 620px, 640px */
  --measure-l:   47rem;   /* 752  — absorbs 44rem (704), 760px */
  --measure-xl:  60rem;   /* 960  — absorbs 900px, 62rem (992) */
  --measure-2xl: 69rem;   /* 1104 — absorbs 1100px (.blog-details__content) */

  --width-wide: min(87.5rem, 100%);  /* was min(1400px, 100%) */
  --width-site: 1790px;              /* .rr-container-1790 — "the site width" */

  /* ---- RADIUS ----------------------------------------------------------
     2px dominates (12 uses) so it is the brand radius, not an afterthought.
     NOTE: --radius-xl is tweened inline by GSAP on .ml-map__frame
     (MapSection.tsx). See src/styles/README.md "Tokens GSAP can override". */
  --radius-xs: 2px;
  --radius-s: 4px;
  --radius-m: 8px;
  --radius-l: 16px;
  --radius-xl: 20px;      /* MapSection frame */
  --radius-pill: 999px;
  --radius-circle: 50%;

  /* ---- Z-INDEX ---------------------------------------------------------
     Until now this ladder existed ONLY as prose in Header.css:7-9. */
  --z-under: -1;
  --z-base: 0;
  --z-raise: 2;            /* was a bare `2` in 6 files */
  --z-raise-2: 3;
  --z-map-controls: 1000;  /* Leaflet controls */
  --z-map-popup: 1200;     /* Leaflet popup */
  --z-lightbox: 9500;      /* UnitDetail lightbox — deliberately BELOW --z-panel */
  --z-devtool: 2147483647; /* SectionLabeler, development only */

  /* Vendor owns these three. Mirrored here as read-only documentation so the
     ladder is complete and greppable. DO NOT redefine them — the vendor
     stylesheet sets the actual z-index on those elements. */
  --z-header: 800;
  --z-offcanvas: 900;
  --z-panel: 9999;

  /* ---- BREAKPOINTS — FOR REFERENCE ONLY --------------------------------
     Custom properties are ILLEGAL inside @media conditions:
       @media (max-width: var(--bp-md))   <-- never works, silently ignored
     Every @media must therefore spell the literal. These exist so the numbers
     are findable and greppable from CSS, and scripts/check-tokens.mjs fails the
     build on any @media width outside this set.
     Source of truth for TS/TSX: src/config/breakpoints.ts */
  --bp-xs: 575px;
  --bp-sm: 767px;
  --bp-md: 991px;
  --bp-lg: 1199px;
  --bp-xl: 1399px;
  --bp-xxl: 1919px;
  --bp-md-up: 992px;   /* the only min-width tier in use */
}


/* ===== styles\01-theme.css ===== */
/* ===========================================================================
   LAYER 2 — SEMANTIC, INVERTIBLE

   ~26 names. THIS is the layer component CSS consumes. Never reach past it into
   Layer 1 (--ml-*) from a component: a Layer 1 value cannot invert, so using one
   directly is how a component silently breaks inside a dark island.

   ---------------------------------------------------------------------------
   WHY THE SELECTORS LOOK LIKE THIS

   Two paired groups, adapted from docs/pxpace-example-css.css. They give three
   utility classes for free:

     .theme-invert        flip relative to the ambient theme
     .theme-always-dark   dark regardless of ambient theme
     .theme-always-light  light regardless of ambient theme

   That is the whole point of this architecture for this site: Mirna Luka is a
   light editorial page with intentional dark BANDS (footer, map, booking rail,
   video section). Before this, each band hardcoded its own palette — five
   separate ad-hoc token blocks under two competing naming conventions, plus a
   `--primary` re-point hack and ~20 !important to undo it. Now a band is just
   an element carrying .theme-always-dark.

   The light group is anchored on `:root:not(.theme-dark)` rather than
   `:root.theme-light` for two reasons:
     1. Specificity (0,2,0) beats the vendor stylesheet's bare `:root` (0,1,0).
        This matters because Next hoists ALL of src/ CSS ABOVE
        /assets/css/style.css in the built <head> (verified in .next output), so
        we can never win on source order — only on specificity.
     2. Light keeps working if the html class is ever dropped. Light is the
        fail-safe default, not a state that must be switched on.

   NESTING LIMIT: .theme-invert resolves correctly one level deep, and
   .theme-always-* resolves correctly inside one .theme-invert. Deeper nesting is
   undefined (same limitation as the pxpace reference). Never put two theme-*
   classes on one element; scripts/check-tokens.mjs fails the build if you do.
   =========================================================================== */

/* ------------------------------- LIGHT ----------------------------------- */
:root:not(.theme-dark),
:root.theme-dark .theme-invert,
:root:not(.theme-dark) .theme-invert .theme-always-light,
/* Bare, so `always` means ALWAYS — including nested inside a dark island, which
   the pxpace selector set did not cover: it only ever anticipated nesting under
   :root.theme-*, so a light chip inside .theme-always-dark silently inherited the
   dark values. That is a real shape here — the Leaflet popup and map pins are
   light cards sitting on a dark map. Specificity is fine at (0,1,0): the dark
   island's values reach the chip by INHERITANCE, and any rule on the element
   itself beats an inherited value. */
.theme-always-light {
  /* Exposed as a TOKEN, not applied here. Applying `color-scheme` in this group
     would put it on :root, changing how the UA paints form controls and
     scrollbars site-wide — a real change, and not one the token layer should
     make on its own. 04-utilities.css applies it to theme islands only, which is
     where it is actually needed (it replaces the hand-set `color-scheme: dark`
     on .rd-rail__card). Root can opt in with one line when the toggle lands. */
  --color-scheme: light;

  /* The one pair that flips. Everything alpha-based derives from these, so a
     theme is defined by two numbers plus the semantic assignments below. */
  --fg-rgb: 23 51 46;
  --bg-rgb: 255 255 255;
  --shadow-rgb: 23 51 46;   /* shadows are teal-tinted on light, never neutral grey */

  /* Surfaces */
  --bg-body: var(--ml-white);
  --bg-surface: var(--ml-white);
  --bg-surface-2: var(--ml-sage-050);
  --bg-inset: var(--ml-sage-100);
  --bg-special: var(--ml-gold-050);

  /* Text */
  /* Brand teal is a DISPLAY colour: it belongs on the Vogue face and nowhere
     else. A 130px sans heading in teal reads as a mistake — which is exactly
     what /explore and /contact showed. --text-heading is the default for every
     heading NOT set in Vogue. */
  --text-title: var(--ml-teal-700);   /* Vogue headings only */
  --text-heading: var(--ml-ink-900);  /* sans headings — the vendor's own #111111 */
  /* Neutral, not sage — see the note on --ml-ink-600. Headings stay brand teal;
     body copy does not get a tint. */
  --text-body: var(--ml-ink-600);
  /* --text-label and --text-muted are DIFFERENT on purpose, and were briefly the
     same value by accident. --text-label backs small uppercase labels that a
     reader has to read (spec keys, figure captions, room labels) and so must
     clear WCAG AA 4.5:1 at 11–12px; --text-muted is the decorative tier and
     deliberately does not. Do not collapse them back together.
     All three are NEUTRAL — see the sage-ramp note in 00-primitives.css. */
  --text-label: var(--ml-ink-500);    /* 5.74:1 on white — past AA on purpose */
  --text-muted: var(--ml-ink-400);    /* 2.85:1 — decorative only */
  --text-faint: var(--ml-ink-300);    /* ornamental: rules, dividers, watermarks */
  --text-inverse: var(--ml-white);

  /* Lines. Named --line*, NOT --border*, on purpose: `--border` is the VENDOR's
     own token name (81 uses, 79 of them border-*). Declaring it here would make
     Layer 2 silently bridge it, which breaks the layering contract — Layer 3
     owns vendor names — and would move every hairline on the site the moment the
     token layer is imported. 03-bridge.css points --border at --line explicitly.
     10% matches the vendor's original rgba(17,17,17,0.10), so the bridge is a
     hue shift and not a darkening. */
  --line: var(--fg-10);
  --line-strong: var(--fg-15);   /* absorbs --rd-line rgba(23,51,46,0.14) */
  --line-faint: var(--fg-5);

  /* Accent */
  --accent: var(--ml-teal-700);
  --accent-hover: var(--ml-teal-600);
  --accent-contrast: var(--ml-white);
  --gold: var(--ml-gold-500);
  --gold-ink: var(--ml-gold-700);
  --gold-surface: var(--ml-gold-050);
  /* Text/iconography sitting ON a --gold fill. Does NOT flip: the gold fill is a
     mid-light colour in both themes, so its contrast partner is always the dark
     end. Reaching for --gold-surface here instead is a trap — that is the tinted
     BACKGROUND token, and on light it resolves to near-white on gold. */
  --gold-contrast: var(--ml-gold-950);

  --focus-ring: 0 0 0 6px rgb(var(--gold-rgb) / 0.2);
}

/* -------------------------------- DARK ----------------------------------- */
:root.theme-dark,
:root:not(.theme-dark) .theme-invert,
:root.theme-dark .theme-invert .theme-always-dark,
/* Bare, mirroring .theme-always-light above. */
.theme-always-dark {
  /* Applied by 04-utilities.css, not here — see the light group. This is what
     replaces the hand-placed `color-scheme: dark` on .rd-rail__card, which was
     there so Chrome paints the <input type="date"> indicator visibly on #111. */
  --color-scheme: dark;

  --fg-rgb: 255 255 255;
  --bg-rgb: 10 13 12;
  --shadow-rgb: 0 0 0;      /* never a white glow */

  --bg-body: var(--ml-ink-950);
  --bg-surface: var(--ml-ink-900);
  --bg-surface-2: var(--ml-ink-850);
  --bg-inset: var(--ml-ink-800);
  --bg-special: var(--ml-gold-950);

  --text-title: var(--ml-white);
  --text-heading: var(--ml-white);    /* both heading roles collapse on dark */
  --text-body: var(--fg-70);          /* 9.5:1 on #111 */
  --text-muted: var(--fg-60);         /* 7.2:1 */
  --text-label: var(--fg-50);         /* 5.3:1 — AA, no darkening needed here:
                                         white-on-dark clears 4.5:1 from 50% up,
                                         which is why only the light theme needed
                                         a dedicated --ml-sage-500 label step. */
  --text-faint: var(--fg-40);         /* 3.8:1 — ornamental only, same rule as light */
  --text-inverse: var(--ml-teal-700);

  --line: var(--fg-10);
  --line-strong: var(--fg-20);
  --line-faint: var(--fg-5);

  /* The brand teal is invisible on near-black, so on dark the accent is white.
     This is not a compromise: the real dark islands (booking-rail CTA, footer
     CTA) were already hand-built with white fills — this makes that the rule. */
  --accent: var(--ml-white);
  --accent-hover: var(--ml-sage-100);
  --accent-contrast: var(--ml-teal-700);
  --gold: var(--ml-gold-500);
  --gold-ink: var(--ml-gold-500);   /* the 700 is unreadable on dark */
  --gold-surface: var(--ml-gold-950);
  --gold-contrast: var(--ml-gold-950);   /* same in both themes — see light group */

  --focus-ring: 0 0 0 6px rgb(var(--gold-rgb) / 0.24);
}

/* ===========================================================================
   ALPHA LADDERS

   Replaces 13 distinct white-alphas and 11 distinct black-alphas in src/ with
   one 12-step ladder that inverts.

   MECHANISM — read this before changing the selector list. A custom property's
   computed value is its specified value with var() ALREADY substituted, and it
   is that substituted value which inherits. So if --fg-15 were declared only on
   :root, it would freeze as rgb(23 51 46 / 0.15) at the root and inherit that
   literal into .theme-always-dark — the ladder would NOT invert, silently.

   Listing the island classes in the selector below makes each island RE-declare
   the ladder on itself, where --fg-rgb has already been flipped by the group
   above. That is what makes the ladder invert.

   Steps snapped from the 24 real alpha values found in src/:
     .05→5 · .1/.12→10 · .14/.16→15 · .22/.25→20 · .28/.3→30 · .35→35
     .45→40 · .5→50 · .55/.6→60 · .65/.66/.7/.72→70 · .85→80 · .92/.94→90
   35 and 70 exist as their own steps specifically to keep rgba(0,0,0,.35) (×3)
   and the .65/.66/.7/.72 cluster from drifting on photo scrims.

   Chosen over pxpace's 8-digit hex ladder (12 hand-computed strings per colour
   per theme; change one value and you recompute all 12) and over color-mix()
   (reads worse at ~200 call sites and hides the alpha number). One flipping
   triplet, one mechanical ladder, and rgb(var(--fg-rgb) / .37) still available
   inline for the genuinely off-ladder case.
   =========================================================================== */
:root,
.theme-invert,
.theme-always-dark,
.theme-always-light {
  /* Foreground alpha — "N% of whatever the text colour is in this context". */
  --fg-5: rgb(var(--fg-rgb) / 0.05);
  --fg-10: rgb(var(--fg-rgb) / 0.1);
  --fg-15: rgb(var(--fg-rgb) / 0.15);
  --fg-20: rgb(var(--fg-rgb) / 0.2);
  --fg-30: rgb(var(--fg-rgb) / 0.3);
  --fg-35: rgb(var(--fg-rgb) / 0.35);
  --fg-40: rgb(var(--fg-rgb) / 0.4);
  --fg-50: rgb(var(--fg-rgb) / 0.5);
  --fg-60: rgb(var(--fg-rgb) / 0.6);
  --fg-70: rgb(var(--fg-rgb) / 0.7);
  --fg-80: rgb(var(--fg-rgb) / 0.8);
  --fg-90: rgb(var(--fg-rgb) / 0.9);

  /* Background alpha — for veils/blurs that must read as the surface. */
  --bg-5: rgb(var(--bg-rgb) / 0.05);
  --bg-10: rgb(var(--bg-rgb) / 0.1);
  --bg-15: rgb(var(--bg-rgb) / 0.15);
  --bg-20: rgb(var(--bg-rgb) / 0.2);
  --bg-30: rgb(var(--bg-rgb) / 0.3);
  --bg-35: rgb(var(--bg-rgb) / 0.35);
  --bg-40: rgb(var(--bg-rgb) / 0.4);
  --bg-50: rgb(var(--bg-rgb) / 0.5);
  --bg-60: rgb(var(--bg-rgb) / 0.6);
  --bg-70: rgb(var(--bg-rgb) / 0.7);
  --bg-80: rgb(var(--bg-rgb) / 0.8);
  --bg-90: rgb(var(--bg-rgb) / 0.9);

  /* SCRIMS — these must stay black in BOTH themes. A scrim exists to make text
     legible over a PHOTOGRAPH, and the photograph does not change when the
     theme does. Using --fg-* here would turn a scrim white on dark and destroy
     the text it was protecting. */
  --scrim-30: rgb(var(--black-rgb) / 0.3);
  --scrim-35: rgb(var(--black-rgb) / 0.35);
  --scrim-50: rgb(var(--black-rgb) / 0.5);
  --scrim-70: rgb(var(--black-rgb) / 0.7);

  /* SHADOWS — 8 real box-shadow values in src/ → 5 steps. Tinted via
     --shadow-rgb so light-theme shadows are teal and dark-theme shadows are
     black rather than an impossible white glow. */
  --shadow-xs: 0 2px 8px rgb(var(--shadow-rgb) / 0.12);
  --shadow-s: 0 6px 16px -6px rgb(var(--shadow-rgb) / 0.4);
  --shadow-m: 0 10px 30px rgb(var(--shadow-rgb) / 0.35);
  --shadow-l: 0 18px 44px -18px rgb(var(--shadow-rgb) / 0.4);
  --shadow-xl: 0 30px 70px -40px rgb(var(--shadow-rgb) / 0.7);
}


/* ===== styles\02-type.css ===== */
/* ===========================================================================
   TYPOGRAPHY — role tokens + role classes

   Replaces, in src/ alone: 40 distinct font-sizes (76% raw px), 16 distinct
   letter-spacings across three unit systems, and 21 distinct line-heights.

   It also replaced the display face's family name, which was retyped as a
   literal in 20 declarations across 11 files under TWO different fallback
   stacks — most files said "Vogue ML", "Times New Roman", serif while
   UnitDetail.css said "Vogue ML", serif, so the same heading font fell back to a
   different serif depending on which file styled it.
   =========================================================================== */

:root {
  /* ---- Font families ---------------------------------------------------
     --font-*-next are the variables next/font declares on <html> (layout.tsx).
     Reading them through these two names keeps the fallback stack in ONE place
     and keeps every stylesheet valid if next/font is ever removed.

     The fallbacks name only real system faces on purpose: with next/font gone
     there would be no @font-face either, so listing the custom family names here
     would just be a lie that resolves to the next entry anyway. */
  --font-sans: var(--font-sans-next, system-ui, sans-serif);
  --font-display: var(--font-display-next, "Times New Roman", serif);
  --font-icon: "Phosphor";   /* == vendor --font_awesome */

  /* ---- Weights ---------------------------------------------------------
     Only 4 are used in src/, and the vendor body default is 500 (not 400) —
     worth knowing before "normalising" anything to 400.
     --fw-black has exactly one use; kept so the grep gate can see it named. */
  --fw-regular: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-black: 900;

  /* ---- Tracking — 16 values in 3 unit systems (em, px, `normal`) → 8, all em.
     em rather than px so tracking scales with the fluid type scale; a fixed
     0.14px of tracking means something different at 11px and at 68px. */
  --track-tighter: -0.03em;  /* == the vendor body's -0.48px at 16px */
  --track-tight: -0.015em;   /* absorbs -0.2px (×5) -0.3px -0.4px */
  --track-none: 0;
  --track-1: 0.015em;        /* absorbs 0.01em (×5) 0.02em (×4) */
  --track-2: 0.04em;         /* absorbs 0.04em (×9) 0.05em 0.06em (×2) */
  --track-3: 0.09em;         /* absorbs 0.08em (×6) 0.1em (×3) */
  --track-4: 0.15em;         /* absorbs 0.14em (×7) 0.16em */
  --track-5: 0.22em;         /* absorbs 0.22em (×2) 3px (×2) 1.5px */

  /* Named aliases for the two that carry design intent, so a component reads as
     "this is the display tracking" rather than "this is 0.04em". */
  --track-display: var(--track-2);
  --track-eyebrow: var(--track-4);

  /* ---- Leading — 21 values → 8, all unitless.
     Unitless matters: the vendor sheet has 731 line-height declarations that are
     almost all px, which is why leading breaks on phones — a 20px line-height
     does not follow a font-size that shrank. The two px outliers in src/ convert
     to their measured ratio (globals.css `34px !important` on a 30px heading is
     1.133, which rounds into --leading-display at 1.14 × 30 = 34.2px). */
  --leading-none: 1;
  --leading-flat: 1.05;      /* absorbs 1.02 (×2) 1.04 */
  --leading-display: 1.14;   /* absorbs 1.1 1.12 1.15 (×3) */
  --leading-tight: 1.22;     /* absorbs 1.2 (×5); == vendor h1–h6 */
  --leading-snug: 1.35;      /* absorbs 1.3 (×2) 1.4 */
  --leading-normal: 1.5;     /* absorbs 1.45 1.5 (×2) */
  --leading-relaxed: 1.62;   /* absorbs 1.55 (×4) 1.6 (×4) 1.65 (×2) */
  --leading-loose: 1.72;     /* absorbs 1.7 (×5) 1.75 (×2) 1.8 */
}

/* ===========================================================================
   ROLE CLASSES

   The `font` shorthand cannot express this set inside a custom property (it
   requires a family and forbids letter-spacing), so composite roles are classes,
   not tokens. Individual tokens above remain for component CSS that styles an
   element it does not own and therefore cannot add a class to. Both exist on
   purpose.

   ---------------------------------------------------------------------------
   THE SPECIFICITY CONTRACT — why some selectors are doubled

   Next hoists every stylesheet imported from src/ ABOVE /assets/css/style.css in
   the built <head>. We therefore always lose on source order to the vendor and
   can only win on specificity. A role class landing on an element the vendor
   also styles (h1, h2, .sub-title, .title) is written DOUBLED — .ml-h1.ml-h1 is
   (0,2,0) and beats the vendor's (0,1,0) — while classes that only ever land on
   this project's own .ml-* / .rd-* markup stay single.

   This is the honest replacement for !important: it states "I am beating a
   vendor rule of equal specificity" in the selector itself, and unlike
   !important it can still be overridden by ordinary means downstream.
   =========================================================================== */

/* ---- Eyebrow / kicker: small, uppercase, wide-tracked ---------------------
   Doubled: lands on the vendor's .sub-title / .page-sub-title / .section-sub-title. */
.ml-eyebrow.ml-eyebrow {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--fw-semibold);
  line-height: var(--leading-none);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--text-label);
}

/* ---- Headings ------------------------------------------------------------
   h1/h2/h3 are the display serif (Vogue). h4–h6 are the sans: Vogue is a
   high-contrast display face that stops being readable below roughly 24px, which
   is the reason globals.css already splits large headings from small ones. */
.ml-h1.ml-h1 {
  font-family: var(--font-display);
  font-size: var(--text-5xl);
  font-weight: var(--fw-regular);
  line-height: var(--leading-display);
  letter-spacing: var(--track-display);
  color: var(--text-title);
}
.ml-h2.ml-h2 {
  font-family: var(--font-display);
  font-size: var(--text-4xl);
  font-weight: var(--fw-regular);
  line-height: var(--leading-display);
  letter-spacing: var(--track-display);
  color: var(--text-title);
}
.ml-h3.ml-h3 {
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: var(--fw-regular);
  line-height: var(--leading-tight);
  letter-spacing: var(--track-display);
  color: var(--text-title);
}
.ml-h4.ml-h4 {
  font-family: var(--font-sans);
  font-size: var(--text-2xl);
  font-weight: var(--fw-medium);
  line-height: var(--leading-tight);
  letter-spacing: var(--track-1);
  color: var(--text-title);
}
.ml-h5.ml-h5 {
  font-family: var(--font-sans);
  font-size: var(--text-xl);
  font-weight: var(--fw-medium);
  line-height: var(--leading-snug);
  letter-spacing: var(--track-1);
  color: var(--text-title);
}
.ml-h6.ml-h6 {
  font-family: var(--font-sans);
  font-size: var(--text-l);
  font-weight: var(--fw-semibold);
  line-height: var(--leading-snug);
  letter-spacing: var(--track-1);
  color: var(--text-title);
}

/* ---- Body + supporting roles ---------------------------------------------
   Single selectors: these only ever land on this project's own markup. */
.ml-body {
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: var(--fw-medium);
  line-height: var(--leading-relaxed);
  letter-spacing: var(--track-tight);
  color: var(--text-body);
}
.ml-body-lg {
  font-family: var(--font-sans);
  font-size: var(--text-l);
  font-weight: var(--fw-regular);
  line-height: var(--leading-normal);
  letter-spacing: var(--track-tight);
  color: var(--text-body);
}
.ml-body-sm {
  font-family: var(--font-sans);
  font-size: var(--text-m);
  font-weight: var(--fw-medium);
  line-height: var(--leading-relaxed);
  letter-spacing: var(--track-tight);
  color: var(--text-body);
}
/* Uppercase micro-label: spec keys, filter chips, stat captions. */
.ml-label {
  font-family: var(--font-sans);
  font-size: var(--text-2xs);
  font-weight: var(--fw-semibold);
  line-height: var(--leading-none);
  letter-spacing: var(--track-3);
  text-transform: uppercase;
  color: var(--text-label);
}
/* Sentence-case secondary text: image captions, helper text. */
.ml-caption {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--fw-regular);
  line-height: var(--leading-snug);
  letter-spacing: var(--track-none);
  color: var(--text-muted);
}
/* Smallest tier: legal lines, photo credits, breadcrumb tails. */
.ml-meta {
  font-family: var(--font-sans);
  font-size: var(--text-3xs);
  font-weight: var(--fw-medium);
  line-height: var(--leading-snug);
  letter-spacing: var(--track-4);
  text-transform: uppercase;
  color: var(--text-faint);
}


/* ===== styles\03-bridge.css ===== */
/* ===========================================================================
   LAYER 3 — VENDOR BRIDGE

   Re-points the purchased template's own custom properties at Layer 2 semantics,
   so that vendor declarations we never edit inherit the token system.
   /assets/css/style.css is 24,080 lines; bridging is how ~150 of its
   declarations become themeable without touching it.

   ---------------------------------------------------------------------------
   WHICH VENDOR TOKENS ARE BRIDGED, AND WHICH DELIBERATELY ARE NOT

   The vendor declares six colour tokens. Counting how each is actually USED
   decides which are safe to bridge — a token used for both `color` and
   `background-color` cannot be re-pointed at a single semantic without breaking
   one of its two jobs:

     var          uses  color  bg   border   verdict
     --primary    366   254    79   30       DO NOT BRIDGE — dual role
     --white      210   167    38   3        DO NOT BRIDGE — dual role
     --black      110   81     23   5        DO NOT BRIDGE — dual role
     --secondary   75   71     2    1        BRIDGE -> var(--text-body)
     --border      81   0      2    79       BRIDGE -> var(--line)
     --bg           0   -      -    -        dead; not bridged, not defined

   Note --border in particular: Layer 2 deliberately calls its own token --line
   instead. A vendor NAME declared in Layer 2 would bridge itself the instant the
   token layer is imported — which is exactly what happened on the first attempt
   here, moving every hairline on the site during a batch that was supposed to
   change nothing. Vendor names are Layer 3's job, and only Layer 3's.

   Concretely: --primary is the template's near-black used for heading text AND
   for dark-band backgrounds (.rr-bg-primary { background-color: var(--primary) }).
   Re-pointing it to the brand teal would turn the footer teal. --white is
   text-on-dark AND background-white; pointing it at a flipping surface token
   would make every dark-band label invisible. Those three are re-pointed only
   INSIDE a theme island, scoped, in 04-utilities.css.

   ---------------------------------------------------------------------------
   THE UNDEFINED VENDOR VARIABLES

   style.css calls eleven variables via var() that it never declares. An invalid
   var() makes the WHOLE declaration drop at computed-value time, so each of these
   is a vendor declaration doing nothing today. Defining them is mostly a free fix
   — but not always, and the difference matters:

   Two of them are LOAD-BEARING WHILE BROKEN. Defining these would be a
   regression, so they stay undefined on purpose:

     --black-6   `.progress-wrap svg path { fill: var(--black-6) }` applies to
                 BOTH svgs in ProgressWrap; the circle overrides it with
                 var(--white), so what is left is the scroll-to-top ARROW, whose
                 path carries the presentation attribute fill="none". A CSS
                 `fill` beats a presentation attribute — so defining --black-6
                 fills the arrow into a solid blob. The invalid declaration is
                 what keeps the arrow a clean stroke.

     --action    `.loader-wrap { background: var(--action) }`. The loader is a
                 curtain: a full-viewport <svg> path (fill:#000) that GSAP morphs
                 upward to reveal the page, then fades out. An opaque background
                 on the wrapper would hide the page during that reveal and turn
                 the curtain into a plain fade.

   Three are font variables and belong to the typography batch, not here.

   The rest are defined below. Four of them (--black-4 ×4, --black-9,
   --rr-common-white/black, --theme) sit on selectors that are unreachable from
   src/ today — `.offcanvas__menu-wrapper`, `.line-divider-sm`, `.scroll__down`
   and `.bar-icon.is-white/.is-black` are never rendered (Header uses a bare
   `.bar-icon`) — so they are inert now and correct if that markup ever appears.

   ---------------------------------------------------------------------------
   Everything here is scoped `:root:not(.theme-dark), :root.theme-dark` (0,2,0)
   so it out-specifies the vendor's bare `:root` (0,1,0). Source order is not
   available to us — see the header of 01-theme.css.
   =========================================================================== */

:root:not(.theme-dark),
:root.theme-dark {
  /* BRIDGE — 71 of 75 uses are `color`, and every one is body copy
     (`body { color: var(--secondary) }` plus paragraphs). Unambiguous.

     --text-body resolves to --ml-ink-600, which IS #555555 — the vendor's own
     original value — so this bridge is now colour-neutral for the ~624 vendor
     paragraphs while still routing them through the token layer, which is what
     makes them follow a theme island.

     It is a clear gain everywhere else: the three project body colours the token
     replaces measured 5.11:1 (#5f726c), 6.20:1 (#5a635f) and 4.34:1 (#6c7d77 —
     that one failed WCAG AA), and all three were green-tinted. They are now
     7.46:1 and neutral.

     The 2 background uses are `.theme-bg-secondary` (0 uses in src/) and one
     hairline div, both of which read acceptably as a body colour. */
  --secondary: var(--text-body);

  /* BRIDGE — 79 of 81 uses are border-*. Was rgba(17, 17, 17, 0.10); --line is
     also 10%, so this too is a hue shift only. Mapping it to a --fg-* value
     additionally makes every vendor hairline invert inside a theme island. */
  --border: var(--line);

  /* Restores `transform: translateY(var(--y)) translateZ(0)` on mobile-menu and
     offcanvas links. Currently the whole declaration drops, which also loses the
     translateZ(0) compositing hint the author clearly wanted. 0 is the resting
     state, so this is visually identical — it just stops being invalid. */
  --y: 0px;

  /* Foreground steps, named for the ladder they came from. Semantically "N0% of
     the current foreground", so they invert correctly inside a theme island —
     which matters for --black-4: all four of its uses are on the offcanvas menu,
     a dark panel with white link text, where a fixed dark value would be wrong.
     NOTE --black-6 is deliberately absent; see the header. */
  --black-4: var(--fg-40);
  --black-9: var(--fg-90);

  /* Single use each, on selectors not currently rendered. `--theme` backs
     `.theme-bg-theme`; the `--rr-common-*` pair backs the white/black variants
     of the hamburger bars. These two genuinely mean a fixed white and a fixed
     near-black rather than a semantic role, which is why they reach into Layer 1
     instead of Layer 2. */
  --theme: var(--accent);
  --rr-common-white: var(--ml-white);
  --rr-common-black: var(--ml-ink-900);

  /* ---- FONT FAMILIES ---------------------------------------------------
     MANDATORY, not cosmetic. The literal family names these tokens used to
     hold ("Bai Jamjuree", "Instrument Serif") no longer resolve: the Google
     @import that loaded them was removed from style.css and next/font
     generates hashed family names instead. Without these three lines, the 150
     vendor declarations reading var(--font_bai) would fall through to plain
     sans-serif. */
  --font_bai: var(--font-sans);

  /* `inherit` rather than a face. All 39 vendor uses of this token are an accent
     <span> inside a heading, always paired with font-style:italic, and
     globals.css already forces `font-family: inherit` on h1/h2 spans with the
     stated intent "one consistent typeface per heading, and Instrument Serif
     retired". Resolving to `inherit` generalises that from two heading levels to
     all six in one line, instead of naming a third typeface.
     (CSS-wide keywords do survive var() substitution — verified in-browser.)
     Only ONE of the 39 is reachable from src/ and is not a heading span:
     `.service-6__thumb span`, the "01."–"04." numerals in ServicesSection. That
     one is handled in the ML LATE-CASCADE block of style.css, because `inherit`
     alone would leave it italic with no italic face to use. */
  --font_instrument: inherit;

  /* These three pointed at the zero-byte .html "fonts" deleted from style.css.
     They were already falling back; aliasing makes the fallback a decision.
     Granola read as a display face, the other two as text sans. */
  --font_granola: var(--font-display);
  --font_bdo-grotsk: var(--font-sans);
  --font_suisse-int: var(--font-sans);

  /* The last three of the eleven never-declared vendor variables.
     --font_bdogrotesk is a typo of --font_bdo-grotsk that silently killed 11
     declarations; the other two name faces this project does not load. */
  --font_bdogrotesk: var(--font_bdo-grotsk);
  --font_inter: var(--font-sans);
  --font_ppeditor: var(--font-display);
}


/* ===== styles\04-utilities.css ===== */
/* ===========================================================================
   UTILITIES — theme islands, section rhythm, measures

   Small, composable classes only. No component styling belongs here.
   =========================================================================== */

/* ---------------------------------------------------------------------------
   THEME ISLANDS

   The three classes are DECLARED (given their token values) by the paired groups
   in 01-theme.css. This block only gives them the two properties an island needs
   to actually look like one, plus the scoped vendor re-points.

   Classes are added in JSX, not aliased onto vendor class names here, for two
   reasons: the island stays visible in the markup where a reader will look for
   it, and the .theme-invert algebra in 01-theme.css keeps working (aliasing
   .rr-bg-primary to an island would create a second, invisible nesting level).
   --------------------------------------------------------------------------- */
.theme-invert,
.theme-always-dark,
.theme-always-light {
  /* Both of these are (0,1,0) and this file loads BEFORE the vendor sheet, so on
     an element that also carries a vendor background class they lose — e.g.
     `.footer-4.theme-bg-primary.theme-always-dark` keeps the vendor's
     `background-color: var(--primary)` (#111111). That is intended: an island
     declares the token DIRECTION, and a band that already paints itself keeps
     its own colour. The declarations here are the floor, so that a plain
     `<section class="theme-always-dark">` with no background of its own still
     renders as a dark surface instead of light text on white. */
  background-color: var(--bg-body);
  color: var(--text-body);

  /* Declared as --color-scheme by the paired groups in 01-theme.css and applied
     HERE so it lands on islands only, never on :root. Reading it from the token
     means the island algebra lives in exactly one place: we do not have to
     re-derive "which direction is this island facing" a second time.
     This is what makes `color-scheme: dark` on .rd-rail__card unnecessary — the
     card becomes an island and gets it automatically, which is what keeps
     Chrome's <input type="date"> indicator visible on the dark card. */
  color-scheme: var(--color-scheme);

  /* NO VENDOR TOKEN IS RE-POINTED HERE. This block used to set
     `--primary: var(--text-title)`, which is wrong on precisely the elements it
     was written for: the vendor's dark bands ARE
     `.rr-bg-primary { background-color: var(--primary) }`, so re-pointing
     --primary on the band element turns the footer's background WHITE in a dark
     island. --primary's dual color+background role makes it unsafe to re-point
     anywhere it might be read as a background.

     Heading colour does not need it. `globals.css` re-points --primary on the
     HEADING elements themselves (h1..h6), where the token can only ever be read
     as a colour, and it resolves to var(--text-title) — which this island has
     already flipped to white. Same mechanism, applied where it is unambiguous.

     --white is likewise left alone: inside a dark island the vendor's
     var(--white) already means "white text on this dark band", which is correct.
     Re-pointing it at a flipping surface token is how 38 vendor dark-band labels
     would go invisible. */
}

/* ---------------------------------------------------------------------------
   SECTION RHYTHM

   This mirrors the vendor's .section-spacing ladder (170 → 100 → 80 → 60)
   exactly. It is deliberately STEPPED, not fluid: a fluidised value would drift
   away from the hundreds of untouched vendor .section-spacing sections that sit
   on the same page, and the mismatch reads as a broken rhythm.

   The literals are spelled out because custom properties are illegal in @media
   conditions. Keep in sync with src/config/breakpoints.ts.
   --------------------------------------------------------------------------- */
:root {
  --space-section: 170px;
}
@media (max-width: 1919px) {
  :root {
    --space-section: 100px;
  }
}
@media (max-width: 1399px) {
  :root {
    --space-section: 80px;
  }
}
@media (max-width: 991px) {
  :root {
    --space-section: 60px;
  }
}

.ml-section {
  padding-block: var(--space-section);
}
/* Tighter rhythm for bands that sit INSIDE a section rather than being one —
   proportional to the ladder so it holds at every tier instead of being
   re-derived per breakpoint the way .ml-gallery-area does today. */
.ml-section--tight {
  padding-block: calc(var(--space-section) * 0.6);
}

/* ---------------------------------------------------------------------------
   MEASURE — centred reading widths.
   --------------------------------------------------------------------------- */
.ml-measure-s,
.ml-measure-m,
.ml-measure-l,
.ml-measure-xl,
.ml-measure-2xl {
  margin-inline: auto;
}
.ml-measure-s { max-width: var(--measure-s); }
.ml-measure-m { max-width: var(--measure-m); }
.ml-measure-l { max-width: var(--measure-l); }
.ml-measure-xl { max-width: var(--measure-xl); }
.ml-measure-2xl { max-width: var(--measure-2xl); }


/* ===== app\(site)\globals.css ===== */
/* Mirna Luka — the template's own style.css (loaded via <link> in layout)
   owns all styling. Keep this file free of any reset that would fight it. */

/* Residences filter grid: keep non-matching cells hidden (backup for the
   scoped rule in ResidencesGallery.css; toggled by ml-enhance.js). */
.works-section-10__item.is-hidden { display: none; }

/* Fix: template sets `main { display: inline-block }`, so <main> aligns to the
   text baseline and reserves a few px of descender space below it — that shows
   as a tiny white strip above the dark footer on every page. Re-align to top to
   remove the gap without changing the template's display/overflow. */
#smooth-content > main { vertical-align: top; }

/* Blog article + legal pages: the OUTER container aligns to the site width
   (rr-container-1790) so meta/title stay flush with the header & footer, but
   the reading body is capped to a comfortable centered measure so long-form
   text and its image rows don't stretch the full 1790px. */
.blog-details__content {
  max-width: 1100px;
  margin-left: auto;
  margin-right: auto;
}

/* The @font-face for "Vogue ML" was here (and duplicated byte-for-byte in
   ResidencesGallery.css). Both are gone: next/font/local in layout.tsx now owns
   the single declaration, from an 11KB woff2 instead of the 31KB raw TTF, and
   fingerprints + preloads it. Reference the face as var(--font-display). */

/* ===========================================================================
   Global heading COLOUR.

   Applied by re-pointing the template's own --primary token ON the heading
   element rather than by a `color` rule. That indirection is the valuable part
   and is deliberately kept: the vendor colours every heading through a single
   `h1..h6 { color: var(--primary) }` at specificity (0,0,1), so changing the
   VALUE it reads works regardless of what specificity any other vendor rule
   uses — no arms race, no !important.

   --text-title is a theme token, so this single rule now handles dark bands too:
   inside a .theme-always-dark island --text-title resolves to white, and these
   headings turn white by themselves. That replaced a 12-selector
   `.rr-bg-primary h1..h6 { --primary: var(--white); color: var(--white) !important }`
   counter-hack plus a 6-selector `... span { color: inherit !important }` rule.

   Heading spans need no colour rule for the same reason: custom properties
   inherit, so a span inside the heading reads the heading's own --primary.

   COLOUR FOLLOWS THE FACE, NOT THE HEADING LEVEL. The brand teal is a DISPLAY
   colour — it works on Vogue and reads as a mistake on the sans. So the three
   rules below deliberately mirror the FONT rules further down, selector for
   selector: whatever is forced onto the sans face is also forced off the teal.
   (An earlier pass applied teal to h1–h6 flat, which put /explore's and
   /contact's 130px sans page titles in teal.)

   All six levels are still covered, which is what makes deleting the old
   `.rr-bg-primary h1..h6 { color: var(--white) !important }` counter-hack safe:
   both --text-title and --text-heading resolve to white inside a
   .theme-always-dark island, so a heading on a dark band cannot end up
   near-black on near-black whichever rule wins.
   =========================================================================== */

/* 1. Default for every heading: the sans heading colour (the vendor's #111111). */
h1,
h2,
h3,
h4,
h5,
h6 {
  --primary: var(--text-heading);
}
/* 2. h1/h2 carry Vogue, so they get the brand teal. Same specificity as rule 1,
      later in the file — order is what promotes them. */
h1,
h2 {
  --primary: var(--text-title);
}
/* 3. …except the h1/h2 that the font rules below force BACK onto the sans.
      Class selectors, so these outrank rule 2 without needing !important.
      Keep this list identical to the two sans font-family blocks below. */
.sub-title,
.page-sub-title,
.section-sub-title,
.breadcrumb__sub-title,
.footer-4__inner .footer-widget-box .title,
.blog-6__inner .blog-6__item .text .title,
.projects__box .title,
.featured-project-3__right .content .title {
  --primary: var(--text-heading);
}

/* Vogue is a high-contrast DISPLAY serif — it sings at large sizes and gets hard
   to read when small. So only h1/h2 get it; small headings and eyebrow labels
   stay on the legible sans (--font-sans). Colour above applies to all six. */
h1,
h2 {
  font-family: var(--font-display) !important;
  letter-spacing: var(--track-display) !important;
}
/* Accent words are wrapped in a child <span> that the template styles with
   Instrument Serif italic. Make every heading span inherit its heading's font
   (Vogue on big titles, sans on the small labels below) and never italic —
   one consistent typeface per heading, and Instrument Serif retired. Also
   covers the .ml-split-word / .ml-split-char spans injected by the reveal. */
h1 span,
h2 span {
  font-family: inherit !important;
  font-style: normal !important;
}

/* --- Small headings → keep the legible sans, NOT the display serif --------- */
/* Eyebrow / kicker labels (small, uppercase): sans + generous tracking. */
.sub-title,
.page-sub-title,
.section-sub-title,
.breadcrumb__sub-title {
  font-family: var(--font_bai) !important;
  letter-spacing: 0.14em !important;
}
/* Small functional titles / values that also use `.title` but are card-sized
   (≤ ~24px) — footer widgets, blog cards, building spec figures: sans, normal
   tracking, so they stay legible. Large section `.title` keep Vogue. */
.footer-4__inner .footer-widget-box .title,
.blog-6__inner .blog-6__item .text .title,
.projects__box .title,
.featured-project-3__right .content .title {
  font-family: var(--font_bai) !important;
  letter-spacing: normal !important;
}

/* Investment value-box heading (`.number` — "Seafront", "Summer let"…) and
   feature-box heading (`.title` — "Buy direct, no chain"…). The template
   rendered `.number` as a 90px odometer-style figure and `.feature-box .title`
   with heavy negative tracking; as short word headings they read better as
   proper Vogue display headings at a consistent h3 size (30px). Both are <h3>,
   so colour comes from the h1..h6 --primary rule above — white here because
   these sections carry .theme-always-dark. */
.value-area .value-box .number,
.feature-area .feature-box .title {
  font-family: var(--font-display) !important;
  font-size: 30px !important;
  font-weight: 400;
  line-height: 34px !important;
  letter-spacing: 0.04em !important;
}
.value-area .value-box .number {
  margin-top: 24px;
}

/* ===========================================================================
   Investment — Circular Gallery (reactbits, OGL/WebGL). A dark section slotted
   between the feature and value areas. The stage holds the drag/scroll canvas;
   it inherits the dark background so the transparent canvas blends in.
   =========================================================================== */
.ml-gallery-area {
  padding-top: 60px;
  padding-bottom: 60px;
}
.ml-gallery-area .ml-gallery-stage {
  position: relative;
  width: 100%;
  height: 600px;
}
@media (max-width: 991px) {
  .ml-gallery-area {
    padding-top: 40px;
    padding-bottom: 40px;
  }
  .ml-gallery-area .ml-gallery-stage {
    height: 480px;
  }
}
@media (max-width: 575px) {
  .ml-gallery-area {
    padding-top: 30px;
    padding-bottom: 30px;
  }
  .ml-gallery-area .ml-gallery-stage {
    height: 380px;
  }
}
.ml-gallery-area .circular-gallery canvas {
  display: block;
}

/* ===========================================================================
   Split-text reveal — every H1/H2 animates in as staggered characters
   (words kept intact so wrapping is preserved). Pure CSS + IntersectionObserver
   (see ml-enhance.js) so it never fights the template's GSAP ScrollSmoother.
   =========================================================================== */
.ml-split-word {
  display: inline-block;
  white-space: nowrap;
}
.ml-split-char {
  display: inline-block;
  will-change: transform, opacity;
}
.ml-split-ready .ml-split-char {
  opacity: 0;
  transform: translateY(0.42em);
  transition: opacity 0.7s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}
.ml-split-in .ml-split-char {
  opacity: 1;
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  .ml-split-ready .ml-split-char {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ===========================================================================
   Stagger reveal — the direct children of any `[data-ml-reveal]` wrapper fade
   and slide up in order as they scroll into view (home property-types grid).
   Same easing and duration as the split-text reveal above so the page has one
   motion language.

   `.ml-reveal-item` is the HIDDEN state and is added by ml-enhance.js, never by
   the server markup: if that script never runs, or the reader prefers reduced
   motion, the cards are plainly visible instead of stuck at opacity 0.
   The per-item `transition-delay` is set inline by the same script.
   =========================================================================== */
.ml-reveal-item {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
  will-change: opacity, transform;
}
.ml-reveal-item.is-in {
  opacity: 1;
  transform: none;
}
@media (prefers-reduced-motion: reduce) {
  .ml-reveal-item {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* React Bits — ScrollReveal: per-word spans need inline-block so the
   scroll-scrubbed opacity/blur render individually (see ScrollReveal.tsx). */
.ml-sr-word {
  display: inline-block;
  will-change: opacity, filter;
}

/* ===========================================================================
   Site footer — six columns instead of the template's four. The newsletter
   column was replaced by three residence columns (P1, P2, Villas) that list
   every detail page, so the footer works as a full residences sitemap. The
   template's `footer-4` grid is hard-coded to four tracks with nth-child
   paddings; this block replaces that geometry (scoped to .ml-footer-cols) and
   tightens the widget spacing so six shorter columns stay legible.
   =========================================================================== */
.footer-4__inner .footer-4__wrapper.ml-footer-cols {
  gap: 0 40px;
  grid-template-columns: minmax(280px, 1.4fr) repeat(5, minmax(140px, 1fr));
}
.footer-4__inner .footer-4__wrapper.ml-footer-cols .footer-widget-box {
  padding: 100px 0 70px;
}
.footer-4__inner .footer-4__wrapper.ml-footer-cols .footer-widget-box:first-child {
  padding-right: 40px;
  border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.footer-4__inner .footer-4__wrapper.ml-footer-cols .footer-widget-box:last-child {
  margin-right: 0;
}
/* Lists sit just under their heading (the template pushed them 117px down to
   bottom-align two tall columns — with six columns that reads as a void). */
.footer-4__inner .footer-4__wrapper.ml-footer-cols .footer-widget-box .footer-nav-list {
  margin-top: 34px;
}
.footer-4__inner .footer-4__wrapper.ml-footer-cols .footer-widget-box .content .info {
  margin-top: 60px;
}
/* Address lines are block spans rather than a <br>: the template hides every
   <br> below 768px, which ran "Primošten, Croatia" into the GPS line on phones. */
.footer-4__inner .footer-4__wrapper.ml-footer-cols .content .info a > span {
  display: block;
}
@media (max-width: 1919px) {
  .footer-4__inner .footer-4__wrapper.ml-footer-cols {
    gap: 0 28px;
    grid-template-columns: minmax(240px, 1.3fr) repeat(5, minmax(120px, 1fr));
  }
  .footer-4__inner .footer-4__wrapper.ml-footer-cols .footer-widget-box:first-child {
    padding-right: 28px;
  }
}
/* Below ~1400px six tracks stop fitting — wrap into 3 × 2, then 2, then 1.
   The contact column loses its divider once it no longer leads a single row. */
@media (max-width: 1399px) {
  .footer-4__inner .footer-4__wrapper.ml-footer-cols {
    gap: 0 30px;
    grid-template-columns: repeat(3, 1fr);
  }
  .footer-4__inner .footer-4__wrapper.ml-footer-cols .footer-widget-box {
    padding: 60px 0 40px;
  }
  .footer-4__inner .footer-4__wrapper.ml-footer-cols .footer-widget-box:first-child {
    padding-right: 0;
    border-right: none;
  }
  .footer-4__inner .footer-4__wrapper.ml-footer-cols .footer-widget-box .content .info {
    margin-top: 30px;
  }
}
@media (max-width: 767px) {
  .footer-4__inner .footer-4__wrapper.ml-footer-cols {
    grid-template-columns: repeat(2, 1fr);
    gap: 0 20px;
  }
  .footer-4__inner .footer-4__wrapper.ml-footer-cols .footer-widget-box {
    padding: 40px 0 20px;
  }
}
@media (max-width: 575px) {
  .footer-4__inner .footer-4__wrapper.ml-footer-cols {
    grid-template-columns: 1fr;
  }
  .footer-4__inner .footer-4__wrapper.ml-footer-cols .footer-widget-box {
    padding: 30px 0 10px;
  }
  .footer-4__inner .footer-4__wrapper.ml-footer-cols .footer-widget-box:last-child {
    padding-bottom: 50px;
  }
  .footer-4__inner .footer-4__wrapper.ml-footer-cols .footer-widget-box .footer-nav-list {
    margin-top: 20px;
  }
}

/* Copyright block: the studio credit sits on its own line under the © line
   (block spans rather than a <br>, which the template hides below 768px).
   The template's track for this cell is narrow, so keep both lines flush right
   with the container — below 992px the template centres the whole row instead. */
.footer-4__inner .copyright .copyright__text > span {
  display: block;
}
@media (min-width: 992px) {
  .footer-4__inner .copyright .copyright__text {
    text-align: right;
  }
}
/* Legal links (Privacy · Terms · Cookies) run in a single row — the template
   stacks them as a nav column, which reads as a fourth menu next to the logo. */
.footer-4__inner .copyright__menu {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 28px;
}
@media (max-width: 991px) {
  .footer-4__inner .copyright__menu {
    justify-content: center;
  }
}

/* Home — about section, 992–1199px only: the template pins the three tracks to
   fixed pixels (350 + 350 + 214 + 2×20 gap = 954px), which is wider than the
   container at the low end of that band (992 − 30 gutter − scrollbar ≈ 947px),
   so the right-hand image gets clipped. Same proportions, expressed as fr so the
   row can never outgrow its container. Below 992px the template's own two-column
   / single-column rules take over and are already fluid. */
@media (min-width: 992px) and (max-width: 1199px) {
  [data-section="home-about"] .about-6__inner {
    grid-template-columns: 1.63fr 1.63fr 1fr;
  }
}

/* About — experience stat: pull the big number up close to its label.
   The template sets a large margin-top (193px) between .experience__item's
   label and number; scope it down just for the About stats band. */
[data-section="about-experience"] .experience__item .title {
  margin-top: 16px;
}

/* Listing heading: the template pushes the second line of a .section-title__wrap
   title to the right (text-align: right plus a large per-breakpoint
   padding-left), which leaves "residences" hanging away from "All" above it.
   Keep both lines on the same left margin. Higher specificity than the
   template's breakpoint rules, so one declaration covers every screen size.
   Keyed to .ml-res-section, not a data-section, because the same block renders
   on /residences and on both building pages under different section ids. */
.ml-res-section .section-title__wrap .section-title span {
  padding-left: 0;
  text-align: left;
}


/* ===== components\common\Buttons.css ===== */
/* ===========================================================================
   Mirna Luka — BUTTON SYSTEM (single source of truth).
   The whole site uses exactly two button shapes. Nothing else, no radius.

     .ml-btn    SOLID — black fill, white label, up-right arrow, letter-morph
                hover. Extends the template's `.rr-btn-primary` (its JS splits
                `.text` into per-char spans + adds `.has-icon` for the morph),
                but forces a square, solid, black look. Ref: home hero-6.
     .ml-line   LINE  — uppercase teal label, right arrow, 1px border-bottom,
                gap-grows on hover. Standalone. Ref: home project-6 "View all".

   Variants (compose): `--invert` for dark backgrounds, `--sm` for a tighter
   solid button. Every rule is a compound selector so it outranks the template
   defaults regardless of stylesheet load order.
   =========================================================================== */

/* ---- SOLID (.ml-btn) ------------------------------------------------------ */
.rr-btn-primary.ml-btn {
  gap: 8px;
  border-radius: 0;
  padding: 18px 30px;
  color: var(--white);
  background-color: var(--black);
  border-color: transparent;
  text-transform: none;
}
.rr-btn-primary.ml-btn .text span {
  color: var(--white);
  text-transform: none;
  letter-spacing: -0.4px;
}
.rr-btn-primary.ml-btn i {
  font-weight: 400;
  color: var(--white);
  transform: rotate(-45deg);
}
/* keep the fill on hover (base .rr-btn-primary flips white→black); the label
   still runs the template letter-morph, the arrow still flies out */
.rr-btn-primary.ml-btn:hover {
  background-color: var(--black);
}
.rr-btn-primary.ml-btn:hover .text span.animate {
  color: var(--white);
}
.rr-btn-primary.ml-btn:hover i {
  color: var(--white);
}

/* Inverse — white fill / dark label, for dark sections (header, dark heroes) */
.rr-btn-primary.ml-btn.ml-btn--invert {
  color: var(--black);
  background-color: var(--white);
}
.rr-btn-primary.ml-btn.ml-btn--invert .text span,
.rr-btn-primary.ml-btn.ml-btn--invert i {
  color: var(--black);
}
.rr-btn-primary.ml-btn.ml-btn--invert:hover {
  background-color: var(--white);
}
.rr-btn-primary.ml-btn.ml-btn--invert:hover .text span.animate,
.rr-btn-primary.ml-btn.ml-btn--invert:hover i {
  color: var(--black);
}

/* Small — tighter padding for inline / header use */
.rr-btn-primary.ml-btn.ml-btn--sm {
  padding: 13px 24px;
  font-size: 15px;
}

/* ---- LINE (.ml-line) ------------------------------------------------------ */
.ml-line {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 0 0 6px;
  margin: 0;
  border: 0;
  border-bottom: 1px solid #17332e;
  background: none;
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #17332e;
  text-decoration: none;
  cursor: pointer;
  transition: gap 0.3s ease, opacity 0.3s ease;
}
.ml-line i {
  font-size: 15px;
  line-height: 1;
  transition: transform 0.3s ease;
}
.ml-line:hover {
  gap: 16px;
  opacity: 0.75;
}

/* Inverse — light label + rule for dark backgrounds (mega menu, dark sections) */
.ml-line.ml-line--invert {
  color: #ffffff;
  border-bottom-color: rgba(255, 255, 255, 0.55);
}
.ml-line.ml-line--invert:hover {
  opacity: 0.85;
}


/* ===== components\common\CallToAction.css ===== */
/* ===========================================================================
   Closing call-to-action block — used by the residence detail pages, the
   residences index, the explore page and both building pages.

   Base styling comes from the template (`call-to-action`, Home 03, in
   public/assets/css/style.css). These rules are the site's overrides: the
   background image stays full-bleed while the copy is pulled back into the
   shared 1790 container so its left edge lines up with the header and footer.
   The container is overlaid over the image, and the template's absolute offset
   on `.section-title-4__wrapper` is dropped so the copy flows from the
   container's own left edge instead of a fixed 80px.

   Moved here from unit/UnitDetail.css when the block became the shared
   <CallToAction> component — see that file's header.
   =========================================================================== */

/* The media keeps its `data-speed` parallax, which can drift and expose a
   sliver below the image. Painting the CTA the same colour as the footer
   (`--primary`) makes any such gap blend into the footer instead of flashing
   white. */
.call-to-action,
.call-to-action .call-to-action__inner {
  background-color: var(--primary);
}
.call-to-action__inner .call-to-action__content {
  position: absolute;
  z-index: 2;
  left: 0;
  right: 0;
  bottom: 140px;
}
.call-to-action__inner .call-to-action__content .section-title-4__wrapper {
  position: static;
  left: auto;
  bottom: auto;
  max-width: 620px;
}
/* Subtitle is a plain label here — no pill border/padding. */
.call-to-action__inner .call-to-action__content .section-title-4__wrapper .sub-title {
  border: 0;
  padding: 0;
}
/* The button itself is the shared <PrimaryButton> (.ml-btn) — no CTA-local
   button styling needed. */
@media (max-width: 1199px) {
  .call-to-action__inner .call-to-action__content {
    bottom: 50px;
  }
}
@media (max-width: 991px) {
  .call-to-action__inner .call-to-action__content {
    bottom: 30px;
  }
}
@media (max-width: 575px) {
  .call-to-action__inner .call-to-action__content {
    bottom: 20px;
  }
}

/* Lead paragraph between the title and the button. */
.call-to-action__inner .section-title-4__wrapper .cta-lead {
  max-width: 520px;
  margin: 22px 0 30px;
  font-size: 17px;
  font-weight: 400;
  line-height: 1.7;
  color: rgba(255, 255, 255, 0.85);
}
@media (max-width: 767px) {
  .call-to-action__inner .section-title-4__wrapper .cta-lead {
    max-width: 100%;
    margin: 16px 0 22px;
    font-size: 15px;
    line-height: 1.6;
  }
}

/* Phones: let the band be as tall as its copy.

   The template's Home-03 CTA pins `.media` to a flat 250px below 767px and
   positions the copy absolutely over it — which works for the two lines of
   title-plus-button that block was drawn for. Ours carries a lead paragraph as
   well, so the copy runs ~290px and the top of it (the sub-title, and the first
   line of the title) was being cut off by __inner's overflow:hidden.

   So on phones the layers swap roles: the image becomes an absolute backdrop
   and the copy goes back in flow, which makes __inner's height follow the copy.
   That holds for any length of lead, on every page that calls this part — no
   magic height to re-tune when the words change.

   Both selectors carry .call-to-action as well, purely for weight: style.css
   loads after this file, so a tie on specificity would go to the template. */
@media (max-width: 767px) {
  .call-to-action .call-to-action__inner .media {
    position: absolute;
    inset: 0;
    height: auto;
  }
  .call-to-action .call-to-action__inner .call-to-action__content {
    position: relative;
    bottom: auto;
    padding-top: 54px;
    padding-bottom: 54px;
  }
}


/* ===== components\home\HeroSection.css ===== */
/* Mirna Luka — hero-6 title uses the Vogue display serif */
.hero-6 .hero-6__title-wrap .title,
.hero-6 .hero-6__title-wrap .title span {
  font-family: var(--font-display) !important;
  font-style: normal !important;
  color: #17332e;
}

/* Mirna Luka — hero-6 subtitle: drop the pill (border + padding + dot),
   plain uppercase label to match the current style. */
.hero-6 .hero-6__title-wrap .sub-title {
  padding: 0 !important;
  border: none !important;
  border-radius: 0 !important;
  text-transform: uppercase;
  letter-spacing: 1.5px;
}
.hero-6 .hero-6__title-wrap .sub-title::before {
  display: none !important;
}


/* ===== components\home\PropertyTypes.css ===== */
/* ===========================================================================
   Home — property types ("Two buildings, two seafront villas").

   Four cards on a light-gray band directly below the video-scroll intro. Nothing
   in the template matches this layout, so the grid and the card are written from
   scratch here; only the section head borrows the template's
   `.section-title__wrapper` three-column grid.

   Palette, spacing and motion follow the rest of the site: teal #17332E for
   headings and values, Vogue for the card titles (the same treatment
   globals.css gives the investment value boxes), Bai Jamjuree for everything
   small, and the 0.22/1/0.36/1 ease used by the split-text reveal.

   Component: components/home/PropertyTypes.tsx
   Switch:    src/config/page-sections.ts → home / "home-property-types"
   =========================================================================== */

/* --- Section head --------------------------------------------------------- */
/* The template's `.section-title__wrapper` is a THREE-column grid
   (430px 810px 1fr) that puts the kicker, the title and the lead paragraph side
   by side. It reads badly here: the kicker sat alone in a 400px column with the
   title starting a second column 380px to its right, and the tiers degrade
   rather than adapt — at 1199px the kicker column drops to 130px, so
   "What is for sale" wrapped inside 80px, while at 1399px the lead was squeezed
   to 260px and pushed the title to 220px tall.

   Replaced with two columns: kicker + title stacked in one headline block, lead
   and CTA beside them. Same shape as `.ml-residences__head` elsewhere on the
   site.

   Specificity (0,2,0) on purpose — the vendor re-declares the columns at four
   breakpoints, but media queries add no specificity, so this single rule beats
   all of them and only our own tiers below need to be written. */
.ml-types .ml-types__head {
  grid-template-columns: minmax(0, 1fr) minmax(300px, 0.66fr);
  align-items: end;
  gap: var(--space-l) var(--space-3xl);
}
/* Flush the kicker to the same left edge as the title.
   The template indents `.section-title__wrapper .section-sub-title` by 50px and
   draws a 40px rule in that gutter via ::before — but the rule is positioned
   `left: 0` against the kicker itself, which is `position: relative`, so it
   lands ON the text rather than beside it. It reads as a faint strikethrough
   through the first word (both are near-black, which is why it went unnoticed
   while the kicker sat alone in its own column).
   Stacked under the title, the 50px indent also just reads as misalignment. So:
   drop both. Matches `.ml-residences__head`, which is this site's own section
   head and has no rule ornament. */
.ml-types .ml-types__headline .section-sub-title {
  margin-left: 0;
  margin-bottom: var(--space-m);
}
.ml-types .ml-types__headline .section-sub-title::before {
  content: none;
}

/* One column below the template's own collapse point, where the lead no longer
   has room to sit beside a 50px title. */
@media (max-width: 1199px) {
  .ml-types .ml-types__head {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Lead paragraph + the section's one primary CTA (→ /residences,
   see docs/CTA-TARGETS.md). */
.ml-types__intro .desc {
  max-width: 560px;
}
.ml-types__intro .ml-line {
  margin-top: 28px;
}

/* --- Grid ----------------------------------------------------------------- */
.ml-types__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 50px 40px;
  margin: 90px 0 0;
  padding: 0;
  list-style: none;
}
@media (max-width: 1399px) {
  .ml-types__grid {
    gap: 40px 30px;
    margin-top: 70px;
  }
}
/* One column from tablet down — two cards side by side would squeeze the three
   stat columns below their labels. */
@media (max-width: 991px) {
  .ml-types__grid {
    grid-template-columns: minmax(0, 1fr);
    gap: 30px;
    margin-top: 50px;
  }
}

/* --- Card ----------------------------------------------------------------- */
/* Equal-height cards: the item stretches, the card fills it, and the body's
   `margin-top: auto` on the CTA pins every link to the same baseline. */
.ml-types__item {
  display: flex;
  min-width: 0;
}
.ml-types__card {
  position: relative;
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  background-color: var(--white);
}

.ml-types__media {
  position: relative;
  overflow: hidden;
  aspect-ratio: 16 / 10;
}
.ml-types__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.9s cubic-bezier(0.22, 1, 0.36, 1);
}
/* Guarded — see the note on the other hover rules: a tap on touch fires :hover
   and leaves the image stuck mid-zoom. Nothing is revealed by it, so losing the
   effect on touch costs nothing. */
@media (hover: hover) and (pointer: fine) {
  .ml-types__card:hover .ml-types__media img {
    transform: scale(1.05);
  }
}
@media (prefers-reduced-motion: reduce) {
  .ml-types__media img,
  .ml-types__card:hover .ml-types__media img {
    transition: none;
    transform: none;
  }
}

/* "5 apartments" / "One house" — the one fact that separates a building from a
   villa, kept on the image so it reads before the copy does. */
.ml-types__badge {
  position: absolute;
  top: 20px;
  left: 20px;
  padding: 9px 14px;
  background-color: rgba(23, 51, 46, 0.92);
  color: var(--white);
  font-family: var(--font_bai);
  font-size: 12px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.ml-types__body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  padding: 34px 34px 38px;
}
@media (max-width: 575px) {
  .ml-types__body {
    padding: 26px 22px 30px;
  }
}

.ml-types__kind {
  margin: 0;
  font-family: var(--font_bai);
  font-size: 12px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(23, 51, 46, 0.55);
}

/* Card-sized display heading — same recipe globals.css uses for the investment
   value boxes, since an h3 gets neither the Vogue nor the teal from globals. */
.ml-types__title {
  margin: 14px 0 0;
  font-family: var(--font-display);
  font-size: 34px;
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: 0.04em;
  color: #17332e;
}
@media (max-width: 575px) {
  .ml-types__title {
    font-size: 28px;
  }
}

.ml-types__desc {
  margin: 16px 0 0;
  font-family: var(--font_bai);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.7;
  color: var(--secondary);
}
@media (max-width: 575px) {
  .ml-types__desc {
    font-size: 15px;
    line-height: 1.6;
  }
}

/* --- Stats ---------------------------------------------------------------- */
.ml-types__stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 20px;
  margin: 28px 0 0;
  padding-top: 24px;
  border-top: 1px solid var(--border);
}
.ml-types__stat-label {
  font-family: var(--font_bai);
  font-size: 11px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(23, 51, 46, 0.55);
}
.ml-types__stat-value {
  margin: 8px 0 0;
  font-family: var(--font_bai);
  font-size: 18px;
  font-weight: 500;
  line-height: 1.3;
  color: #17332e;
}
/* On a phone the card is too narrow for three tracks — "65–66 m²" would wrap
   mid-figure. Stack the stats as label/value rows instead. */
@media (max-width: 575px) {
  .ml-types__stats {
    grid-template-columns: minmax(0, 1fr);
    gap: 12px;
  }
  .ml-types__stat {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;
  }
  .ml-types__stat-value {
    margin: 0;
    font-size: 16px;
  }
}

/* --- Card link ------------------------------------------------------------ */
/* The shared `.ml-line` button (common/Buttons.css) is the card's only link.
   Its ::after stretches over the whole card so the image and the copy are
   clickable too, without wrapping a heading or an image in an <a> — see
   docs/CTA-TARGETS.md rule 2. */
.ml-line.ml-types__cta {
  margin-top: auto;
  padding-top: 30px;
  align-self: flex-start;
}
.ml-line.ml-types__cta::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
}
/* Hovering anywhere on the card runs the button's own hover (gap + fade). The
   link keeps its native focus ring — it is visible text at the foot of the card,
   so no card-level focus style is needed. */
.ml-types__card:hover .ml-line.ml-types__cta {
  gap: 16px;
  opacity: 0.75;
}


/* ===== components\home\ServicesSection.css ===== */
/* Mirna Luka — service-6 title marquee-on-hover */

/* Single typeface for the whole title — Vogue display serif */
.service-6__title,
.service-6__title span {
  font-family: var(--font-display) !important;
  font-style: normal !important;
  color: #17332e;
}

/* Clipping box for the marquee. This was the title's <a> until the phantom
   links were removed (see ServicesSection.tsx) — same box, no link. */
.service-6__title .service-6__clip {
  display: block;
  max-width: 100%;
  overflow: hidden;
  white-space: nowrap;
}

/* Let the row shrink to the column it is in.

   .service-6__item is a flex container with 30px of side padding, and
   .service-6__inner is a flex item inside it — so `min-width: auto` gave the
   inner an automatic minimum equal to its MIN-CONTENT, which is wider than the
   padded box it has to live in. It therefore pushed out past the item on both
   sides of the breakpoint, for two different reasons:

     ≤767px  the row wraps and the photo alone reports ~370px, 40px more than
             the 330px content box on a 390px phone — the image ran off screen.
     768px+  the row is nowrap, and thumb (358px) + gap + title (370px) needs
             758px inside a 708px box.

   Zeroing the automatic minimum on the row and on its two children lets flex
   do what it was already asked to do. Nothing is lost by the title shrinking:
   .service-6__clip is `overflow: hidden` by design and the marquee is what
   reveals the rest of it. */
.service-6__inner,
.service-6__thumb,
.service-6__title {
  min-width: 0;
}
/* Percentage max-widths do not constrain a replaced element's min-content
   contribution, so the photo needs a real width to follow its box down. */
.service-6__thumb img {
  max-width: 100%;
}

/* On hover the item gets a black background — flip the teal title to white
   so it stays legible. */
.service-6__item:hover .service-6__title,
.service-6__item:hover .service-6__title span {
  color: #ffffff;
}

.service-6__marquee {
  display: inline-flex;
  will-change: transform;
}

.service-6__marquee-copy {
  padding-right: 60px;
  flex-shrink: 0;
  white-space: nowrap;
}

/* Run the marquee continuously while the item is hovered, stop on leave */
.service-6__item:hover .service-6__marquee {
  animation: service6-marquee 6s linear infinite;
}

@keyframes service6-marquee {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .service-6__item:hover .service-6__marquee {
    animation: none;
  }
}

/* Scroll-driven activation, mobile/tablet only — there is no hover to rely on
   there, so the item scrolled into the vertical centre of the viewport gets
   the `is-active` class (set by an IntersectionObserver in the component) and
   shows every hover state: background fill, white title, running marquee.
   Mirrors the global .service-6__item:hover rules in style.css plus the local
   title/marquee rules above. Only one item is active at a time. Desktop keeps
   the plain :hover behaviour — the observer is disabled above this width. */
@media (max-width: 991px) {
  .service-6__item.is-active::before {
    height: 100%;
    top: 0;
  }
  .service-6__item.is-active .service-6__thumb span,
  .service-6__item.is-active .service-6__dec,
  .service-6__item.is-active .service-6__title,
  .service-6__item.is-active .service-6__title span {
    color: #ffffff;
  }
  .service-6__item.is-active .service-6__dec {
    opacity: 0.7;
  }
  .service-6__item.is-active .service-6__arrow {
    border-color: #ffffff;
  }
  .service-6__item.is-active .service-6__arrow span {
    background-color: #ffffff;
  }
  .service-6__item.is-active .service-6__marquee {
    animation: service6-marquee 6s linear infinite;
  }
}

@media (max-width: 991px) and (prefers-reduced-motion: reduce) {
  .service-6__item.is-active .service-6__marquee {
    animation: none;
  }
}

/* Thumbnails: one uniform 16:9 frame per item, image cropped to fill it.
   The template sheet gives these a 300x156 pill (border-radius: 500px) with no
   object-fit, so portrait/landscape sources came out stretched and the frames
   were not all the same shape. !important because ml-theme.css loads BEFORE
   style.css — see the load-order note in functions.php. */
.service-6 .service-6__thumb img {
  display: block;
  width: 300px;
  max-width: 300px;
  height: auto;
  max-height: none !important;
  aspect-ratio: 16 / 9;
  flex-shrink: 0;
  object-fit: cover;
  object-position: center;
  border-radius: 8px !important;
}

@media (max-width: 575px) {
  .service-6 .service-6__thumb img {
    width: 100%;
    max-width: 100%;
  }
}


/* ===== components\home\TestimonialSection.css ===== */
/* Testimonials ("Reviews") overrides — scoped to .testimonial-6, never edits
   the template style.css. Aligns the big title to the right and removes the
   large bottom gap the template used to make room for the (now removed) stats. */

.testimonial-6 .testimonial-6__title {
  /* move just the big word to the right edge of its column */
  text-align: right;
  /* was 143px — that left a huge gap above the Map section below */
  padding-bottom: 0;
}

@media (max-width: 767px) {
  .testimonial-6 .testimonial-6__title { text-align: left; }
}


/* ===== components\home\VideoScroll.css ===== */
/* Mirna Luka — cinematic intro above the hero. Two modes, picked at runtime in
   VideoScroll.tsx: desktop scrubs an image sequence on canvas across a 300vh
   pinned section (sticky is emulated in JS because GSAP ScrollSmoother's
   transform breaks position:sticky/fixed); phones autoplay a plain <video> in an
   ordinary 100vh block with no pinning at all. */

.ml-vs {
  position: relative;
  width: 100%;
  height: 300vh; /* drives the scrub distance; ~2x viewport of travel while pinned */
  margin: 0;
  background: #0b1a17; /* deep teal-black; shows before frames decode */
}

/* Phone/autoplay mode: nothing is scrubbed, so the section needs no scroll
   runway — it collapses to a single viewport and the pin stops moving. */
.ml-vs--video {
  height: 100vh;
}

.ml-vs__pin {
  position: relative;
  top: 0;
  width: 100%;
  height: 100vh;
  overflow: hidden;
  will-change: transform;
}

.ml-vs--video .ml-vs__pin {
  will-change: auto; /* never transformed in this mode */
}

.ml-vs__canvas,
.ml-vs__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* matches the canvas path's cover-fit blit, so the framing is identical */
.ml-vs__video {
  object-fit: cover;
  object-position: center;
  pointer-events: none;
}

/* legibility scrim behind the overlay copy — bottom-weighted so the frame stays
   clean up top */
.ml-vs__scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to right, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.25) 42%, rgba(0, 0, 0, 0) 70%),
    linear-gradient(to top, rgba(0, 0, 0, 0.35) 0%, rgba(0, 0, 0, 0) 45%);
}

/* overlay copy is left-aligned inside the site's content container so it lines
   up with every other section (hero, residences…). Vertically centred. */
.ml-vs__overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  color: #fff;
  z-index: 2;
  will-change: opacity;
}
.ml-vs__overlay .container {
  width: 100%;
}
/* 720px, not 640: at the top of the title's clamp() "MIRNA LUKA" measures ~660px
   and used to break after "MIRNA", so the cluster read as three lines on wide
   screens and two everywhere else. With the extra 80px it is always
   "PRIMOŠTEN / MIRNA LUKA". Below 1422px the font scales with vw, so the name
   keeps shrinking faster than the column. The lead keeps its own 40ch measure. */
.ml-vs__content {
  max-width: 720px;
  text-align: left;
}

/* Sequential fade-down: each block starts slightly above + transparent, then
   drops into place. `.is-in` is added (once) when the section enters view. */
.ml-vs__reveal {
  opacity: 0;
  transform: translateY(-32px);
  will-change: opacity, transform;
}
.ml-vs__content.is-in .ml-vs__reveal {
  animation: ml-vs-fadedown 0.9s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
.ml-vs__content.is-in .ml-vs__eyebrow.ml-vs__reveal {
  animation-delay: 0.05s;
}
.ml-vs__content.is-in .ml-vs__title.ml-vs__reveal {
  animation-delay: 0.2s;
}
.ml-vs__content.is-in .ml-vs__lead.ml-vs__reveal {
  animation-delay: 0.38s;
}
.ml-vs__content.is-in .ml-vs__cta-wrap.ml-vs__reveal {
  animation-delay: 0.54s;
}

@keyframes ml-vs-fadedown {
  from {
    opacity: 0;
    transform: translateY(-32px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .ml-vs__reveal {
    opacity: 1;
    transform: none;
  }
  .ml-vs__content.is-in .ml-vs__reveal {
    animation: none;
  }
}

.ml-vs__eyebrow {
  margin: 0 0 0.9rem;
  font-size: 13px;
  letter-spacing: 3px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.85);
}

.ml-vs__title {
  font-family: var(--font-display);
  font-style: normal;
  font-weight: 400;
  color: #fff;
  margin: 0;
  line-height: 1;
  font-size: clamp(3.5rem, 9vw, 8rem);
  text-shadow: 0 2px 40px rgba(0, 0, 0, 0.35);
}

/* "Primošten Mirna Luka" is ONE cluster: the town takes the title's own size,
   face and tracking and only gets its own line. Nothing is declared here that
   would make it a second, smaller heading — the size therefore follows
   .ml-vs__title's clamp() at every width, with no second clamp to keep in step.
   The display face is caps-only, so "Primošten" and "Mirna Luka" read as one
   block without a text-transform. */
.ml-vs__place {
  display: block;
}

.ml-vs__lead {
  margin: 1.2rem 0 2rem;
  max-width: 40ch;
  font-size: clamp(1rem, 1.4vw, 1.15rem);
  line-height: 1.55;
  color: rgba(255, 255, 255, 0.92);
}

/* system primary button, forced to a black fill (default `--primary` is teal) */
.rr-btn-primary.ml-vs__cta,
.rr-btn-primary.ml-vs__cta:hover {
  background-color: #111111;
  border-color: #111111;
}

/* subtle scroll cue, only meaningful at the very start */
.ml-vs__hint {
  position: absolute;
  left: 50%;
  bottom: 2.2vh;
  transform: translateX(-50%);
  font-size: 11px;
  letter-spacing: 3px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.7);
  z-index: 2;
  animation: ml-vs-bob 2s ease-in-out infinite;
}

@keyframes ml-vs-bob {
  0%, 100% { transform: translate(-50%, 0); opacity: 0.5; }
  50% { transform: translate(-50%, 6px); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .ml-vs__hint {
    animation: none;
  }
}

@media (max-width: 767px) {
  .ml-vs__content {
    max-width: 100%;
  }
  .ml-vs__lead {
    max-width: 100%;
  }
}


/* ===== components\home\SeaviewSection.css ===== */
/* ===========================================================================
   Home — the position and the view.

   Two columns directly below the video-scroll intro: the client's three view
   photographs on the left, the argument for the plot on the right. White band,
   because home-property-types below it is the #EDECE8 one.

   Twelve-column grid with fr tracks rather than the purchased template's fixed
   pixel tracks (.about-6__inner), so the two columns cannot outgrow the
   container between breakpoints the way that section did — see the 992–1199px
   patch at the top of this file.

   The list is .rd-overview__list's hairline rows with a Phosphor check in front
   of each; the slider's bullets and arrows are scoped under .ml-seaview because
   this sheet loads BEFORE style.css and a bare .swiper-* selector would lose
   every tie to the vendor's own.
   =========================================================================== */
.ml-seaview__inner {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  align-items: center;
  column-gap: clamp(2rem, 4vw, 5rem);
}
.ml-seaview__media {
  grid-column: 1 / 8;
}
.ml-seaview__text {
  grid-column: 9 / 13;
  max-width: var(--measure-s);
}

/* --- Slider ---------------------------------------------------------------- */
/* Without JS the wrapper is a plain flex row: overflow + a full-width slide
   leave the first photograph showing and clip the other two, rather than
   spilling three of them across the grid. */
.ml-seaview__slider {
  overflow: hidden;
}
.ml-seaview__slider .swiper-slide {
  width: 100%;
  flex-shrink: 0;
}
.ml-seaview__slide img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.ml-seaview__controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-m);
  margin-top: var(--space-s);
}

/* Thin ticks, not dots — the same shape the template uses for .project-6. */
.ml-seaview .ml-seaview__dots {
  display: flex;
  align-items: center;
  gap: 8px;
  line-height: 0;
}
.ml-seaview .swiper-pagination-bullet {
  width: 22px;
  height: 2px;
  margin: 0;
  opacity: 1;
  border-radius: 0;
  background: var(--line-strong);
  transition: background 0.3s ease;
}
.ml-seaview .swiper-pagination-bullet-active {
  background: var(--accent);
}

.ml-seaview__arrows {
  display: flex;
  gap: 10px;
}
.ml-seaview__arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-circle);
  background: none;
  color: var(--text-title);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  transition: background 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
.ml-seaview__arrow.swiper-button-disabled {
  opacity: 0.35;
  cursor: default;
}

/* --- Text column ---------------------------------------------------------- */
.ml-seaview__desc {
  margin-top: var(--space-l);
}

.ml-seaview__list {
  margin: var(--space-xl) 0 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--line-strong);
}
.ml-seaview__list li {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 13px 2px;
  border-bottom: 1px solid var(--line-strong);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: var(--fw-medium);
  line-height: 1.45;
  color: var(--text-title);
}
.ml-seaview__list li:last-child {
  border-bottom: 0;
}
.ml-seaview__list i {
  flex: 0 0 auto;
  margin-top: 0.1em;
  font-size: 1.05rem;
  color: var(--accent);
}

.ml-seaview__cta {
  margin-top: var(--space-xl);
}

/* ---- Responsive ---------------------------------------------------------- */
@media (max-width: 1199px) {
  .ml-seaview__media {
    grid-column: 1 / 7;
  }
  .ml-seaview__text {
    grid-column: 7 / 13;
    max-width: none;
  }
}

@media (max-width: 991px) {
  /* Stack, photograph first — the view is the argument, the copy explains it. */
  .ml-seaview__inner {
    grid-template-columns: minmax(0, 1fr);
    row-gap: clamp(1.75rem, 6vw, 2.5rem);
  }
  .ml-seaview__media,
  .ml-seaview__text {
    grid-column: 1 / -1;
  }
  .ml-seaview__media {
    order: 0;
  }
  .ml-seaview__text {
    order: 1;
  }
  /* The line break was cut for a half-width column; full width sets its own. */
  .ml-seaview .section-title-6__wrapper .title br {
    display: none;
  }
}

@media (max-width: 767px) {
  /* 16:9 is a letterbox once the column is the whole phone. */
  .ml-seaview__slide img {
    aspect-ratio: 4 / 3;
  }
  .ml-seaview__arrow {
    width: 40px;
    height: 40px;
    font-size: 15px;
  }
  .ml-seaview__list li {
    padding: 11px 2px;
  }
}

@media (max-width: 575px) {
  /* The template stops stepping .title down at 1399px. */
  .ml-seaview .section-title-6__wrapper .title {
    font-size: 30px;
    line-height: 38px;
  }
  .ml-seaview .swiper-pagination-bullet {
    width: 16px;
  }
}

@media (hover: hover) and (pointer: fine) {
  .ml-seaview__arrow:hover {
    border-color: var(--accent);
    background: var(--accent);
    color: var(--accent-contrast);
  }
}

@media (prefers-reduced-motion: reduce) {
  .ml-seaview__arrow,
  .ml-seaview .swiper-pagination-bullet {
    transition: none;
  }
}


/* ===== components\home\WorkSection.css ===== */
/* Mirna Luka — work-6 item titles use the Vogue display serif */
.work-6__item .text .title,
.work-6__item .text .title a {
  font-family: var(--font-display) !important;
  font-style: normal !important;
  color: #17332e;
  font-size: 34px;
  line-height: 1.2;
}

/* Scroll-driven activation, every width, and the only way an item lights up —
   work-6 deliberately has no hover state (the theme's :hover rules were removed
   from style.css). The item scrolled into the vertical centre of the viewport
   gets the `is-active` class (set by an IntersectionObserver in the component),
   which reveals the image, fills the background, turns the number/title/desc
   white and shows the underline. Only one item is active at a time. */
.work-6__inner .work-6__item.is-active .media img {
  clip-path: inset(0 0 0 0);
}
.work-6__inner .work-6__item.is-active .content::before {
  width: 100%;
}
.work-6__inner .work-6__item.is-active .content .number {
  color: var(--white);
  border-color: rgba(255, 255, 255, 0.1);
}
.work-6__inner .work-6__item.is-active .content .text .title,
.work-6__inner .work-6__item.is-active .content .text .title a {
  color: var(--white);
}
.work-6__inner .work-6__item.is-active .content .text .rr-underline {
  display: inline-flex;
}
.work-6__inner .work-6__item.is-active .content .text .desc {
  color: rgba(248, 248, 248, 0.7);
}


/* ===== components\home\ResidencesGallery.css ===== */
/* Residences listing marquee — scoped to .ml-listings, never touches the template style.css.
   Full-width row of portrait listing cards (image + feature pills + floating white info
   panel) that auto-scrolls in a seamless infinite loop, pauses on hover/focus, and can be
   dragged to scrub. Light palette to match the reference. */

/* A duplicate @font-face for "Vogue ML" was here, byte-identical to the one in
   globals.css. next/font/local in layout.tsx now owns the only declaration;
   reference the face as var(--font-display). */

/* ---- Section header (matches the reference layout) ------------------------ */
.ml-residences__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 40px;
  margin-bottom: 46px;
}
.ml-residences__intro { max-width: 760px; }
.ml-residences__label {
  display: inline-block;
  font-size: var(--text-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-5);
  text-transform: uppercase;
  color: var(--text-label);
  margin-bottom: 20px;
}
.ml-residences__title {
  margin: 0 0 22px;
  font-family: var(--font-display);
  font-size: var(--text-5xl);
  line-height: var(--leading-flat);
  letter-spacing: var(--track-1);
  text-transform: uppercase;
  color: var(--text-title);
}
.ml-residences__title span { color: var(--text-title); }
.ml-residences__lead {
  margin: 0;
  font-size: var(--text-l);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
  max-width: 520px;
}
/* Layout role only — appearance comes from the shared `.ml-line` button
   (see components/common/Buttons.css). This is the LINE-button reference. */
.ml-residences__all { flex: 0 0 auto; }

@media (max-width: 767px) {
  .ml-residences__head { flex-direction: column; align-items: flex-start; gap: 24px; }
}

.ml-listings {
  /* Genuine component API — the marquee's own geometry, referenced by the
     responsive blocks below. Unlike the --ink/--muted/--panel trio that used to
     sit here, these are not colours pretending to be a design system. */
  --card-w: clamp(340px, 28vw, 440px);
  --gap: 30px;

  position: relative;
  /* break out of the parent container to true full-bleed width */
  width: 100vw;
  left: 50%;
  right: 50%;
  margin-left: -50vw;
  margin-right: -50vw;
  padding-block: 26px 8px;
  overflow: hidden;
  /* soft fade at both edges */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}

/* ---- Track (drag layer) + lane (marquee layer) --------------------------- */
.ml-listings__track {
  transform: translateX(var(--drag, 0px));
  cursor: grab;
  touch-action: pan-y; /* let the page scroll vertically on touch */
}
.ml-listings__track.is-dragging { cursor: grabbing; }

.ml-listings__lane {
  display: flex;
  gap: var(--gap);
  width: max-content;
  will-change: transform;
  animation: ml-marquee 200s linear infinite;
}
/* pause when hovering the row, focusing a card, or actively dragging */
.ml-listings:hover .ml-listings__lane,
.ml-listings:focus-within .ml-listings__lane,
.ml-listings__track.is-dragging .ml-listings__lane {
  animation-play-state: paused;
}

@keyframes ml-marquee {
  /* the lane holds two identical copies (+ a trailing gap), so shifting by half
     the lane width — one copy plus one gap — lands exactly on the next copy */
  from { transform: translateX(0); }
  to   { transform: translateX(calc(-50% - (var(--gap) / 2))); }
}

@media (prefers-reduced-motion: reduce) {
  .ml-listings__lane { animation: none; }
  .ml-listings__track { overflow-x: auto; scrollbar-width: none; }
  .ml-listings__track::-webkit-scrollbar { display: none; }
}

/* ---- Card ----------------------------------------------------------------- */
/* The card previously re-declared --ink/--muted/--panel here, duplicating the
   block in .ml-listings, with the comment "so the card renders correctly
   ANYWHERE (marquee or grid), not only inside .ml-listings". That problem is
   gone: the semantics it needs are global, so the card works in either parent
   without carrying a copy of the palette. Duplicating the trio was the symptom;
   locally-scoped colour tokens were the cause. */
.ml-listing {
  width: 100%;
}
/* Marquee-only sizing: fixed-width cards on the home page infinite row. */
.ml-listings__lane .ml-listing {
  flex: 0 0 var(--card-w);
  width: var(--card-w);
}
.ml-listing__link {
  display: block;
  position: relative;
  text-decoration: none;
  color: inherit;
  -webkit-user-drag: none;
}

.ml-listing__media {
  position: relative;
  width: 100%;
  aspect-ratio: 3 / 4;
  border-radius: 0;
  overflow: hidden;
  background: var(--bg-inset);
}
.ml-listing__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
  pointer-events: none;
}
/* Hover affordance, guarded: on a touch screen a tap fires :hover and the
   state STICKS until something else is tapped, leaving the image stuck mid-zoom.
   Guarding also states the intent — nothing here reveals content, so losing it on
   touch costs nothing. Decorative colour/opacity hovers stay unguarded; see
   src/styles/README.md. */
@media (hover: hover) and (pointer: fine) {
  .ml-listing__link:hover .ml-listing__img { transform: scale(1.05); }
}

/* ---- Tag line: kind + features, in the panel ------------------------------
   These used to be glass pills floating over the top-right of the photo. They
   now sit on one line with the kind ("APARTMENT"), separated by an oversized
   middot — the photo keeps only the sales badge, and the features read as a
   label row rather than as five competing chips.

   The type is set once on the row: kind and features are one line of the same
   uppercase label, so nothing here is per-item. Tracking is --track-2, not the
   label-standard --track-4, and the middot is tighter than it looks like it
   wants to be: uppercase "APARTMENT · PRIVATE POOL · SUN DECK" is the longest
   line a card can hold and it has to survive the 268px panel of the three-up
   grid. Measured at 1440px: 10px to spare. Below that the panel narrows faster
   than the grid drops a column, so the longest lines wrap — which is why the
   separator trails its tag (see below) instead of leading the next one. */
.ml-listing__tags {
  margin: 0 0 8px;
  font-size: var(--text-2xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--track-2);
  line-height: var(--leading-snug);
  text-transform: uppercase;
  color: var(--text-label);
}
/* Inline flow, not flex, and the separator TRAILS its item: a wrapped line then
   starts with a word instead of a stranded dot (flex + ::before did exactly
   that on the narrow marquee cards). */
.ml-listing__tags > * {
  display: inline;
}
.ml-listing__tags > *:not(:last-child)::after {
  content: "·";
  margin: 0 4px 0 5px;
  font-size: 1.6em;
  line-height: 0;
  vertical-align: -0.08em;
  color: var(--text-faint);
}
/* Only thing a tag needs of its own: two-word tags ("Rooftop pool") break as a
   unit, never across the middot. */
.ml-listing__pill,
.ml-listing__kicker {
  white-space: nowrap;
}

/* ---- Sales status (top-left of the card image) ----------------------------
   Left, opposite the feature pills, and squared rather than pill-shaped: this
   is a fact about the residence, not one of its features. */
.ml-listing__status {
  position: absolute;
  top: 16px;
  left: 16px;
  z-index: 1;
  padding: 7px 13px;
  border-radius: var(--radius-xs);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--fw-medium);
  line-height: var(--leading-none);
  letter-spacing: var(--track-3);
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--text-title);
  background: rgb(var(--bg-rgb) / 0.82);
  border: 1px solid rgb(var(--bg-rgb) / 0.6);
  backdrop-filter: blur(10px) saturate(1.1);
  -webkit-backdrop-filter: blur(10px) saturate(1.1);
  box-shadow: 0 2px 8px var(--fg-10);
}
/* Sold is the state a buyer must not miss, so it stops being transparent. */
.ml-listing__status--sold {
  color: var(--text-inverse);
  background: var(--ml-teal-700);
  border-color: var(--ml-teal-700);
}
/* The whole card steps back once it is sold — still browsable, plainly gone. */
.ml-listing--sold .ml-listing__img {
  filter: saturate(0.55) brightness(0.94);
}
@media (hover: hover) and (pointer: fine) {
  .ml-listing--sold .ml-listing__link:hover .ml-listing__img {
    filter: saturate(0.75) brightness(0.97);
  }
}

/* Screen-reader-only text. Named ml-* so it cannot collide with the vendor's
   own utility classes. */
.ml-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---- Floating white info panel -------------------------------------------- */
.ml-listing__panel {
  position: relative;
  z-index: 2;
  margin: -74px 18px 0;   /* overlap up onto the lower image */
  padding: 22px 24px 24px;
  background: var(--bg-surface);
  border-radius: var(--radius-xl);
}

.ml-listing__title {
  margin: 0 0 10px;
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  line-height: var(--leading-flat);
  letter-spacing: var(--track-1);
  text-transform: uppercase;
  color: var(--text-title);
}

.ml-listing__desc {
  margin: 0 0 18px;
  font-size: var(--text-m);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.ml-listing__rule {
  display: block;
  height: 1px;
  margin-bottom: 16px;
  background: var(--line-strong);
}

/* wrap, not nowrap: the three items are individually white-space:nowrap, so a
   nowrap row makes their combined width (~341px) the card's hard minimum —
   more than the panel gets in ANY layout the card ships in, phone or desktop,
   so the last item was being clipped by the panel edge and, on a phone, the
   card pushed its whole grid column off-screen. Wrapping costs a second line
   only where the row genuinely does not fit. */
.ml-listing__meta {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 10px 16px;
}
.ml-listing__meta li {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-s);
  font-weight: var(--fw-medium);
  color: var(--text-title);
  white-space: nowrap;
}
.ml-listing__meta li i {
  font-size: var(--text-l);
  color: var(--text-label);
}

/* ---- Responsive ----------------------------------------------------------- */
@media (max-width: 991px) {
  .ml-listings { --card-w: clamp(320px, 62vw, 400px); --gap: 22px; }
}
/* Phones: drop the full-bleed break-out so the row sits inside the same container
   (15px inset) as the section header and every other section, instead of running
   edge-to-edge past them. The lane still overflows the box and is clipped, so the
   marquee keeps scrolling — the next card just peeks in from the right. */
@media (max-width: 767px) {
  .ml-listings {
    width: auto;
    left: auto;
    right: auto;
    margin-left: 0;
    margin-right: 0;
    -webkit-mask-image: linear-gradient(90deg, #000 90%, transparent);
    mask-image: linear-gradient(90deg, #000 90%, transparent);
  }
}
@media (max-width: 575px) {
  .ml-listings { --card-w: 80vw; --gap: 16px; }
  .ml-listing__panel { margin: -64px 14px 0; padding: 20px; }
  .ml-listing__desc { font-size: var(--text-s); }
}

/* ===========================================================================
   /residences filter layout — left checkbox sidebar + toolbar + a 2-column grid
   of the SAME listing card as the home page. Scoped to .ml-res-* / a compound
   selector so no global template CSS is touched.
   =========================================================================== */
.ml-res-layout {
  display: grid;
  grid-template-columns: 250px minmax(0, 1fr);
  gap: 48px;
  margin-top: 50px;
}

/* Optional standfirst under the listing heading — the building pages pass their
   own description here, /residences passes none. Sits above .ml-res-layout, so
   it takes that block's top margin off when present. */
.ml-res-lead {
  max-width: 720px;
  margin: 0;
  font-size: var(--text-m);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
}
/* General sibling, not an adjacent chain: the scrim sits between these two in
   the markup but ml-enhance.js moves it to <body> the first time the phone
   panel opens, which would break `lead + scrim + layout` mid-session. */
.ml-res-lead ~ .ml-res-layout { margin-top: 34px; }
@media (max-width: 767px) {
  .ml-res-lead { font-size: var(--text-s); }
  .ml-res-lead ~ .ml-res-layout { margin-top: 22px; }
}

/* ---- Sidebar -------------------------------------------------------------- */
.ml-res-filters {
  align-self: start;
  position: sticky;
  top: 110px;
}

/* The off-canvas chrome. All of it is inert above 767px — the sidebar is a
   plain sticky column there and these three pieces simply do not render. The
   panel only goes `position: fixed` on phones, and that is not a stylistic
   choice: the template instantiates ScrollSmoother only when
   `window.screen.width > 767`, and a smoothed #smooth-content carries a
   transform, which would make IT the containing block for `fixed` instead of
   the viewport. Below 768px there is no smoother, so `fixed` means fixed. */
.ml-res-scrim,
.ml-res-filters__close,
.ml-res-filters__foot {
  display: none;
}

/* Toggle sits to the right of the section heading, phones only. */
.ml-res-toggle {
  display: none;
  align-items: center;
  gap: 9px;
  padding: 10px 17px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  background: none;
  color: var(--text-title);
  font-family: var(--font-display);
  font-size: var(--text-m);
  letter-spacing: var(--track-4);
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
}
.ml-res-toggle i {
  font-size: var(--text-l);
  line-height: var(--leading-none);
}
.ml-res-toggle__count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 19px;
  height: 19px;
  padding: 0 5px;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--bg-surface);
  font-family: var(--font-sans);
  font-size: var(--text-2xs);
  font-weight: var(--fw-semibold);
  letter-spacing: 0;
  line-height: var(--leading-none);
}
.ml-res-filters__title {
  margin: 0 0 22px;
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  letter-spacing: var(--track-1);
  text-transform: uppercase;
  color: var(--text-title);
}
.ml-res-group {
  padding: 18px 0;
  border-top: 1px solid var(--line-strong);
}
.ml-res-group:last-child { border-bottom: 1px solid var(--line-strong); }
/* --text-BODY, not --text-label, and the reason generalises: --text-label is
   calibrated to clear WCAG AA against the PAGE surface (4.55:1 on white). This
   sidebar sits on the template's tinted #EDECE8 band, where the same token falls
   to 3.85:1 — and this is a filter control label, which a reader has to read in
   order to operate the filter. --text-body measures 4.73:1 there.
   Rule of thumb: on a tinted band, step a label up one rung. */
.ml-res-group__label {
  display: block;
  margin-bottom: 14px;
  font-family: var(--font-display);
  font-size: var(--text-m);
  letter-spacing: var(--track-4);
  text-transform: uppercase;
  color: var(--text-body);
}
.ml-res-check {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 7px 0;
  cursor: pointer;
  font-size: var(--text-base);
  color: var(--text-title);
}
.ml-res-check input {
  width: 18px;
  height: 18px;
  accent-color: var(--accent);
  cursor: pointer;
  flex: 0 0 auto;
}
.ml-res-check:hover { color: var(--text-title); }

/* ---- Toolbar (chips + sort) ----------------------------------------------- */
.ml-res-toolbar {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
}
/* only take up space once there are active-filter chips to show */
.ml-res-toolbar:has(.ml-res-chips:empty) {
  margin-bottom: 0;
}
.ml-res-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 26px;
}
.ml-res-chips:empty {
  margin-bottom: 0;
}
.ml-res-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border: 0;
  border-radius: 0;
  background: var(--fg-10);
  color: var(--text-title);
  font-size: var(--text-s);
  font-weight: var(--fw-medium);
  cursor: pointer;
  transition: background 0.25s ease;
}
.ml-res-chip:hover { background: var(--fg-15); }
.ml-res-chip span { font-size: var(--text-base); line-height: var(--leading-none); opacity: 0.7; }

/* ---- Grid (compound selector outranks the template's default 3-col wrap) --- */
.works-section-10__wrap.ml-residences-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 44px 28px;
}
.ml-residences-grid .works-section-10__item {
  width: 100%;
  margin: 0;
  padding: 0;
  float: none;
  opacity: 1;
  transform: none;
}
/* filter hides non-matching cells (toggled by ml-enhance.js) */
.ml-residences-grid .works-section-10__item.is-hidden {
  display: none;
}

@media (max-width: 1199px) {
  .ml-res-layout { grid-template-columns: 220px minmax(0, 1fr); gap: 38px; }
  .works-section-10__wrap.ml-residences-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 991px) {
  /* minmax(0,…), not a bare 1fr: a bare `1fr` is `minmax(auto, 1fr)` and lets
     a wide child stretch the column past the container instead of being
     clamped by it. */
  .ml-res-layout { grid-template-columns: minmax(0, 1fr); gap: 34px; }
  .ml-res-filters { position: static; top: auto; }
  .ml-res-group { padding: 14px 0; }
  /* sidebar now full-width on top → main can hold 3 columns again */
  .works-section-10__wrap.ml-residences-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 767px) {
  .works-section-10__wrap.ml-residences-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 26px; }

  /* ---- Sidebar → off-canvas ---------------------------------------------- */
  .ml-res-toggle { display: inline-flex; }
  /* Bottom-align the pill: level with the LAST line of the two-line display
     heading, which sits closer to the cards it acts on and reads as part of
     the toolbar rather than floating up by the header. */
  .ml-res-section .section-title__wrap.project { align-items: flex-end; }

  .ml-res-scrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: calc(var(--z-offcanvas) - 1);
    background: rgb(var(--shadow-rgb) / 0.45);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.3s ease, visibility 0s linear 0.3s;
  }
  .ml-res-scrim.is-open {
    opacity: 1;
    visibility: visible;
    transition: opacity 0.3s ease, visibility 0s;
  }

  /* Taken out of the grid entirely, so .ml-res-main is the only in-flow item
     and the cards start straight under the heading. */
  .ml-res-filters {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: var(--z-offcanvas);
    display: flex;
    flex-direction: column;
    width: min(86vw, 380px);
    background: var(--bg-surface);
    box-shadow: var(--shadow-l);
    transform: translateX(100%);
    visibility: hidden;
    transition: transform 0.38s cubic-bezier(0.22, 1, 0.36, 1), visibility 0s linear 0.38s;
  }
  .ml-res-filters.is-open {
    transform: none;
    visibility: visible;
    transition: transform 0.38s cubic-bezier(0.22, 1, 0.36, 1), visibility 0s;
  }

  .ml-res-filters__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex: 0 0 auto;
    padding: 18px 20px;
    border-bottom: 1px solid var(--line-strong);
  }
  .ml-res-filters__title { margin: 0; }
  .ml-res-filters__close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    margin-right: -8px;   /* optical: pull the glyph to the panel's text edge */
    padding: 0;
    border: 0;
    background: none;
    color: var(--text-title);
    font-size: var(--text-xl);
    cursor: pointer;
  }

  .ml-res-filters__body {
    flex: 1 1 auto;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    padding: 0 20px 20px;
  }
  /* the head already draws a line right above it */
  .ml-res-filters__body .ml-res-group:first-child { border-top: 0; }

  .ml-res-filters__foot {
    display: flex;
    align-items: center;
    gap: 14px;
    flex: 0 0 auto;
    padding: 14px 20px;
    padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--line-strong);
  }
  .ml-res-clear {
    padding: 0;
    border: 0;
    background: none;
    color: var(--text-body);
    font-size: var(--text-s);
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
  }
  .ml-res-done {
    flex: 1 1 auto;
    padding: 14px 18px;
    border: 0;
    border-radius: 0;
    background: var(--text-title);
    color: var(--bg-surface);
    font-family: var(--font-display);
    font-size: var(--text-m);
    letter-spacing: var(--track-4);
    text-transform: uppercase;
    cursor: pointer;
  }

  /* Scroll lock while the panel is open. Safe as a plain overflow:hidden
     because ScrollSmoother — which would need its own pause() — is never
     running at this width. */
  .ml-res-lock,
  .ml-res-lock body { overflow: hidden; }
}

@media (prefers-reduced-motion: reduce) {
  .ml-res-filters,
  .ml-res-filters.is-open,
  .ml-res-scrim,
  .ml-res-scrim.is-open { transition: none; }
}
@media (max-width: 575px) {
  .works-section-10__wrap.ml-residences-grid { grid-template-columns: minmax(0, 1fr); gap: 30px; }
  /* Heading and the filters it labels were 90px apart on a phone (40px of
     section padding + the 50px layout margin), far enough that they read as
     two unrelated blocks. Halve it — the section above still has its own
     40px of top padding, so the band keeps its air. */
  .ml-res-section .section-title__wrap.project { padding-bottom: 22px; }
  .ml-res-layout { margin-top: 28px; }

  /* Clear the fixed header when the listing is the first thing on the page.

     The block was written to sit BELOW a page-title-area, so its own top
     spacing (60px section + 40px on the title wrap = 100px) never had to
     account for the header. Once /residences and both building pages dropped
     their title area the listing became main's first child, and at this
     breakpoint the header is 104px tall — so the heading started 4px UNDER it
     and read as if it were stuck to the bar.

     110px puts the heading at y=150, which is exactly where /contact, /explore
     and /about put theirs: 46px of clear air below the header. Restricted to
     the first-child case so re-enabling a title area hands the spacing back to
     it instead of leaving a double gap. */
  main > .ml-res-section:first-child { padding-top: 110px; }
}


/* ===== components\home\MapIntro.css ===== */
/* Transition band: white background, black text. The per-character "Location"
   reveal is driven from MapIntro.tsx (rAF + rect), not GSAP — the template's
   ScrollSmoother makes a second GSAP instance / sticky unreliable. This file
   only holds the static look. */

.ml-mi {
  position: relative;
  /* was 100vh — that left a full viewport of white between Testimonials and the
     centred word. A shorter band brings the two sections closer. */
  min-height: 56vh;
  background-color: #ffffff;
  overflow: hidden;
}

/* the word sits centred in the band as it passes through */
.ml-mi__stage {
  position: sticky; /* falls back to static under ScrollSmoother — fine, it just
                       scrolls with the band; the reveal is scroll-progress based */
  top: 0;
  min-height: 56vh;
  display: grid;
  place-items: center;
  padding: 6vh 24px;
}

/* centred column holding the word + the lead paragraph */
.ml-mi__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  max-width: 900px;
}

.ml-mi__word {
  margin: 0;
  display: flex;
  flex-wrap: nowrap;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(64px, 16vw, 260px);
  line-height: 1;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #111111; /* black text on the white band */
  user-select: none;
}

.ml-mi__char {
  display: inline-block;
  transform-origin: 50% 0%;
  opacity: 0;
  will-change: opacity, transform;
}

/* ---- Lead paragraph (centred, black on white) ----------------------------- */
.ml-mi__lead {
  /* more air below the big word so the paragraph doesn't crowd it */
  margin: clamp(32px, 5vw, 64px) 0 0;
  max-width: 760px;
  text-align: center;
  font-family: var(--font_bai), sans-serif;
  font-size: clamp(18px, 1.9vw, 26px);
  line-height: 1.7;
  letter-spacing: 0.01em;
  color: #1a1a1a;
}

@media (max-width: 767px) {
  /* shrink the big word so all 8 characters fit the viewport (no overflow/clip)
     and allow it to wrap if a very narrow screen still can't fit it on one line */
  .ml-mi { min-height: auto; }
  .ml-mi__stage { min-height: auto; padding: 12vh 20px; }
  .ml-mi__word {
    flex-wrap: wrap;
    justify-content: center;
    font-size: clamp(44px, 13vw, 92px);
    letter-spacing: 0.02em;
  }
  .ml-mi__lead { margin-top: clamp(24px, 6vw, 40px); }
}

@media (prefers-reduced-motion: reduce) {
  .ml-mi__char { will-change: auto; }
}


/* ===== components\home\MapSection.css ===== */
/* Map Location section — Leaflet map + custom pins + brand popup cards.
   Scoped to .ml-map / .ml-pin / .ml-poi-popup; never touches template style.css.

   THREE contexts, each an island rather than a hand-mixed palette:

     .ml-map          the dark map frame          -> .theme-always-dark
     .ml-pin          light teardrop on the map   -> .theme-always-light
     .ml-poi-popup    light card on the map       -> .theme-always-light

   The nested light-inside-dark shape is why the theme groups in
   src/styles/01-theme.css carry a BARE `.theme-always-light` selector: the
   original pxpace selector set only anticipated nesting under :root, so a light
   chip inside a dark island silently inherited the dark values.

   A local `--ink/--muted/--line/--accent/--gold` block used to sit here, plus a
   full section-header layout (.ml-map__head/__aside/__label/__title/__lead).
   The header was DEAD — no component has ever rendered that markup; both
   MapSection.tsx and ExploreMap.tsx use .ml-map only as the map frame — so it
   was deleted rather than migrated. */

/* ---- Scroll-expand shell -------------------------------------------------- */
.ml-map-expand {
  position: relative;
  /* continues the white MapIntro until the card expands */
  background: var(--bg-surface);
}
.ml-map-expand__stage {
  height: 100vh;
  display: grid;
  place-items: center;
  overflow: hidden; /* clip the growing frame while it's pinned */
  background: var(--bg-surface);
}

/* the animated frame — width / height / border-radius are driven by GSAP.
   Initial values below (container width, 62vh) also render pre-JS with no flash. */
.ml-map__frame {
  position: relative;
  width: min(var(--width-site), 100vw);
  height: 76vh;
  /* GSAP tweens this inline (MapSection.tsx) and inline always wins, so the
     tween READS this token rather than repeating the number. The value here is
     still what renders pre-JS and under prefers-reduced-motion. */
  border-radius: var(--radius-xl);
  overflow: hidden;
  background: var(--bg-body);
  box-shadow: var(--shadow-xl);
  will-change: width, height;
}

/* the map itself stays a fixed 100vw × 100vh, centred on the viewport; the frame
   simply reveals more of it as it grows — so Leaflet never needs to resize. */
.ml-map__canvas {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  background: var(--bg-body); /* near-black — shows through while tiles load */
}
.ml-map__canvas--loading {
  background:
    linear-gradient(100deg, var(--bg-body) 30%, var(--bg-surface-2) 50%, var(--bg-body) 70%)
    var(--bg-body);
  background-size: 200% 100%;
  animation: ml-map-shimmer 1.4s ease-in-out infinite;
}
@keyframes ml-map-shimmer {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}
.ml-map__leaflet {
  width: 100%;
  height: 100%;
  background: var(--bg-body); /* Leaflet's own container bg behind dark tiles */
}
/* Airbnb-style zoom control: stacked rounded pill, bottom-right */
.ml-map__leaflet .leaflet-control-zoom {
  margin: 0 18px 26px 0;
  border: 0;
  border-radius: 14px;
  overflow: hidden;
  box-shadow: 0 6px 20px var(--scrim-50);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
.ml-map__leaflet .leaflet-control-zoom a {
  width: 42px;
  height: 42px;
  /* Not typographic leading — this is the old vertical-centring trick, matched to
     the 42px box above. A --leading-* token here would break the centring. */
  line-height: 42px;
  font-size: var(--text-2xl);
  font-weight: 400;
  color: var(--text-title);
  background: rgb(var(--bg-rgb) / 0.82);
  border: 0;
  border-bottom: 1px solid var(--fg-10);
  transition: background 0.2s ease, color 0.2s ease;
}
.ml-map__leaflet .leaflet-control-zoom a:last-child { border-bottom: 0; }
.ml-map__leaflet .leaflet-control-zoom a:hover {
  background: rgb(var(--bg-rgb) / 0.95);
  color: var(--gold);
}
/* Selector doubled on .leaflet-container — which IS this same element — to reach
   (0,3,0). Leaflet ships `.leaflet-container .leaflet-control-attribution
   { background: rgba(255,255,255,.8) }` at (0,2,0), and leaflet.css arrives via
   MapCanvas's dynamic import, so it loads AFTER this file and wins any tie. The
   attribution therefore sat on a WHITE strip all along; that went unnoticed while
   the text was mid-sage, and would have become white-on-white the moment the
   colour started resolving from the dark island. */
.ml-map__leaflet.leaflet-container .leaflet-control-attribution {
  font-size: var(--text-3xs);
  background: rgb(var(--bg-rgb) / 0.7);
  color: var(--text-muted);
}
.ml-map__leaflet.leaflet-container .leaflet-control-attribution a {
  color: var(--text-body);
}

/* ---- "Hold Ctrl and scroll to zoom" hint ---------------------------------- */
.ml-map__hint {
  position: absolute;
  z-index: var(--z-map-popup); /* above tiles/markers, below popups */
  inset: 0;
  margin: auto;
  width: max-content;
  height: max-content;
  padding: 14px 26px;
  border-radius: var(--radius-pill);
  background: rgb(var(--bg-rgb) / 0.72);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  color: var(--text-title);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: var(--fw-medium);
  letter-spacing: var(--track-1);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.25s ease;
}
.ml-map__hint.is-visible { opacity: 1; }

/* ---- Custom "show region" controls ---------------------------------------- */
.ml-map__ctrls {
  position: absolute;
  z-index: var(--z-map-controls); /* above tiles/markers, below Leaflet popups */
  left: 14px;
  bottom: 22px;
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}
.ml-map__ctrl {
  padding: 9px 15px;
  border: 1px solid var(--fg-15);
  border-radius: var(--radius-pill);
  background: rgb(var(--bg-rgb) / 0.78);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  color: var(--text-title);
  font-size: var(--text-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-2);
  cursor: pointer;
  box-shadow: 0 4px 16px var(--scrim-50);
  transition: background 0.25s ease, transform 0.2s ease, border-color 0.25s ease;
}
.ml-map__ctrl:hover {
  background: rgb(var(--bg-rgb) / 0.92);
  border-color: var(--gold);
  transform: translateY(-1px);
}

/* ---- Pins (L.divIcon) ----------------------------------------------------- */
.ml-pin { background: none; border: 0; }
.ml-pin__body {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  border-radius: 50% 50% 50% 0;
  transform-origin: center;
  rotate: -45deg;
  background: var(--bg-surface);
  color: var(--text-title);
  border: 1.5px solid var(--text-title);
  box-shadow: 0 6px 16px -6px var(--fg-50);
  transition: transform 0.2s ease;
}
.ml-pin__glyph {
  width: 20px;
  height: 20px;
  rotate: 45deg; /* counter-rotate the glyph upright */
}
/* the small drop shadow "stem" under the teardrop tip */
.ml-pin__stem {
  position: absolute;
  bottom: 2px;
  left: 50%;
  width: 8px;
  height: 4px;
  transform: translateX(-50%);
  background: var(--fg-20);
  border-radius: 50%;
  filter: blur(1px);
}
/* Hover affordance, guarded: on a touch screen a tap fires :hover and the
   state STICKS until something else is tapped, leaving the image stuck mid-zoom.
   Guarding also states the intent — nothing here reveals content, so losing it on
   touch costs nothing. Decorative colour/opacity hovers stay unguarded; see
   src/styles/README.md. */
@media (hover: hover) and (pointer: fine) {
  .ml-pin:hover .ml-pin__body { transform: translateX(-50%) scale(1.08); }
}

/* centre marker (the development) — filled gold, larger, glows on the dark map */
.ml-pin--center .ml-pin__body {
  width: 54px;
  height: 54px;
  background: var(--gold);
  color: var(--gold-contrast);
  /* --bg-*, not --fg-*: this is the white halo that lifts the marker off the
     dark map. The pin is a .theme-always-light island, so its FOREGROUND is the
     dark teal — a --fg-* here would draw a dark ring and lose the halo. */
  border-color: var(--bg-80);
  box-shadow: 0 0 0 6px rgb(var(--gold-rgb) / 0.18),
    0 10px 24px -6px var(--scrim-70);
}
.ml-pin--center .ml-pin__glyph { width: 24px; height: 24px; }

/* featured tier (famous destinations) — warm gold pin so they stand out */
.ml-pin--featured .ml-pin__body {
  background: var(--gold-surface);
  color: var(--gold-ink);
  border-color: var(--gold);
  box-shadow: 0 0 0 4px rgb(var(--gold-rgb) / 0.16),
    0 6px 16px -6px var(--scrim-70);
}
.ml-pin--featured:hover .ml-pin__body {
  box-shadow: 0 0 0 6px rgb(var(--gold-rgb) / 0.24),
    0 8px 18px -6px var(--scrim-70);
}
/* the centre marker keeps its own (filled gold) styling — don't let the
   featured rule above touch it */
.ml-pin--center.ml-pin--featured .ml-pin__body { background: var(--gold); color: var(--gold-contrast); }

/* ---- Popup card ----------------------------------------------------------- */
.ml-poi-popup .leaflet-popup-content-wrapper {
  border-radius: var(--radius-l);
  padding: 4px 6px;
  box-shadow: 0 18px 44px -18px var(--fg-50);
  border: 1px solid var(--line-strong);
}
.ml-poi-popup .leaflet-popup-content {
  margin: 16px 18px;
  min-width: 190px;
}
.ml-poi-popup .leaflet-popup-tip { box-shadow: none; }
.ml-poi-popup a.leaflet-popup-close-button {
  color: var(--text-faint);
  padding: 8px 8px 0 0;
}
.ml-poi-popup a.leaflet-popup-close-button:hover { color: var(--text-title); }

.ml-poi-popup__kicker {
  display: block;
  font-family: var(--font-sans);
  font-size: var(--text-3xs);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-4);
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0 0 6px;
}
.ml-poi-popup__title {
  margin: 0 0 10px;
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  line-height: var(--leading-display);
  letter-spacing: var(--track-1);
  text-transform: uppercase;
  color: var(--text-title);
}
.ml-poi-popup__chip {
  display: inline-block;
  margin: 0 0 10px;
  padding: 4px 11px;
  border-radius: var(--radius-pill);
  background: var(--bg-inset);
  color: var(--text-title);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--fw-semibold);
}
.ml-poi-popup__desc {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-s);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
}

/* ---- Responsive ----------------------------------------------------------- */
@media (max-width: 991px) {
  .ml-map__head {
    grid-template-columns: 1fr;
    align-items: start;
    gap: 24px;
  }
  .ml-map__aside { padding-bottom: 0; }
  .ml-map__lead { max-width: 640px; }
}

@media (max-width: 767px) {
  .ml-map__ctrls { left: 10px; bottom: 14px; }
  .ml-map__ctrl { padding: 8px 12px; font-size: var(--text-xs); }

  /* Static, non-pinned map on mobile (JS bails out on ≤767px). The stage is no
     longer a 100vh pin target — collapse it to a plain block that just holds the
     card, and give the frame a fixed, self-contained size. dvh tracks the real
     visible height so the address bar can't leave a gap. */
  .ml-map-expand__stage {
    height: auto;
    padding: 0;
    overflow: visible;
  }
  .ml-map__frame {
    width: 100%;
    height: 68dvh;
    min-height: 420px;
    border-radius: 0;
    box-shadow: none;
    will-change: auto;
  }
  /* the fixed-size (100vw × 100vh) canvas is fine — it's clipped by the frame */
}


/* ===== components\home\ExploreMap.css ===== */
/* Explore-page Location Map — full-width, fixed 90vh, no scroll animation.
   Reuses .ml-map / MapCanvas styling; overrides only the frame + canvas box so
   the map fills the viewport width and a static 90vh height. */

.ml-explore-map {
  position: relative;
  background: var(--bg-body);
}

.ml-explore-map__frame {
  position: relative;
  width: 100vw;
  margin-left: calc(50% - 50vw); /* break out of any centred container */
  height: 90vh;
  overflow: hidden;
  background: var(--bg-body);
}

/* the map itself fills the frame directly (no oversized centred canvas trick,
   since there's no expand animation to reveal) */
.ml-explore-map__canvas {
  position: absolute;
  inset: 0;
  top: 0;
  left: 0;
  transform: none;
  width: 100%;
  height: 100%;
}


/* ===== components\shell\Loader.css ===== */
/* The opener reads "Primošten / Mirna Luka" on two lines (header.php). The town
   is set at 0.45em of the loader's own font-size, so it follows the three
   breakpoints in style.css (100 / 50 / 40px) without repeating them.

   Everything is set on the LINE, never on .load-text: style.css already owns
   `.loader-wrap .loader-wrap-heading .load-text` with three classes, and
   ml-theme.css loads BEFORE it — it wins on specificity only, never on source
   order, so a rule with the same weight there would lose. On the line there is
   nothing to fight, and `.load-text .load-text__line` also outweighs
   `.load-text span` (two classes beat one class + one element), which is what
   keeps the per-letter opacity fade off the wrappers.

   line-height is the fix for the collision: the template leaves the loader at
   the body's flat 20px, which was invisible while the opener was one line and
   turned into 100px glyphs crashing into a 20px line box the moment it became
   two. Both lines are centred — the wrapper is a flex item as wide as
   "MIRNA LUKA", so the shorter town line would otherwise hang off to the left. */
.load-text .load-text__line {
  display: block;
  animation: none;
  white-space: nowrap;
  line-height: 1.06;
  text-align: center;
}
.load-text .load-text__line--place {
  font-size: 0.45em;
  letter-spacing: 0.08em;
  /* Tracking adds a trailing gap after the last letter, which pushes a centred
     line visually left by half of it; pull back exactly that half. */
  margin-right: -0.04em;
  margin-bottom: 0.08em;
}

/* The per-letter fade sweeps LEFT TO RIGHT across both lines.

   style.css staggers `.load-text span` with nth-child(1)…(7) only — 0 to 0.6s in
   0.1s steps — and leaves every later letter at 0s. With one ten-letter line
   that already meant letters 8-10 blinked in time with letter 1; once the opener
   became two lines, nth-child restarted per line and the pulse looked like it
   was spreading out of the middle. So the delays are restated here for every
   position either line can hold, 0.07s apart, and both lines run the same ramp:
   the shimmer crosses the block as one vertical wipe moving right.

   `.load-text .load-text__line span:nth-child(n)` carries three classes against
   style.css's one, so it wins from ml-theme.css's earlier position in the chain
   — the same rule that lets everything else in this block work. */
.load-text .load-text__line span:nth-child(1)  { animation-delay: 0s; }
.load-text .load-text__line span:nth-child(2)  { animation-delay: 0.07s; }
.load-text .load-text__line span:nth-child(3)  { animation-delay: 0.14s; }
.load-text .load-text__line span:nth-child(4)  { animation-delay: 0.21s; }
.load-text .load-text__line span:nth-child(5)  { animation-delay: 0.28s; }
.load-text .load-text__line span:nth-child(6)  { animation-delay: 0.35s; }
.load-text .load-text__line span:nth-child(7)  { animation-delay: 0.42s; }
.load-text .load-text__line span:nth-child(8)  { animation-delay: 0.49s; }
.load-text .load-text__line span:nth-child(9)  { animation-delay: 0.56s; }
.load-text .load-text__line span:nth-child(10) { animation-delay: 0.63s; }


/* ===== components\shell\Header.css ===== */
/* Auto-hide sticky header.

   The header is rendered OUTSIDE #smooth-wrapper (see TemplateShell) because
   GSAP ScrollSmoother transforms #smooth-content, and position:fixed inside a
   transformed ancestor resolves against that ancestor instead of the viewport —
   the same constraint documented in MapIntro.tsx / VideoScroll.tsx. Out there it
   is a plain fixed bar, so it carries its own z-index: 800 is the template's own
   sticky-header layer — above page content, below the offcanvas overlay (900)
   and the side panel (9999).

   The template ships .header-sticky/.sticky/.transformed for exactly this
   behaviour, but its handler is dead code (common.js declares pinned_header()
   twice, so the hoisted second declaration wins) and .sticky forces a white bar
   while this header is solid black. Hence our own classes, toggled in Header.tsx.

   Selectors are doubled up (.header-area-2.ml-sticky-header) so they outrank the
   template's `.header-area-2 { position: absolute }` regardless of whether
   Next's bundled CSS lands before or after the <link> to style.css. */
.header-area-2.ml-sticky-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  width: 100%;
  z-index: 800;
  transform: translateY(0);
  transition: transform 0.4s cubic-bezier(0.22, 0.61, 0.36, 1),
    box-shadow 0.4s ease;
  will-change: transform;
}

/* scrolling down past the top zone — slide the whole bar off the top edge */
.header-area-2.ml-sticky-header.is-hidden {
  transform: translateY(-100%);
}

/* revealed mid-page (scrolling up) — lift it off the content it now covers */
.header-area-2.ml-sticky-header.is-stuck {
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
}

@media (prefers-reduced-motion: reduce) {
  .header-area-2.ml-sticky-header {
    transition: none;
  }
}


/* ===== components\shell\ResidencesMega.css ===== */
/* ============================================================
   Residences mega menu — tabbed, image-card layout.
   Left column = Apartments P1 / Apartments P2 / Villas tabs;
   right panel = the selected area's units as image cards
   (thumbnail on top, title below).

   Reuses the template's dark .mega-menu shell (position, hover
   reveal, black background). Overrides the base grid + link
   styling, so selectors carry the extra .ml-mega / card classes
   to beat `.main-menu .mega-menu li a` and the ML colour rules.
   ============================================================ */

/* Panel shell: replace the 4-column grid with a full-width block */
.header-area-2 .ml-desktop-menu .mega-menu.ml-mega {
  display: block;
  grid-template-columns: none;
  column-gap: 0;
  padding: 30px 40px;
  border-radius: 0 0 10px 10px;
}

.ml-mega__layout {
  display: grid;
  grid-template-columns: 250px 1fr;
  gap: 44px;
  align-items: stretch;
  list-style: none;
}

/* ---- Left column: tabs ---- */
.ml-mega__tabs {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-right: 28px;
  border-right: 1px solid #2a2a2e;
}
.ml-mega__tab {
  appearance: none;
  -webkit-appearance: none;
  font-family: inherit;
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: 0;
  padding: 15px 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  text-align: left;
  color: #cfcfcf;
  font-size: 16px;
  font-weight: 500;
  line-height: 1.2;
  transition: background 0.2s ease, color 0.2s ease;
}
.ml-mega__tab:hover {
  color: #ffffff;
  background: #232326;
}
.ml-mega__tab.is-active {
  color: #ffffff;
  background: #2c2c2f;
}
.ml-mega__tab-count {
  font-size: 12px;
  font-weight: 600;
  color: #8a8a8a;
}
.ml-mega__tab.is-active .ml-mega__tab-count {
  color: var(--secondary);
}

/* ---- Right column: content ---- */
.ml-mega__content {
  display: flex;
  flex-direction: column;
}
/* One wrapper per tab (the React version rendered only the active panel, so it
   had no equivalent). It has to be the flex column itself: `.ml-mega__all`
   below relies on `align-self` to sit at the right edge, and that only works if
   its direct parent is the flex container. Kept as a `display` the JS can
   restore — ml-header.js toggles panels with `style.display = ""` / "none". */
.ml-mega__panel-wrap {
  display: flex;
  flex-direction: column;
}
.ml-mega__panel {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 22px 18px;
}

/* Image card — override the base `.mega-menu li a` chip styling */
.header-area-2 .ml-desktop-menu .mega-menu.ml-mega .ml-mega__card {
  display: block;
  height: auto;
  padding: 0;
  background: transparent;
  border-radius: 0;
  color: #e6e6e6;
  overflow: visible;
}
.header-area-2 .ml-desktop-menu .mega-menu.ml-mega .ml-mega__card:hover {
  background: transparent;
}
.ml-mega__thumb {
  display: block;
  overflow: hidden;
  border-radius: 8px;
  aspect-ratio: 4 / 3;
  background: #1a1a1c;
}
.ml-mega__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.5s ease;
}
/* Guarded — see the note on the other hover rules: a tap on touch fires :hover
   and leaves the image stuck mid-zoom. Nothing is revealed by it, so losing the
   effect on touch costs nothing. */
@media (hover: hover) and (pointer: fine) {
  .ml-mega__card:hover .ml-mega__thumb img {
    transform: scale(1.05);
  }
}
.ml-mega__title {
  display: inline-block;
  margin-top: 12px;
  font-size: 15px;
  font-weight: 600;
  color: #ffffff;
  transition: color 0.2s ease;
}
.ml-mega__card:hover .ml-mega__title {
  color: #ffffff;
}
.ml-mega__caption {
  display: block;
  margin-top: 3px;
  font-size: 13px;
  color: #9a9a9a;
  transition: color 0.2s ease;
}
.ml-mega__card:hover .ml-mega__caption {
  color: #cfcfcf;
}

/* "See all" link under the cards — appearance is the shared
   `.ml-line.ml-line--invert` button (see components/common/Buttons.css).
   The compound selector resets the base `.mega-menu li a` chip styling
   (border-radius, black fill, fixed height/padding) that otherwise wins on
   specificity and re-adds the rounded pill look. */
.header-area-2 .ml-desktop-menu .mega-menu.ml-mega .ml-mega__all {
  align-self: flex-end;
  margin-top: 24px;
  height: auto;
  padding: 0 0 6px;
  background: transparent;
  border-radius: 0;
  overflow: visible;
}
.header-area-2 .ml-desktop-menu .mega-menu.ml-mega .ml-mega__all:hover {
  background: transparent;
}

/* Narrower desktop widths: keep 5 columns, tighten spacing */
@media (max-width: 1399px) {
  .ml-mega__layout {
    grid-template-columns: 210px 1fr;
    gap: 28px;
  }
  .ml-mega__panel {
    gap: 18px 12px;
  }
}


/* ===== components\unit\UnitDetail.css ===== */
/* ===========================================================================
   Residence detail template — the whole page in one stylesheet.

   Namespace: `.rd-*` (residence detail). Imported once, by UnitDetail.tsx; the
   sub-components (ResidenceHero, ResidenceStory, ResidenceGallery, BookingCard,
   SimilarResidences, Lightbox) are styled from here rather than each carrying a
   file, because they only ever appear together on this one template.

   Nothing here leans on the template's `.portfolio-details` / `.portfolio-gallery`
   / `.page-title-area` rules — that markup is gone (2026-07-30 rewrite), and so
   are the styles that propped up its specs bar.

   No colour, size or spacing literals: everything comes from the token layer in
   src/styles/. Two local palettes used to live here — a light `--rd-ink/body/
   label/line` set and a dark `--rd-card-*` set for the booking rail — which is
   what made this file its own private design system. Both are gone; the page now
   has THREE contexts and each is expressed the same way:

     light reading column   ambient theme, Layer 2 semantics
     .rd-rail__card         .theme-always-dark island (class in BookingCard.tsx)
     .rd-lightbox           .theme-always-dark island (class in Lightbox.tsx)

   Inside an island every semantic flips, so the SAME declaration —
   `color: var(--text-body)` — is sage on the reading column and 70% white on the
   card. That is the whole reason the local palettes could be deleted rather than
   renamed.
   =========================================================================== */

/* ---- HERO MOSAIC ---------------------------------------------------------- */
/* Height is fixed in CSS, never derived from the images. ScrollSmoother measures
   #smooth-content once at init, so a grid that grows when photos decode leaves
   the page with dead scroll at the bottom. */
.rd-hero {
  padding-top: clamp(120px, 13vw, 175px);
}
.rd-hero__grid {
  position: relative;
  display: grid;
  grid-template-columns: 1.55fr 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 10px;
  height: clamp(420px, 58vh, 620px);
}
.rd-hero__tile {
  display: block;
  padding: 0;
  margin: 0;
  border: 0;
  border-radius: var(--radius-xs);
  background: none;
  overflow: hidden;
  cursor: pointer;
}
.rd-hero__tile--lead {
  grid-row: span 2;
}
.rd-hero__tile img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}
/* Floor plans are line drawings: cropping one loses the rooms at the edge, so
   the plan tiles letterbox on white instead of filling like a photo. */
.rd-hero__tile--plan img {
  object-fit: contain;
  background: var(--ml-white);
}
/* Guarded — see the note on the other hover rules: a tap on touch fires :hover
   and leaves the image stuck mid-zoom. Nothing is revealed by it, so losing the
   effect on touch costs nothing. */
@media (hover: hover) and (pointer: fine) {
  .rd-hero__tile:hover img {
    transform: scale(1.035);
  }
}
.rd-hero__more {
  position: absolute;
  right: 18px;
  bottom: 18px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 11px 18px;
  border: 0;
  border-radius: var(--radius-xs);
  /* Near-opaque surface over a photograph. Off-ladder alpha on purpose: at .90
     the photo starts reading through the label. Built from --bg-rgb rather than
     literal white so the chip follows the surface colour if this ever sits in an
     inverted context. */
  background: rgb(var(--bg-rgb) / 0.94);
  font-family: var(--font-sans);
  font-size: var(--text-s);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--track-1);
  color: var(--text-title);
  cursor: pointer;
  transition: background 0.3s ease;
}
.rd-hero__more:hover {
  background: var(--bg-surface);
}
.rd-hero__more i {
  font-size: var(--text-l);
}

/* ---- INTRO: breadcrumb, title, stats bar, feature pills ------------------- */
.rd-intro {
  padding-top: clamp(34px, 4vw, 54px);
}
.rd-intro__crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--track-3);
  text-transform: uppercase;
  color: var(--text-label);
}
.rd-intro__crumbs a {
  color: var(--text-label);
  text-decoration: none;
  transition: color 0.3s ease;
}
.rd-intro__crumbs a:hover {
  color: var(--text-title);
}
.rd-intro__crumb-current {
  color: var(--text-title);
}

.rd-intro__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 22px;
  margin-top: 20px;
}
/* The page's ONE <h1> — ml-enhance.js splits it into per-character spans for the
   site-wide reveal. Every other heading on this page is h3–h6 so it stays out. */
.rd-intro__title {
  margin: 0;
  font-size: var(--text-5xl);
  font-weight: var(--fw-medium);
  line-height: var(--leading-flat);
  color: var(--text-title);
}
.rd-intro__kicker {
  margin: 14px 0 0;
  font-family: var(--font-sans);
  font-size: var(--text-l);
  line-height: var(--leading-normal);
  color: var(--text-body);
}
.rd-intro__chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 11px 16px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-xs);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--track-3);
  text-transform: uppercase;
  color: var(--text-title);
  white-space: nowrap;
}
.rd-intro__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
/* Sold: filled, so it carries at a glance next to the completion chip. */
.rd-intro__chip--sold {
  color: var(--text-inverse);
  background: var(--ml-teal-700);
  border-color: var(--ml-teal-700);
}
/* Available is a quiet confirmation, not a shout — a dot, not a fill. */
.rd-intro__chip--available::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: var(--radius-circle);
  background: var(--ml-teal-700);
}
/* Sold, in the price cell: the tinted cell would otherwise read as an offer. */
.rd-stats__item--sold {
  background: var(--bg-inset);
}
/* Only the figure is struck through — a line through the word "Sold" as well
   would be nonsense, and text-decoration on the <dd> cannot be undone by a
   child, so the value carries its own box. */
.rd-stats__item--sold .rd-stats__value {
  text-decoration: line-through;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--text-muted);
}
.rd-stats__item--sold .rd-stats__note {
  color: var(--text-title);
  font-weight: var(--fw-semibold);
}

.rd-stats {
  display: flex;
  flex-wrap: wrap;
  margin: clamp(28px, 3.5vw, 44px) 0 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-xs);
}
.rd-stats__item {
  flex: 1 1 150px;
  min-width: 140px;
  padding: 24px 28px;
  border-right: 1px solid var(--line-strong);
}
.rd-stats__item:last-child {
  border-right: 0;
}
.rd-stats dt {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--fw-medium);
  line-height: var(--leading-snug);
  letter-spacing: var(--track-3);
  text-transform: uppercase;
  color: var(--text-label);
}
.rd-stats dd {
  margin: 8px 0 0;
  font-family: var(--font-sans);
  font-size: var(--text-l);
  font-weight: var(--fw-medium);
  line-height: var(--leading-normal);
  letter-spacing: var(--track-tight);
  color: var(--text-title);
}
.rd-stats__note {
  display: block;
  margin-top: 2px;
  font-size: var(--text-xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--track-3);
  text-transform: uppercase;
  color: var(--text-label);
}
/* The price is the one figure a buyer scans for: it gets the tinted cell. */
.rd-stats__item--price {
  background: var(--bg-inset);
}

.rd-features {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 18px 0 0;
  padding: 0;
  list-style: none;
}
.rd-features li {
  padding: 7px 14px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-xs);
  font-family: var(--font-sans);
  font-size: var(--text-s);
  font-weight: var(--fw-medium);
  color: var(--text-title);
}

/* ---- AREA SCHEDULE -------------------------------------------------------- */
/* The measured room-by-room table (inc/units-render.php: ml_render_unit_areas).
   Sits between the intro bar and the reading column, full container width, so
   it reads as a document rather than as body copy. */
.rd-areas {
  padding: clamp(34px, 4vw, 56px) 0 0;
}
.rd-areas__lead {
  flex: 1 1 420px;
  max-width: var(--measure-m);
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-m);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
}
.rd-areas__table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-sans);
}
.rd-areas__table thead th {
  padding: 0 0 10px;
  border-bottom: 1px solid var(--line-strong);
  font-size: var(--text-xs);
  font-weight: var(--fw-medium);
  line-height: var(--leading-snug);
  letter-spacing: var(--track-3);
  text-transform: uppercase;
  text-align: left;
  color: var(--text-label);
}
.rd-areas__num {
  text-align: right;
  white-space: nowrap;
}
/* Numeric column headings must line up with the figures below them: the blanket
   .rd-areas__table thead th rule above sets text-align:left and out-specifies
   .rd-areas__num, so the header twin of .rd-areas__row td.rd-areas__num is
   required. Re-applied 2026-09-22 after a theme re-export dropped it. */
.rd-areas__table thead th.rd-areas__num {
  text-align: right;
}
.rd-areas__row th,
.rd-areas__row td {
  padding: 11px 0;
  border-bottom: 1px solid var(--line);
  font-size: var(--text-m);
  font-weight: var(--fw-regular);
  line-height: var(--leading-normal);
  text-align: left;
  color: var(--text-body);
}
.rd-areas__row td.rd-areas__num {
  text-align: right;
}
.rd-areas__no {
  display: inline-block;
  min-width: 2.1em;
  font-size: var(--text-xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--track-2);
  color: var(--text-muted);
}
.rd-areas__num--coef {
  color: var(--text-muted);
}
/* Group caption ("Interior", "Ground floor — outdoor"). */
.rd-areas__row--head th {
  padding: 26px 0 8px;
  border-bottom: 1px solid var(--line-strong);
  font-size: var(--text-xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--track-3);
  text-transform: uppercase;
  color: var(--text-label);
}
.rd-areas__row--total th,
.rd-areas__row--total td {
  font-weight: var(--fw-medium);
  color: var(--text-title);
}
.rd-areas__row--grand th,
.rd-areas__row--grand td {
  padding: 16px 0;
  border-top: 1px solid var(--line-strong);
  border-bottom: 0;
  font-size: var(--text-l);
  font-weight: var(--fw-semibold);
  color: var(--text-title);
}
.rd-areas__hint {
  display: block;
  margin-top: 3px;
  font-size: var(--text-xs);
  font-weight: var(--fw-regular);
  letter-spacing: var(--track-2);
  text-transform: none;
  color: var(--text-label);
}
.rd-areas__foot {
  margin: 16px 0 0;
  padding-left: 14px;
  border-left: 2px solid var(--line-strong);
  font-family: var(--font-sans);
  font-size: var(--text-m);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
}
@media (max-width: 767px) {
  /* Measured + coefficient are provenance, not the figure being bought: drop
     them rather than let a four-column table scroll sideways on a phone. */
  .rd-areas__num--raw,
  .rd-areas__num--coef {
    display: none;
  }
}

/* ---- TWO-COLUMN BODY ------------------------------------------------------ */
.rd-body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 380px;
  gap: clamp(32px, 4vw, 72px);
  align-items: stretch; /* the rail cell must span the row for the sticky travel */
  padding: clamp(44px, 5vw, 70px) 0 clamp(56px, 7vw, 100px);
}
/* residence-booking switched off in the registry → single reading column */
.rd-body--wide {
  grid-template-columns: minmax(0, 1fr);
}
.rd-body--wide .rd-main {
  max-width: var(--measure-xl);
}
.rd-main > section + section {
  margin-top: clamp(40px, 5vw, 66px);
  padding-top: clamp(36px, 4.5vw, 58px);
  border-top: 1px solid var(--line-strong);
}

.rd-block__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  margin-bottom: 22px;
}
.rd-block__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--fw-medium);
  line-height: var(--leading-tight);
  letter-spacing: var(--track-2);
  color: var(--text-title);
}

/* ---- ABOUT THE PROPERTY --------------------------------------------------- */
.rd-overview__lead {
  margin: 0 0 22px;
  font-family: var(--font-sans);
  font-size: var(--text-xl);
  font-weight: var(--fw-medium);
  line-height: var(--leading-relaxed);
  letter-spacing: var(--track-tight);
  color: var(--text-title);
}
.rd-overview__lead strong {
  font-weight: var(--fw-semibold);
}
.rd-overview p {
  margin: 0 0 18px;
  font-family: var(--font-sans);
  font-size: var(--text-l);
  font-weight: var(--fw-medium);
  line-height: var(--leading-loose);
  letter-spacing: var(--track-tight);
  color: var(--text-body);
}
.rd-overview__sub {
  margin: 30px 0 12px;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--track-3);
  text-transform: uppercase;
  color: var(--text-label);
}
.rd-overview__list {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--line-strong);
}
.rd-overview__list li {
  padding: 11px 2px;
  border-bottom: 1px solid var(--line-strong);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: var(--fw-medium);
  color: var(--text-title);
}
/* the section divider below already draws a rule — two of them 50px apart read
   as an empty table row */
.rd-overview__list li:last-child,
.rd-story__rooms li:last-child {
  border-bottom: 0;
}

/* ---- THE STORY (rooms as an article) -------------------------------------- */
.rd-story__title {
  margin: 0 0 14px;
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: var(--fw-medium);
  line-height: var(--leading-tight);
  letter-spacing: var(--track-2);
  color: var(--text-title);
}
.rd-story__lead {
  margin: 0 0 26px;
  font-family: var(--font-sans);
  font-size: var(--text-l);
  font-weight: var(--fw-medium);
  line-height: var(--leading-loose);
  letter-spacing: var(--track-tight);
  color: var(--text-title);
}
/* --text-HEADING, not --text-title: this sub-heading is set in the sans, and the
   brand teal is reserved for the Vogue face. Same rule as globals.css. */
.rd-story h4 {
  margin: 34px 0 12px;
  font-family: var(--font-sans);
  font-size: var(--text-xl);
  font-weight: var(--fw-semibold);
  line-height: var(--leading-snug);
  letter-spacing: var(--track-tight);
  color: var(--text-heading);
}
.rd-story p {
  margin: 0 0 16px;
  font-family: var(--font-sans);
  font-size: var(--text-l);
  font-weight: var(--fw-medium);
  line-height: var(--leading-loose);
  letter-spacing: var(--track-tight);
  color: var(--text-body);
}
.rd-story__figure {
  margin: 30px 0;
}
.rd-story__figure img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  border-radius: var(--radius-xs);
}
.rd-story__figure figcaption {
  margin-top: 10px;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--track-3);
  text-transform: uppercase;
  color: var(--text-label);
}
/* Selector doubled up to `.rd-story p.rd-story__label` (0,2,1) so it outranks
   `.rd-story p` (0,1,1) a few rules above, which is the ONLY thing the three
   !important here were beating — a sibling rule in this same file, not the
   vendor. Specificity states that relationship; !important hid it. */
.rd-story p.rd-story__label {
  margin: 38px 0 12px;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--track-3);
  text-transform: uppercase;
  color: var(--text-label);
}
.rd-story__rooms {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--line-strong);
}
.rd-story__rooms li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 20px;
  padding: 12px 2px;
  border-bottom: 1px solid var(--line-strong);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: var(--fw-medium);
}
.rd-story__room-name {
  color: var(--text-title);
}
.rd-story__room-area {
  color: var(--text-label);
  white-space: nowrap;
}
/* The apartment m² are generated, not measured — this footnote says so.
   Doubled selector for the same reason as .rd-story__label above. */
.rd-story p.rd-story__note {
  margin: 18px 0 0;
  padding-left: 14px;
  border-left: 2px solid var(--line-strong);
  font-size: var(--text-s);
  line-height: var(--leading-relaxed);
  color: var(--text-label);
}

/* ---- BOOKING RAIL --------------------------------------------------------- */
/* `position: sticky` is the baseline and works wherever no ScrollSmoother exists
   (phones skip it entirely). When an instance IS present the rail is inert, so
   useStickyRail.ts adds `.is-emulated` and drives a transform instead. */
.rd-rail {
  position: relative;
}
/* Kept deliberately compact: once the card is taller than the viewport,
   useStickyRail switches to holding its BOTTOM on screen so the submit button
   stays reachable, and the heading scrolls out of sight. Every row trimmed here
   buys another laptop height where that never happens. */
/* The card is the one dark surface in the reading column — black so it reads as
   the thing to act on against a white page. It carries `theme-always-dark`
   (BookingCard.tsx), which is what the old four local `--rd-card-*` properties
   were approximating by hand: the previous comment here promised the card "can
   be flipped back to light by editing these four lines", and swapping that one
   class to `theme-always-light` is now literally that.

   `color-scheme: dark` used to be set here, and is not cosmetic — without it
   Chrome paints the native <input type="date"> calendar indicator near-black,
   i.e. invisible on #111. The island sets it now, from --color-scheme. */
.rd-rail__card {
  position: sticky;
  top: 110px; /* clears the fixed header when it slides back in */
  padding: 26px 24px;
  /* Border matches the fill: it exists so the card keeps its exact box size
     whether or not a future variant outlines it. */
  border: 1px solid var(--bg-surface);
  border-radius: var(--radius-xs);
  background: var(--bg-surface);
  color: var(--text-title);
  will-change: transform;
}
.rd-rail__card.is-emulated {
  position: relative;
  top: auto;
}
.rd-rail__kicker {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--track-3);
  text-transform: uppercase;
  color: var(--text-body);
}
.rd-rail__title {
  margin: 6px 0 18px;
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--fw-medium);
  line-height: var(--leading-tight);
  letter-spacing: var(--track-2);
  color: var(--text-title);
}

.rd-form__row {
  margin-bottom: 14px;
}
.rd-field {
  display: block;
}
.rd-field__label {
  display: block;
  margin-bottom: 6px;
  padding: 0;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--track-3);
  text-transform: uppercase;
  color: var(--text-body);
}
.rd-form .rd-field__input {
  width: 100%;
  height: 36px;
  padding: 0;
  border: 0;
  border-bottom: 1px solid var(--line-strong);
  border-radius: 0;
  background: none;
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: var(--fw-medium);
  color: var(--text-title);
  transition: border-color 0.3s ease;
}
.rd-form .rd-field__input::placeholder {
  color: var(--text-label);
}
.rd-form .rd-field__input:focus {
  outline: none;
  border-bottom-color: var(--text-title);
}
.rd-form .rd-field__input:focus-visible {
  outline: 2px solid var(--text-title);
  outline-offset: 4px;
}

/* Date field — dress the native <input type="date"> so it reads as part of the
   set instead of a raw browser control. The native picker is kept (accessible,
   correct on mobile, no JS fighting the template/ScrollSmoother); only its chrome
   is restyled: reserve room on the right and swap the stock calendar glyph for a
   Phosphor "calendar-blank" drawn with a mask so it inherits the field's text
   colour — no light/dark icon to hand-maintain. */
.rd-form .rd-field__input[type="date"] {
  padding-right: 26px;
  cursor: pointer;
}
.rd-form .rd-field__input[type="date"]::-webkit-datetime-edit {
  padding: 0;
}
.rd-form .rd-field__input[type="date"]::-webkit-calendar-picker-indicator {
  width: 16px;
  height: 16px;
  margin: 0;
  padding: 0;
  opacity: 0.65;
  cursor: pointer;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath d='M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32Zm0,16V72H48V48ZM48,208V88H208V208Z'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Cpath d='M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32Zm0,16V72H48V48ZM48,208V88H208V208Z'/%3E%3C/svg%3E") center / contain no-repeat;
  transition: opacity 0.3s ease;
}
.rd-form .rd-field__input[type="date"]:hover::-webkit-calendar-picker-indicator,
.rd-form .rd-field__input[type="date"]:focus::-webkit-calendar-picker-indicator {
  opacity: 1;
}

/* Time slots — radios, not a <select>: the template bundle runs
   $("select").niceSelect() globally and would replace any select with its own
   markup that React knows nothing about. */
.rd-slots {
  margin: 0 0 14px;
  padding: 0;
  border: 0;
}
.rd-slots__options {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.rd-slot {
  position: relative;
  display: block;
}
.rd-slot input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
  cursor: pointer;
}
.rd-slot span {
  display: block;
  padding: 9px 14px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-xs);
  font-family: var(--font-sans);
  font-size: var(--text-s);
  font-weight: var(--fw-medium);
  color: var(--text-body);
  transition: color 0.3s ease, border-color 0.3s ease, background-color 0.3s ease;
}
.rd-slot input:hover + span {
  border-color: var(--text-title);
  color: var(--text-title);
}
/* selected slot inverts to a solid light chip — the only filled element in the
   card besides the submit button, so the choice is unmistakable */
.rd-slot input:checked + span {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--accent-contrast);
}
.rd-slot input:focus-visible + span {
  outline: 2px solid var(--text-title);
  outline-offset: 3px;
}

.rd-stepper {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
}
.rd-stepper .rd-field__label {
  margin-bottom: 0;
}
.rd-stepper__controls {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-xs);
}
.rd-stepper__controls button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  background: none;
  font-size: var(--text-m);
  color: var(--text-title);
  cursor: pointer;
  transition: opacity 0.3s ease;
}
/* .45 rather than .3: a disabled control still has to be legible on black */
.rd-stepper__controls button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.rd-stepper__controls button:focus-visible {
  outline: 2px solid var(--text-title);
  outline-offset: 2px;
}
.rd-stepper__controls output {
  min-width: 82px;
  text-align: center;
  font-family: var(--font-sans);
  font-size: var(--text-m);
  font-weight: var(--fw-medium);
  color: var(--text-title);
}

/* `.ml-btn--invert` (white fill, dark label) from common/Buttons.css — the plain
   `.ml-btn` is a black fill and would vanish into the card. */
.rd-form__submit {
  display: flex;
  width: 100%;
  justify-content: center;
  margin-top: 18px;
}
.rd-form__note {
  margin: 12px 0 0;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
}
/* Shown after "Check availability" — the widget is demonstrative, so this says
   plainly that nothing was sent and points at the real form. */
.rd-form__reply {
  margin-top: 20px;
  padding-top: 18px;
  border-top: 1px solid var(--line-strong);
}
.rd-form__reply p {
  margin: 0 0 14px;
  font-family: var(--font-sans);
  font-size: var(--text-m);
  line-height: var(--leading-relaxed);
  color: var(--text-body);
}

/* ---- OTHER RESIDENCES ----------------------------------------------------- */
.rd-similar {
  padding: clamp(48px, 6vw, 84px) 0;
  border-top: 1px solid var(--line-strong);
}
.rd-similar__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  margin-bottom: clamp(24px, 3vw, 38px);
}
.rd-similar__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: var(--fw-medium);
  line-height: var(--leading-display);
  letter-spacing: var(--track-2);
  color: var(--text-title);
}
/* NOT `.ml-residences-filter` — that class is what ml-enhance.js binds the
   /residences facet filter to, and there is no filter on this page. */
.rd-similar__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 24px;
}

/* ---- LIGHTBOX ------------------------------------------------------------- */
/* Portalled into <body>, i.e. outside #smooth-content, so `fixed` resolves
   against the viewport (see Lightbox.tsx). z-index sits above the header (800)
   and below the side panel (9999). */
.rd-lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-lightbox);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(16px, 4vw, 56px);
  /* Off-ladder alpha: the backdrop has to read as opaque so the page behind it
     does not compete with the photograph, while still hinting at depth. */
  background: rgb(var(--bg-rgb) / 0.95);
}
.rd-lightbox__stage {
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-width: var(--width-wide);
}
.rd-lightbox__img {
  display: block;
  max-width: 100%;
  max-height: 76vh;
  object-fit: contain;
  margin: 0 auto;
}
.rd-lightbox__caption {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-s);
  color: var(--text-body);
}
.rd-lightbox__count {
  white-space: nowrap;
  letter-spacing: var(--track-3);
}
.rd-lightbox__btn {
  position: absolute;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  padding: 0;
  border: 1px solid var(--fg-30);
  border-radius: var(--radius-xs);
  /* --scrim, not --bg: these buttons sit over the PHOTOGRAPH, not over the
     backdrop, so the wash under them must stay black in either theme. */
  background: var(--scrim-30);
  font-size: var(--text-xl);
  color: var(--text-title);
  cursor: pointer;
  transition: background-color 0.3s ease, border-color 0.3s ease;
}
.rd-lightbox__btn:hover {
  background: var(--fg-15);
  border-color: var(--fg-50);
}
.rd-lightbox__btn--close {
  top: clamp(14px, 3vw, 28px);
  right: clamp(14px, 3vw, 28px);
}
.rd-lightbox__btn--prev,
.rd-lightbox__btn--next {
  top: 50%;
  transform: translateY(-50%);
}
.rd-lightbox__btn--prev {
  left: clamp(14px, 3vw, 28px);
}
.rd-lightbox__btn--next {
  right: clamp(14px, 3vw, 28px);
}

/* ---- RESPONSIVE ----------------------------------------------------------- */
@media (max-width: 1199px) {
  .rd-body {
    grid-template-columns: minmax(0, 1fr) 330px;
  }
  .rd-similar__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 991px) {
  .rd-body {
    grid-template-columns: minmax(0, 1fr);
  }
  /* Stacked below the reading column — nothing to stick to, and the emulation
     hook stops writing transforms below this width.
     The !important is the one legitimate kind and must stay: useStickyRail.ts
     writes `card.style.transform` INLINE, and an inline declaration outranks any
     selector. It clears the last transform written before the viewport crossed
     this breakpoint, which would otherwise leave the card visibly offset. */
  .rd-rail__card,
  .rd-rail__card.is-emulated {
    position: static;
    transform: none !important;
  }
  .rd-hero__grid {
    grid-template-columns: 1.4fr 1fr;
    height: clamp(380px, 52vh, 520px);
  }
  /* only the lead shot plus the first two tiles fit at this width */
  .rd-hero__tile:nth-child(n + 4) {
    display: none;
  }
  .rd-stats__item {
    flex: 1 1 33.333%;
  }
}

@media (max-width: 767px) {
  .rd-hero {
    padding-top: 110px;
  }
  .rd-hero__grid {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr;
    height: clamp(300px, 46vh, 400px);
  }
  .rd-hero__tile--lead {
    grid-row: span 1;
  }
  .rd-hero__tile:nth-child(n + 2) {
    display: none;
  }
  .rd-similar__grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (max-width: 575px) {
  .rd-stats__item {
    flex: 1 1 50%;
    padding: 18px 20px;
  }
  .rd-rail__card {
    padding: 22px;
  }
  .rd-lightbox__btn {
    width: 42px;
    height: 42px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .rd-hero__tile img,
  .rd-slot span,
  .rd-form .rd-field__input,
  .rd-lightbox__btn {
    transition: none;
  }
  .rd-hero__tile:hover img {
    transform: none;
  }
}


/* ===== components\unit\RoomByRoom.css ===== */
/* ===========================================================================
   Room-by-room feature section (residence detail pages, above the gallery).
   Centred Vogue intro over alternating, interlocked image/text rows. Scoped
   entirely to .room-by-room so it never touches template sections.
   =========================================================================== */

.room-by-room {
  padding: clamp(4rem, 9vw, 9rem) 0 clamp(3rem, 7vw, 7rem);
  overflow: hidden; /* contain the negative-margin interlock */
}

/* --- Centred intro --------------------------------------------------------- */
.room-by-room__head {
  max-width: 62rem;
  margin: 0 auto clamp(3rem, 6vw, 6rem);
  text-align: center;
}
.room-by-room__title {
  font-family: var(--font-display);
  color: #17332e;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  line-height: 1.12;
  font-size: clamp(1.7rem, 3.4vw, 3.1rem);
  margin: 0;
}
.room-by-room__intro {
  max-width: 44rem;
  margin: 1.25rem auto 0;
  color: #5a635f;
  font-size: clamp(0.9rem, 1.1vw, 1rem);
  line-height: 1.7;
}

/* --- Alternating rows ------------------------------------------------------ */
.room-by-room__row {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  align-items: center;
  column-gap: clamp(2rem, 4vw, 5rem);
}
/* Breathing room between rows so each one reads as a distinct block. */
.room-by-room__row + .room-by-room__row {
  margin-top: clamp(3rem, 6vw, 6.5rem);
}

/* Keep media + text on the SAME grid row. Without this, on the even rows the
   media (DOM-first, right-hand columns) makes the auto-placer push the text to
   a new row — the misalignment we saw. Pinning both to row 1 fixes it. */
.room-by-room__media,
.room-by-room__text {
  grid-row: 1;
}
/* Odd rows: image left, text right. */
.room-by-room__row:nth-child(odd) .room-by-room__media {
  grid-column: 1 / 8;
}
.room-by-room__row:nth-child(odd) .room-by-room__text {
  grid-column: 9 / 13;
}
/* Even rows: text left, image right. */
.room-by-room__row:nth-child(even) .room-by-room__media {
  grid-column: 6 / 13;
}
.room-by-room__row:nth-child(even) .room-by-room__text {
  grid-column: 1 / 5;
}

.room-by-room__media {
  border-radius: 2px;
}
.room-by-room__media img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

/* --- Text block ------------------------------------------------------------ */
.room-by-room__badge {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: #17332e;
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  margin-bottom: 0.85rem;
}
.room-by-room__badge i {
  font-size: 1.05rem;
  opacity: 0.75;
}
.room-by-room__name {
  font-family: var(--font-display);
  color: #17332e;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-size: clamp(1.35rem, 2.2vw, 2rem);
  line-height: 1.15;
  margin: 0 0 1rem;
}
.room-by-room__desc {
  color: #5a635f;
  font-size: clamp(0.85rem, 1vw, 0.95rem);
  line-height: 1.75;
  margin: 0;
  max-width: 30rem;
}

/* --- Stack on smaller screens ---------------------------------------------- */
@media (max-width: 991px) {
  .room-by-room__row,
  .room-by-room__row:nth-child(even) {
    grid-template-columns: 1fr;
    row-gap: 1.5rem;
  }
  .room-by-room__row + .room-by-room__row {
    margin-top: clamp(2.5rem, 8vw, 4.5rem);
  }
  .room-by-room__row .room-by-room__media,
  .room-by-room__row .room-by-room__text {
    grid-column: 1 / -1;
    grid-row: auto; /* stack, don't overlap on row 1 */
  }
  /* Keep the image first on every stacked row. */
  .room-by-room__media {
    order: 0;
  }
  .room-by-room__text {
    order: 1;
  }
  .room-by-room__desc {
    max-width: none;
  }
}


/* ===== components\reactbits\Aurora\Aurora.css ===== */
.aurora-container {
  width: 100%;
  height: 100%;
}


/* ===== components\reactbits\ScrollFloat\ScrollFloat.css ===== */
.scroll-float {
  overflow: hidden;
}

.scroll-float-text {
  display: inline-block;
  font-size: clamp(1.6rem, 8vw, 10rem);
  font-weight: 900;
  text-align: center;
  line-height: 1.5;
}

.char {
  display: inline-block;
}


/* ===== components\reactbits\SpotlightCard\SpotlightCard.css ===== */
.card-spotlight {
  position: relative;
  border-radius: 1.5rem;
  border: 1px solid #222;
  background-color: #111;
  padding: 2rem;
  overflow: hidden;
  --mouse-x: 50%;
  --mouse-y: 50%;
  --spotlight-color: rgba(255, 255, 255, 0.05);
}

.card-spotlight::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: radial-gradient(circle at var(--mouse-x) var(--mouse-y), var(--spotlight-color), transparent 80%);
  opacity: 0;
  transition: opacity 0.5s ease;
  pointer-events: none;
}

.card-spotlight:hover::before,
.card-spotlight:focus-within::before {
  opacity: 0.6;
}


/* ===== components\reactbits\TiltedCard\TiltedCard.css ===== */
.tilted-card-figure {
  position: relative;
  width: 100%;
  height: 100%;
  perspective: 800px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.tilted-card-mobile-alert {
  position: absolute;
  top: 1rem;
  text-align: center;
  font-size: 0.875rem;
  display: none;
}

@media (max-width: 640px) {
  .tilted-card-mobile-alert {
    display: block;
  }
  .tilted-card-caption {
    display: none;
  }
}

.tilted-card-inner {
  position: relative;
  transform-style: preserve-3d;
}

.tilted-card-img {
  position: absolute;
  top: 0;
  left: 0;
  object-fit: cover;
  border-radius: 15px;
  will-change: transform;
  transform: translateZ(0);
}

.tilted-card-overlay {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  will-change: transform;
  transform: translateZ(30px);
}

.tilted-card-caption {
  pointer-events: none;
  position: absolute;
  left: 0;
  top: 0;
  border-radius: 4px;
  background-color: #fff;
  padding: 4px 10px;
  font-size: 10px;
  color: #2d2d2d;
  opacity: 0;
  z-index: 3;
}

