/* ==========================================================================
   Crestbrick — Home, Base31-referenced option
   Self-contained stylesheet. This page links ONLY this file, deliberately:
   the old version of this option was an override layer stacked on styles.css +
   home.css, and three layers of ever-more-specific selectors fought over
   `color`. That cascade war is what produced unreadable and inconsistent text
   (measured: h4 at 1.26:1 dark-on-dark, cream lede at 1.58:1 on a sky photo).

   ------------------------------------------------------------------------
   THE ONE INVARIANT — do not break it:

     Only a [data-surface] block may contain a literal colour.
     Every component names a ROLE (--text-1/-2/-3, --accent, --rule, --btn-*),
     never a colour.

   Why this makes the bug structurally impossible: a surface always binds its
   background and its full text set together, so text can never be left behind
   when a section's background changes. And because every text rule is written
   at flat, minimal specificity, nothing needs !important to win — so no rule
   can be silently outranked the way `body.home .bg-green-950 .num-grid.notch-row
   > .num-card h4` (0,4,3) outranked its own parent's cream (0,3,2).

   Every role pair below is verified >= 4.6:1 (AA body) — see the contrast
   matrix in the commit notes. Muted text is the role that kept failing before
   (3.09-4.36:1); it now sits at 5.35-8.07:1.

   Design reference: the Crestbrick GHL page
   (sites.leadconnectorhq.com/preview/VdxoUlfV0SrDI2j3rz0V) — mottled paper
   texture on cream bands, deep-green bands, a rounded inner card for the
   consultation CTA, and a hairline-divided footer. Values below were measured
   off it over CDP, not eyeballed: card radius 25px, field radius 15px, pill
   button radius 30px, footer rule rgba(235,235,235,.46), section pad 80px.
   Structural inheritance from the earlier base31.ca pass is kept where it does
   not fight the reference: hairline-divided columns, uppercase micro-labels,
   centred editorial chapters, glyph-prefixed buttons.
   Testimonials follow redpointproperties.uk: one dark full-bleed photo band,
   quotes set open on the ground with an accent quote glyph — no cards, no
   carousel.
   Fonts are the CBK set and ONLY that: DM Serif Display + Manrope. DM Mono was
   dropped (see --mono).
   ========================================================================== */

/* ==========================================================================
   1. PRIMITIVES — the only place raw hex is allowed to originate
   ========================================================================== */
:root {
  /* Crestbrick brand greens (CBK Brand Guide) */
  --c-deep:      #0C2A1E;
  --c-forest:    #113E2C;
  --c-moss:      #195A40;
  --c-lime:      #A3D75F;

  /* Inks for light surfaces. Solved against the WORST realistic ground, not the
     bare surface: components sit on --tint, and a card on a tinted surface can be
     double-tinted (#D7D5CB). The first pass used #4A6357 / #3D6B24, which passed
     on bare cream but measured 4.44 / 4.29 there. These clear 4.9 everywhere. */
  --c-ink-1:     #0C2A1E;
  --c-ink-2:     #24473A;
  --c-ink-3:     #3F564B;
  --c-accent-dk: #35601C;

  /* Creams for dark surfaces */
  --c-cream-1:   #F6F2EF;
  --c-cream-2:   #DCE7E0;
  --c-cream-3:   #A9C2AE;
  /* A fourth, for the moss ground only. --c-cream-3 is the muted role on deep
     and forest, but on moss it measures 4.27:1 and fails AA; this clears it at
     5.22:1 while staying visibly quieter than --c-cream-2 (6.42:1 there). */
  --c-cream-4:   #BCD5C3;

  /* Neutral surfaces */
  --c-paper:     #FFFFFF;
  --c-cream-bg:  #F6F2EF;
  --c-sand:      #EDE8DE;

  /* Scrim channel. Every veil over photography mixes THIS colour, so the ground
     under overlaid text is the same brand green everywhere and can't drift
     per-component. Declared as raw channels so alpha stays tunable per stop. */
  /* Paper / canvas textures.
     --tex-paper is the reference site's own paper stock, pulled local (the GHL
     CDN is never referenced from this repo — see CLAUDE.md). It replaces
     cream-glow.jpg, which was a soft directional *gradient*, not a grain: at the
     wash alphas below it read as a flat tint and the "paper" never showed. This
     one is a mottled cream fibre, which is the texture the reference actually
     uses (measured off it: background-size:cover, position 50% 50%).
     (The reference also has a coarser grey stock for one band it sets on grey
     rather than cream. This page has no grey band — its light sections are always
     separated by a green one — so that file is not carried here.) */
  --tex-paper:  url(../assets/texture/paper-cream.jpg);
  --tex-canvas: url(../assets/texture/green-canvas.jpg);

  --scrim-rgb:   12, 42, 30;
  --photo-cue-rgb: 0, 0, 0;   /* neutral ground for a pill sitting on a photo */
  /* Flat floor layer, composited UNDER every shaped scrim. Any overlay that
     carries text uses it, so the weakest point of a scrim is never left to the
     gradient alone. Without it the band's lime label measured 4.29:1 over the
     photo's bright dusk sky; with it, 5.4:1. */
  --scrim-floor: linear-gradient(rgba(var(--scrim-rgb), .34), rgba(var(--scrim-rgb), .34));
  --shadow-menu: 0 24px 60px -28px rgba(12, 42, 30, .5);

  /* Type — CBK brand faces only: DM Serif Display + Manrope. Nothing else ships.
     --mono keeps its name because ~20 rules below reference it, but it is no
     longer a monospace: DM Mono was dropped. The micro-labels it drives are
     letterspaced uppercase Manrope, which is also what the reference site does
     with its own labels (they are the content face in caps, not a mono). */
  --sans:  "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --serif: "DM Serif Display", ui-serif, Georgia, "Times New Roman", serif;
  --mono:  var(--sans);

  /* Rhythm — base31's spacing tokens */
  --gutter:      clamp(16px, 4vw, 24px);
  --gap:         clamp(12px, 1.4vw, 16px);
  --pad-section: clamp(64px, 8vw, 112px);
  --pad-band:    clamp(48px, 6vw, 80px);
  --maxw:        1360px;
  --maxw-text:   62ch;
  /* Uniform radii — every corner of an element gets the SAME value. (An earlier
     pass used Crestbrick's asymmetric notch/swoop corners; dropped in favour of
     this.) The three values below are the reference site's own, read off its
     computed styles rather than estimated: it uses exactly 15px on every field,
     25px on the consultation card, and a full pill on every button. */
  --radius:      15px;   /* inputs, chips, small controls */
  --radius-lg:   25px;   /* cards, panels, media, figures */
  --radius-pill: 999px;  /* buttons */
  --nav-h:       clamp(64px, 7vw, 78px);
  --tap:         44px;       /* minimum tap target */

  /* One interaction colour across every actionable control. Bright CBK green
     is used as a fill where dark text is needed and as text on dark surfaces. */
  --action-hover-bg: var(--c-lime);
  --action-hover-fg: var(--c-deep);
  --action-hover-text: var(--c-lime);
}

/* ==========================================================================
   2. SURFACES — a surface binds background + the COMPLETE text set together.
      Adding a new surface means filling in every role. That is the point.
   ========================================================================== */
[data-surface="paper"] {
  --tex: var(--tex-paper);
  --tex-wash: rgba(255, 255, 255, .52);
  --bg: var(--c-paper);
  --text-1: var(--c-ink-1);  --text-2: var(--c-ink-2);  --text-3: var(--c-ink-3);
  --accent: var(--c-accent-dk);
  --rule: rgba(12, 42, 30, .16);
  --rule-strong: rgba(12, 42, 30, .42);
  --btn-bg: var(--c-deep);       --btn-fg: var(--c-cream-1);  --btn-bg-hover: var(--c-forest);
  --btn2-fg: var(--c-ink-1);     --btn2-bd: rgba(12, 42, 30, .42);  --btn2-bg-hover: rgba(12, 42, 30, .07);
  --chip-fg: var(--c-ink-2);     --chip-bd: rgba(12, 42, 30, .30);
  --field-bg: var(--c-paper);    --field-fg: var(--c-ink-1);  --field-bd: rgba(12, 42, 30, .26);
  --tint: rgba(12, 42, 30, .04);
  --card-bg: var(--c-cream-bg);  --card-bd: rgba(12, 42, 30, .10);  --card-shadow: 0 1px 2px rgba(12, 42, 30, .04);
}

[data-surface="cream"] {
  --tex: var(--tex-paper);
  --tex-wash: rgba(246, 242, 239, .40);
  --bg: var(--c-cream-bg);
  --text-1: var(--c-ink-1);  --text-2: var(--c-ink-2);  --text-3: var(--c-ink-3);
  --accent: var(--c-accent-dk);
  --rule: rgba(12, 42, 30, .17);
  --rule-strong: rgba(12, 42, 30, .44);
  --btn-bg: var(--c-deep);       --btn-fg: var(--c-cream-1);  --btn-bg-hover: var(--c-forest);
  --btn2-fg: var(--c-ink-1);     --btn2-bd: rgba(12, 42, 30, .44);  --btn2-bg-hover: rgba(12, 42, 30, .07);
  --chip-fg: var(--c-ink-2);     --chip-bd: rgba(12, 42, 30, .32);
  --field-bg: var(--c-paper);    --field-fg: var(--c-ink-1);  --field-bd: rgba(12, 42, 30, .28);
  --tint: rgba(12, 42, 30, .05);
  --card-bg: var(--c-paper);  --card-bd: rgba(12, 42, 30, .07);  --card-shadow: 0 1px 3px rgba(12, 42, 30, .05);
}

[data-surface="sand"] {
  --tex: var(--tex-paper);
  --tex-wash: rgba(237, 232, 222, .48);
  --bg: var(--c-sand);
  --text-1: var(--c-ink-1);  --text-2: var(--c-ink-2);  --text-3: var(--c-ink-3);
  --accent: var(--c-accent-dk);
  --rule: rgba(12, 42, 30, .18);
  --rule-strong: rgba(12, 42, 30, .46);
  --btn-bg: var(--c-deep);       --btn-fg: var(--c-cream-1);  --btn-bg-hover: var(--c-forest);
  --btn2-fg: var(--c-ink-1);     --btn2-bd: rgba(12, 42, 30, .46);  --btn2-bg-hover: rgba(12, 42, 30, .07);
  --chip-fg: var(--c-ink-2);     --chip-bd: rgba(12, 42, 30, .34);
  --field-bg: var(--c-paper);    --field-fg: var(--c-ink-1);  --field-bd: rgba(12, 42, 30, .30);
  --tint: rgba(12, 42, 30, .05);
  --card-bg: var(--c-paper);  --card-bd: rgba(12, 42, 30, .08);  --card-shadow: 0 1px 3px rgba(12, 42, 30, .05);
}

/* Brand lime as a GROUND, not an accent. Used by the values cards on /about.
   Note --accent: the default light-surface accent (--c-accent-dk #35601C)
   measures 4.38:1 here and fails AA for the small tracked labels that use it,
   so this surface binds --c-forest (7.11:1) instead. That is exactly the kind
   of thing a surface exists to get right once. */
[data-surface="lime"] {
  --tex: none;
  --tex-wash: none;
  --bg: var(--c-lime);
  --text-1: var(--c-ink-1);  --text-2: var(--c-ink-2);  --text-3: var(--c-ink-3);
  --accent: var(--c-forest);
  --rule: rgba(12, 42, 30, .24);
  --rule-strong: rgba(12, 42, 30, .52);
  --btn-bg: var(--c-deep);       --btn-fg: var(--c-cream-1);  --btn-bg-hover: var(--c-forest);
  --btn2-fg: var(--c-ink-1);     --btn2-bd: rgba(12, 42, 30, .52);  --btn2-bg-hover: rgba(12, 42, 30, .09);
  --chip-fg: var(--c-ink-1);     --chip-bd: rgba(12, 42, 30, .42);
  --field-bg: var(--c-paper);    --field-fg: var(--c-ink-1);  --field-bd: rgba(12, 42, 30, .32);
  --tint: rgba(12, 42, 30, .09);
  --card-bg: var(--c-paper);  --card-bd: rgba(12, 42, 30, .12);  --card-shadow: 0 1px 3px rgba(12, 42, 30, .06);
  /* The site-wide hover fill IS lime. On a lime ground that is an invisible
     hover, so this surface flips the pair rather than inheriting it. */
  --action-hover-bg: var(--c-deep);
  --action-hover-fg: var(--c-lime);
  --action-hover-text: var(--c-forest);
}

/* Mid-green ground. It exists because deep and forest are only 1.28:1 apart —
   set side by side as two cards they read as one block — where moss against
   deep is 1.89:1 and separates properly. */
[data-surface="moss"] {
  --tex: var(--tex-canvas);
  --tex-wash: rgba(25, 90, 64, .84);
  --bg: var(--c-moss);
  --text-1: var(--c-cream-1);  --text-2: var(--c-cream-2);  --text-3: var(--c-cream-4);
  --accent: var(--c-lime);
  --rule: rgba(246, 242, 239, .22);
  --rule-strong: rgba(246, 242, 239, .50);
  --btn-bg: var(--c-cream-1);    --btn-fg: var(--c-deep);     --btn-bg-hover: var(--c-paper);
  --btn2-fg: var(--c-cream-1);   --btn2-bd: rgba(246, 242, 239, .50);  --btn2-bg-hover: rgba(246, 242, 239, .12);
  --chip-fg: var(--c-cream-1);   --chip-bd: rgba(246, 242, 239, .38);
  --field-bg: var(--c-cream-1);  --field-fg: var(--c-ink-1);  --field-bd: rgba(12, 42, 30, .30);
  --tint: rgba(246, 242, 239, .08);
  --card-bg: var(--c-forest);  --card-bd: rgba(246, 242, 239, .14);  --card-shadow: none;
}

[data-surface="deep"] {
  --tex: var(--tex-canvas);
  --tex-wash: rgba(12, 42, 30, .84);
  --bg: var(--c-deep);
  --text-1: var(--c-cream-1);  --text-2: var(--c-cream-2);  --text-3: var(--c-cream-3);
  --accent: var(--c-lime);
  --rule: rgba(246, 242, 239, .18);
  --rule-strong: rgba(246, 242, 239, .46);
  --btn-bg: var(--c-cream-1);    --btn-fg: var(--c-deep);     --btn-bg-hover: var(--c-paper);
  --btn2-fg: var(--c-cream-1);   --btn2-bd: rgba(246, 242, 239, .46);  --btn2-bg-hover: rgba(246, 242, 239, .12);
  --chip-fg: var(--c-cream-2);   --chip-bd: rgba(246, 242, 239, .34);
  --field-bg: rgba(246, 242, 239, .06);  --field-fg: var(--c-cream-1);  --field-bd: rgba(246, 242, 239, .32);
  --tint: rgba(246, 242, 239, .06);
  --card-bg: var(--c-forest);  --card-bd: rgba(246, 242, 239, .10);  --card-shadow: none;
}

[data-surface="forest"] {
  --tex: var(--tex-canvas);
  --tex-wash: rgba(17, 62, 44, .82);
  --bg: var(--c-forest);
  --text-1: var(--c-cream-1);  --text-2: var(--c-cream-2);  --text-3: var(--c-cream-3);
  --accent: var(--c-lime);
  --rule: rgba(246, 242, 239, .20);
  --rule-strong: rgba(246, 242, 239, .48);
  --btn-bg: var(--c-cream-1);    --btn-fg: var(--c-deep);     --btn-bg-hover: var(--c-paper);
  --btn2-fg: var(--c-cream-1);   --btn2-bd: rgba(246, 242, 239, .48);  --btn2-bg-hover: rgba(246, 242, 239, .12);
  --chip-fg: var(--c-cream-2);   --chip-bd: rgba(246, 242, 239, .36);
  /* Fields on forest are SOLID CREAM, not a translucent dark well. This is the
     reference's own treatment (measured: bg #F6F2EF, 1px solid #0E3A29, radius
     15px) and it is also the more legible of the two — a .07-alpha cream well on
     green gives the typed value a ~#1B4636 ground, where cream gives it ink on
     paper. The enquiry form is the only place fields land on this surface. */
  --field-bg: var(--c-cream-1);  --field-fg: var(--c-ink-1);  --field-bd: rgba(12, 42, 30, .30);
  --tint: rgba(246, 242, 239, .07);
  --card-bg: var(--c-moss);  --card-bd: rgba(246, 242, 239, .12);  --card-shadow: none;
}

/* "media" = text sitting on photography. Identical role set to deep, because
   the scrim below guarantees a deep-green ground no matter what the photo does.
   Text over an image NEVER trusts the image. */
[data-surface="media"] {
  --tex: none;
  --tex-wash: none;
  --bg: var(--c-deep);
  --text-1: var(--c-cream-1);  --text-2: var(--c-cream-2);  --text-3: var(--c-cream-3);
  --accent: var(--c-lime);
  --rule: rgba(246, 242, 239, .26);
  --rule-strong: rgba(246, 242, 239, .55);
  --btn-bg: var(--c-cream-1);    --btn-fg: var(--c-deep);     --btn-bg-hover: var(--c-paper);
  --btn2-fg: var(--c-cream-1);   --btn2-bd: rgba(246, 242, 239, .62);  --btn2-bg-hover: rgba(246, 242, 239, .16);
  --chip-fg: var(--c-cream-1);   --chip-bd: rgba(246, 242, 239, .46);
  --field-bg: rgba(12, 42, 30, .40);  --field-fg: var(--c-cream-1);  --field-bd: rgba(246, 242, 239, .40);
  --tint: rgba(246, 242, 239, .10);
  --card-bg: var(--c-forest);  --card-bd: rgba(246, 242, 239, .14);  --card-shadow: none;
}

/* ==========================================================================
   3. BASE
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; overflow-x: hidden; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  font-family: var(--sans);
  font-size: 1rem;              /* 16px floor for body copy */
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: .01em;
  background: var(--c-paper);
  color: var(--c-ink-1);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, video, svg { display: block; max-width: 100%; }
img, video { height: auto; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; }

::selection { background: var(--c-lime); color: var(--c-deep); }

:focus-visible {
  outline: 3px solid var(--c-lime);
  outline-offset: 2px;
}
/* On light surfaces a lime ring is weak — use the ink instead. */
[data-surface="paper"] :focus-visible,
[data-surface="cream"] :focus-visible,
[data-surface="sand"] :focus-visible,
[data-surface="lime"] :focus-visible { outline-color: var(--c-deep); }

.skip {
  position: absolute; left: 50%; top: -100px; z-index: 200;
  transform: translateX(-50%);
  padding: 12px 22px;
  background: var(--c-deep); color: var(--c-cream-1);
  font-family: var(--mono); font-size: .8125rem; text-transform: uppercase; letter-spacing: .14em;
}
.skip:focus { top: 8px; }

.vh {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* ==========================================================================
   4. SURFACE APPLICATION + TEXT ROLES
      Flat specificity. No component below ever names a colour.
   ========================================================================== */
[data-surface] {
  background-color: var(--bg);
  /* Colour wash FIRST, texture behind it — the site-wide pattern from
     styles.css. The wash alpha keeps the rendered ground near --bg, which is
     what lets the texture be decorative without touching contrast. Verified by
     measuring rendered pixels, not by assuming. */
  background-image: linear-gradient(var(--tex-wash, transparent), var(--tex-wash, transparent)), var(--tex, none);
  background-repeat: no-repeat, no-repeat;
  background-position: center top, center center;
  /* cream-glow/green-canvas are directional washes, NOT tileable patterns —
     repeating them produced hard vertical seams down the page. They get stretched
     once per surface instead, exactly as styles.css uses them on body.home.
     Note: `background-attachment: fixed` must not be used here. It anchors to the
     viewport, so the texture re-renders at every scroll position, is unsupported
     on iOS Safari, and wrecks scroll performance. */
  background-size: cover, cover;
  color: var(--text-1);
}
/* A surface nested inside another must not re-paint the texture on top of its
   parent's — it only re-binds the text roles. */
[data-surface] [data-surface] { background-image: none; }

/* Headings are NOT given their own `[data-surface] h1..h6 { color }` rule.
   They inherit --text-1 from the surface already, and such a rule would carry
   specificity (0,1,1) — enough to quietly outrank any component that legitimately
   needs a different role (a serif card title, an accent number). One less rule
   that can win an argument it shouldn't be in. */

/* NOTE — do not add a blanket `[data-surface] p, [data-surface] span, ... {
   color: inherit }` "safety net" here. It looks harmless but its specificity is
   (0,1,1), which outranks BOTH [data-surface] (0,1,0) and every component role
   such as .chip / .label (0,1,0) — so it silently drags every element back up
   to body's ink and undoes the whole system. This exact rule was written during
   this rebuild and measured: .chip and .figure went ink-on-photo at 4.02:1 and
   .label lost its accent colour. `color` already inherits by default; the rule
   buys nothing and breaks everything. Descendant selectors that set `color` are
   what caused the original bug too — keep this file free of them. */

/* Explicit role utilities — the only way to step off --text-1 */
.t-1 { color: var(--text-1); }
.t-2 { color: var(--text-2); }
.t-3 { color: var(--text-3); }
.t-accent { color: var(--accent); }

/* ==========================================================================
   5. TYPOGRAPHY — base31's scale, Crestbrick's fonts
   ========================================================================== */
h1, h2, h3, h4, h5, h6 { margin: 0; font-weight: 800; letter-spacing: -.02em; }
p { margin: 0; }
/* blockquote and figure both carry a UA `margin: 1em 40px`. Left in place it
   silently narrows any quote set inside a card by 80px. */
blockquote, figure { margin: 0; }

/* Display: serif, for hero + photo-band headlines (base31 uses Ivar here) */
.display {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(2.5rem, 6.2vw, 5.25rem);
  line-height: 1.0;
  letter-spacing: -.015em;
}

/* Chapter heading: bold sans (base31 uses Denim) */
.h-chapter {
  font-family: var(--sans);
  font-weight: 800;
  font-size: clamp(2rem, 4.2vw, 3.5rem);
  line-height: 1.06;
  letter-spacing: -.03em;
}

.h-sub {
  font-family: var(--sans);
  font-weight: 800;
  font-size: clamp(1.5rem, 2.6vw, 2.25rem);
  line-height: 1.15;
  letter-spacing: -.025em;
}

/* Serif sub-head, for cards (base31 uses Ivar for card titles) */
.h-card {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(1.3rem, 1.9vw, 1.625rem);
  line-height: 1.2;
  letter-spacing: -.005em;
}

/* Uppercase mono micro-label — base31's signature */
.label {
  display: inline-block;
  font-family: var(--mono);
  font-size: .8125rem;         /* 13px — a label, not body copy */
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--accent);
}
.label--quiet { color: var(--text-3); }
.label--stack { max-width: 26ch; }

.lede {
  font-size: clamp(1.0625rem, 1.5vw, 1.1875rem);
  line-height: 1.62;
  color: var(--text-2);
  max-width: 54ch;
}

.body-copy { color: var(--text-2); }
.body-quiet { color: var(--text-3); }

.measure { max-width: var(--maxw-text); }
.measure-sm { max-width: 46ch; }

/* ==========================================================================
   6. LAYOUT
   ========================================================================== */
.wrap {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.wrap--narrow { max-width: 940px; }

.section { padding-block: var(--pad-section); }
.section--tight { padding-block: var(--pad-band); }
.section--flush-b { padding-bottom: 0; }

/* base31's signature: a thin deep band as a chapter divider */
.band-rule { height: 10px; background: var(--c-deep); }

.center { text-align: center; }
.center .lede, .center .measure, .center .measure-sm { margin-inline: auto; }

.stack { display: grid; gap: var(--s, 20px); }
.stack--tight { --s: 12px; }
.stack--loose { --s: 30px; }

/* Chapter header: base31 centres these, mono label above, heading, then lede */
.chapter-head {
  display: grid;
  gap: 20px;
  justify-items: center;
  text-align: center;
  max-width: 900px;
  margin: 0 auto clamp(44px, 6vw, 76px);
}
.chapter-head .display,
.chapter-head .h-chapter { max-width: 22ch; }

/* Left-aligned variant. redpoint sets its section heads flush left over a
   full-bleed band rather than centring them, and a centred head would fight the
   left-aligned columns of quotes underneath it. */
.chapter-head--start {
  justify-items: start;
  text-align: left;
  max-width: none;
  margin-inline: 0;
}
.chapter-head--start .h-chapter { max-width: 26ch; }
.chapter-head--start .lede { margin-inline: 0; }

/* ==========================================================================
   7. BUTTONS — base31: square, mono uppercase, glyph-prefixed
   ========================================================================== */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 48px;
  padding: 13px 26px;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  font-family: var(--mono);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .13em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background-color .18s ease, color .18s ease, border-color .18s ease;
}
.btn__glyph { font-size: .7em; line-height: 1; opacity: .9; }

.btn--primary { background: var(--btn-bg); color: var(--btn-fg); }

.btn--outline {
  background: transparent;
  color: var(--btn2-fg);
  border-color: var(--btn2-bd);
}
.btn--primary:hover,
.btn--primary:focus-visible,
.btn--outline:hover,
.btn--outline:focus-visible {
  background: var(--action-hover-bg);
  color: var(--action-hover-fg);
  border-color: var(--action-hover-bg);
}

.btn-row { display: flex; flex-wrap: wrap; gap: 12px; }
.center .btn-row { justify-content: center; }

/* Inline text link with arrow */
.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-height: var(--tap);
  font-family: var(--mono);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--text-1);
  border-bottom: 1px solid var(--rule-strong);
  transition: border-color .18s ease, gap .18s ease;
}
.link-arrow:hover,
.link-arrow:focus-visible {
  color: var(--action-hover-text);
  border-color: var(--action-hover-text);
  gap: 14px;
}

/* Outlined tag chip — base31's card tags */
.chip {
  display: inline-flex;
  align-items: center;
  padding: 6px 10px;
  border: 1px solid var(--chip-bd);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: .75rem;
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--chip-fg);
}

/* ==========================================================================
   8. NAV
   ========================================================================== */
/* Nav sits over the hero photo; the hero's own scrim guarantees its ground. */
.nav-outer {
  position: sticky;
  top: 0;
  z-index: 80;
  transition: background-color .25s ease, border-color .25s ease;
  border-bottom: 1px solid transparent;
}
.nav-outer.scrolled {
  background-color: var(--bg);
  border-bottom-color: var(--rule);
}
.nav {
  display: flex;
  align-items: center;
  gap: var(--gap);
  min-height: var(--nav-h);
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.nav__brand { display: flex; align-items: center; min-height: var(--tap); margin-right: auto; }
.nav__brand img { height: clamp(34px, 3.4vw, 44px); width: auto; }

.nav__links { display: flex; align-items: center; gap: clamp(14px, 1.8vw, 28px); }
.nav__links > a, .nav__dd-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: var(--tap);
  padding: 0;
  background: none;
  border: 0;
  font-family: var(--mono);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-1);
  cursor: pointer;
  white-space: nowrap;
}
.nav__links > a:hover,
.nav__links > a:focus-visible,
.nav__dd-btn:hover,
.nav__dd-btn:focus-visible { color: var(--action-hover-text); }
.nav__caret { font-size: .6em; opacity: .75; }

.nav__dd { position: relative; }
.nav__dd-menu {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 5;
  display: none;
  min-width: 290px;
  padding: 8px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--bg);
  box-shadow: var(--shadow-menu);
}
.nav__dd.open .nav__dd-menu { display: block; }
.nav__dd-menu a {
  display: block;
  padding: 12px 14px;
  min-height: var(--tap);
  font-size: .9375rem;
  color: var(--text-2);
}
.nav__dd-menu a:hover,
.nav__dd-menu a:focus-visible {
  background: var(--action-hover-bg);
  color: var(--action-hover-fg);
}
.nav__dd-menu a:hover b,
.nav__dd-menu a:hover span,
.nav__dd-menu a:focus-visible b,
.nav__dd-menu a:focus-visible span { color: var(--action-hover-fg); }
.nav__dd-menu a b { display: block; font-weight: 700; color: var(--text-1); }
.nav__dd-menu a span {
  display: block;
  margin-top: 2px;
  font-family: var(--mono);
  font-size: .75rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-3);
}
.nav__dd-menu hr { margin: 6px 0; border: 0; border-top: 1px solid var(--rule); }

.nav__account {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap); height: var(--tap);
  border: 1px solid var(--btn2-bd);
  border-radius: var(--radius);
  color: var(--text-1);
}
.nav__account:hover,
.nav__account:focus-visible,
.nav__toggle:hover,
.nav__toggle:focus-visible {
  background: var(--action-hover-bg);
  border-color: var(--action-hover-bg);
  color: var(--action-hover-fg);
}
.nav__account svg { width: 19px; height: 19px; }

.nav__toggle {
  display: none;
  align-items: center; justify-content: center;
  width: var(--tap); height: var(--tap);      /* 44px — meets the tap minimum */
  background: none;
  border: 1px solid var(--btn2-bd);
  border-radius: var(--radius);
  color: var(--text-1);
  cursor: pointer;
}
.nav__toggle svg { width: 22px; height: 22px; }

/* ==========================================================================
   9. HERO — full-bleed media with a GUARANTEED scrim
   ========================================================================== */
.hero {
  position: relative;
  isolation: isolate;
  display: grid;
  align-items: end;
  min-height: min(100svh, 920px);
  margin-top: calc(-1 * var(--nav-h));
  padding-top: var(--nav-h);
  overflow: hidden;
}
.hero__media { position: absolute; inset: 0; z-index: 0; }
.hero__media > img, .hero__media > video {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: center 45%;
}
/* The scrim. Text over media never trusts the media: opaque enough at the
   type's own band that any photo underneath still yields AA. */
.hero::before {
  content: "";
  position: absolute; inset: 0; z-index: 1;
  /* Two layers: a shaped gradient for depth, plus a FLAT veil underneath that
     sets a hard floor. The floor is the point — the brightest thing the video
     can show (blown-out sky, measured at rgb(100,124,148)) still composites
     down to a ground the cream/lime roles clear AA against. Never let the
     scrim's weakest point be decided by the gradient alone. */
  background:
    linear-gradient(180deg,
      rgba(var(--scrim-rgb), .86) 0%, rgba(var(--scrim-rgb), .42) 24%,
      rgba(var(--scrim-rgb), .70) 54%, rgba(var(--scrim-rgb), .92) 100%),
    var(--scrim-floor);
  pointer-events: none;
}
/* Text-only hero — no photograph behind it. /about had no hero image before
   this rebuild (it used a faded monogram on the flat green), and giving it one
   would be inventing content. The full-viewport min-height exists to let a
   photograph breathe; with no photograph it is just 900px of empty ground, so
   this variant sizes to its copy instead. The ::before scrim still applies and
   is harmless — over a flat --bg it reads as a slight vertical deepening. */
.hero--plain { min-height: min(72svh, 620px); }

/* Cross-fading photo hero — /about.
   Same machinery as the .cta-slide band further down: one keyframe animation
   per frame, staggered with --slide-delay, no JavaScript and no will-change.
   The will-change omission is deliberate and is the same call made there —
   seven ~1600px photographs pinned as composited layers is real memory on a
   phone, and an opacity/transform animation is promoted for its own duration
   anyway.

   Cycle 42s / 7 frames = 6s a frame, dissolving over 1.2s.

   The one thing to preserve if you retime this: a frame does NOT start leaving
   at the moment its successor starts arriving. It holds 0.6s longer (to 15.714%
   = 6.6s) and then fades. A symmetric dissolve puts both frames near 50% opacity
   halfway through, and the scrim reads that as a flash. Holding the outgoing
   frame at full opacity until the incoming one is opaque removes the dip
   entirely — including at the 7 -> 1 wrap, where frame 1 is the earlier sibling
   and so comes up UNDER frame 7 rather than over it.

   The scale reset lives at 22%, inside the frame's own invisible stretch, so the
   snap back from 1.075 is never on screen.

   The light desaturation is doing real work here: the seven frames come from
   different rooms — warm wood panelling, a blue-lit stage, a neon sign — and at
   full saturation the band visibly changes temperature every six seconds. */
.hero-slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 45%;
  opacity: 0;
  filter: saturate(.9) contrast(.98);
  animation: about-hero-slide 42s linear var(--slide-delay, 0s) infinite;
}
.hero-slide:nth-child(2) { --slide-delay: 6s; }
.hero-slide:nth-child(3) { --slide-delay: 12s; }
.hero-slide:nth-child(4) { --slide-delay: 18s; }
.hero-slide:nth-child(5) { --slide-delay: 24s; }
.hero-slide:nth-child(6) { --slide-delay: 30s; }
.hero-slide:nth-child(7) { --slide-delay: 36s; }

@keyframes about-hero-slide {
  0%      { opacity: 0; transform: scale(1); }
  2.857%  { opacity: 1; }                        /* 1.2s — faded up */
  15.714% { opacity: 1; }                        /* 6.6s — holds past the handover */
  18.571% { opacity: 0; transform: scale(1.075); }
  22%     { opacity: 0; transform: scale(1); }   /* reset, off screen */
  100%    { opacity: 0; transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  /* No zoom and no dissolve — the band keeps the first frame as a still photo. */
  .hero-slide { animation: none; }
  .hero-slide:first-child { opacity: 1; }
}

.hero__panel {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding: clamp(80px, 12vh, 150px) var(--gutter) clamp(40px, 6vh, 64px);
  display: grid;
  gap: 22px;
  justify-items: center;
  text-align: center;
}
.hero__panel .display { max-width: 20ch; }
.hero__panel .lede { max-width: 52ch; color: var(--text-2); }
.hero__footnote {
  margin-top: clamp(22px, 4vh, 48px);
  padding-top: 18px;
  border-top: 1px solid var(--rule);
  width: min(100%, 620px);
  font-family: var(--mono);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--text-2);
}

/* Homepage hero — the eyebrow pair, and where the cluster sits in the video.
   Both changes are scoped to the modifier: .hero__footnote is shared with the
   about/malaysia/singapore/singapore-commercial/uk heroes, which keep the
   spacing above.

   1. VERTICAL ANCHOR. `align-items: center` is the whole change, and it centres
      the cluster in the video the eye actually sees rather than the video's
      geometric middle. Those differ: the hero pulls itself up under the nav with
      a negative margin and pays it back as `padding-top: var(--nav-h)`, so the
      content box starts at the nav's lower edge. On this page that edge is a
      hard one — index.html puts data-surface="media" on .nav-outer, and
      `[data-surface]` (§4) paints background-color, so the nav band is opaque
      from the first paint and hides the top ~78px of the frame. Centring in the
      content box therefore lands on the midpoint of the visible video, which is
      what was asked for. Compensating the padding to reach the element's true
      centre was tried and is wrong — it sits the cluster ~39px high, because
      half the offset it corrects for is behind an opaque band.

   2. THE PAIR READS AS ONE UNIT. Both lines were already 13px mono at weight
      500, but they did not look it: .hero__footnote sets no line-height, so it
      inherited body's 1.6 against .label's 1.35, and its tracking was .18em
      against .16em. Equalising both — and taking the leading to 1.2 — is what
      makes the two measure the same. Spacing goes from 70px above the hairline
      and 18px below it to 16px on each side; the -6px cancels 6 of the panel's
      22px row gap so the two sides actually match rather than reading 22/16.
      Colour is the one difference left standing, and it is the hierarchy:
      accent eyebrow, cream markets line. */
.hero--centered { align-items: center; }
.hero--centered .hero__panel { padding-block: clamp(40px, 6vh, 64px); }
.hero--centered .label,
.hero--centered .hero__footnote { line-height: 1.2; letter-spacing: .16em; }
.hero--centered .hero__footnote { margin-top: -6px; padding-top: 16px; }

/* ==========================================================================
   10. HAIRLINE COLUMN GRID — base31 divides columns with rules, not cards
   ========================================================================== */
.cols {
  display: grid;
  grid-template-columns: repeat(var(--n, 3), minmax(0, 1fr));
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}
.cols > * {
  padding: clamp(28px, 3.4vw, 44px) clamp(20px, 2.4vw, 34px);
  border-right: 1px solid var(--rule);
}
.cols > *:last-child { border-right: 0; }

.col { display: grid; gap: 14px; align-content: start; }
.col__n {
  font-family: var(--mono);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .16em;
  color: var(--accent);
}
.col p { color: var(--text-2); font-size: 1rem; }

/* Stat columns */
.stat__n {
  font-family: var(--sans);
  font-weight: 800;
  font-size: clamp(2.5rem, 5.2vw, 4rem);
  line-height: 1;
  letter-spacing: -.035em;
  color: var(--text-1);
  white-space: nowrap;
}
.stat__cap {
  margin-top: 12px;
  font-size: 1rem;
  color: var(--text-2);       /* was the 4.24:1 failure — now a real role */
  max-width: 24ch;
}

/* ==========================================================================
   11. MEDIA BANDS — full-bleed photo chapters with overlaid type
   ========================================================================== */
.band {
  position: relative;
  isolation: isolate;
  display: grid;
  align-items: center;
  min-height: clamp(440px, 52vw, 660px);
  overflow: hidden;
}
.band__media { position: absolute; inset: 0; z-index: 0; }
.band__media img { width: 100%; height: 100%; object-fit: cover; }
.band::before {
  content: "";
  position: absolute; inset: 0; z-index: 1;
  background:
    linear-gradient(180deg, rgba(var(--scrim-rgb), .58) 0%, rgba(var(--scrim-rgb), .72) 100%),
    var(--scrim-floor);
  pointer-events: none;
}
.band__inner {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding: clamp(56px, 8vw, 96px) var(--gutter);
  display: grid;
  gap: 22px;
  justify-items: center;
  text-align: center;
}
.band__inner .display { max-width: 20ch; }
.band__inner .lede { color: var(--text-2); }

/* ==========================================================================
   12. INSET FIGURE CARDS — base31's image cards with overlay label
   ========================================================================== */
.figure-grid {
  display: grid;
  grid-template-columns: repeat(var(--n, 3), minmax(0, 1fr));
  gap: var(--gap);
}
.figure {
  position: relative;
  isolation: isolate;
  display: grid;
  align-items: end;
  min-height: clamp(300px, 30vw, 420px);
  overflow: hidden;
  border-radius: var(--radius-lg);
}
.figure > img {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; object-fit: cover;
  transition: transform .6s cubic-bezier(.2, .7, .3, 1);
}
.figure > img.figure__img--top { object-position: center 10%; }
.figure::before {
  content: "";
  position: absolute; inset: 0; z-index: 1;
  /* Left as-is deliberately. A strengthened version of this ramp (plus a flat
     floor) was tried during this pass, on the strength of a reading that said the
     cream title measured 2.65:1 over Grandeur Park's lit pool. That number was
     wrong — it came from sampling the title's whole bounding box, so it was
     really the water BETWEEN the letters. Measured at the glyphs, this ramp gives
     the title 5.52:1 and the chip 7.58:1, both already past AA, and the stronger
     version only bought headroom in exchange for visibly muddier photography.
     Verify at the glyphs before touching it. */
  background: linear-gradient(180deg, rgba(var(--scrim-rgb), .12) 34%, rgba(var(--scrim-rgb), .86) 100%);
  pointer-events: none;
}
.figure:hover > img,
.figure:focus-visible > img { transform: scale(1.035); }
.figure:hover .chip,
.figure:focus-visible .chip {
  background: var(--action-hover-bg);
  border-color: var(--action-hover-bg);
  color: var(--action-hover-fg);
}
.figure__body {
  position: relative;
  z-index: 2;
  display: grid;
  gap: 10px;
  justify-items: start;
  padding: clamp(20px, 2.4vw, 32px);
}
.figure__name,
.figure:hover .figure__name,
.figure:focus-visible .figure__name,
.figure:active .figure__name,
.figure:visited .figure__name {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(1.5rem, 2.6vw, 2.25rem);
  line-height: 1.1;
  color: var(--text-1);
}
.figure--tall { min-height: clamp(380px, 42vw, 560px); }

/* ==========================================================================
   13. NUMBERED EDITORIAL GRID — one flat brand-colour block per item

   Used by the values chapter on /about. Each card is its OWN [data-surface],
   and that is what gives it its colour — the component never names one.
   Writing `background: var(--c-lime)` here instead would paint the ground and
   leave the text roles bound to the SECTION's surface, so the copy would come
   out cream-on-lime. The surface system exists precisely to make that
   impossible; this component is the payoff.

   Nesting also means `[data-surface] [data-surface] { background-image: none }`
   applies, so the cards are flat colour with no texture laid over the band's —
   which is what the reference shows.

   Cards stretch to a common height per row (grid default), so a short value
   and a long one still square off against each other.
   ========================================================================== */
.value-grid {
  display: grid;
  grid-template-columns: repeat(var(--n, 4), minmax(0, 1fr));
  gap: var(--gap);
}
.value {
  display: grid;
  gap: 12px;
  align-content: start;
  padding: clamp(22px, 2.3vw, 32px);
  border-radius: var(--radius-lg);
  /* For one card in practice: white separates from the sand band at only
     1.22:1, so the paper card needs the lift to read as a card at all. On the
     lime, moss and deep cards it is invisible and costs nothing. */
  box-shadow: 0 2px 12px -6px rgba(12, 42, 30, .2);
}
.value__n {
  font-family: var(--mono);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--accent);
}
/* Bold sans, as in the reference — not the serif .h-card the other cards use. */
.value__title {
  font-family: var(--sans);
  font-weight: 800;
  font-size: clamp(1.375rem, 2vw, 1.75rem);
  line-height: 1.15;
  letter-spacing: -.02em;
  color: var(--text-1);
}
.value p { color: var(--text-2); }

@media (max-width: 1024px) {
  /* !important because --n is set per instance as an inline style, which beats
     any rule in this file. Same trap as .cols and .pod-grid. */
  .value-grid { --n: 2 !important; }
}
@media (max-width: 560px) {
  /* Two-up at 380px leaves ~122px of content inside the padding — narrower
     than the copy can set. One per row below this. */
  .value-grid { --n: 1 !important; }
}

/* ==========================================================================
   HOW WE WORK — light panel, numbered rows with a thumbnail
      Layout from the supplied reference: a pale panel lifted off the dark band,
      a heading with a call-to-action opposite it, then hairline-divided rows of
      [thumbnail] [number] [title + copy].

      Replaces the 2x2 .num-grid this section used to be. That grid gave the four
      steps equal visual weight in a block; the rows read as a sequence, which is
      what "01, 02, 03, 04" is claiming.
   ========================================================================== */
.panel {
  padding: clamp(26px, 3.6vw, 56px);
  border-radius: var(--radius-lg);
}
/* Flush variant — the chapter sits directly on the band instead of in a card.
   The card version put a white panel on the green band, which cut the page in
   two right where the feature section above it hands over; without it the dark
   ground runs straight through and the row hairlines do the dividing. No
   [data-surface] on the element either, so it inherits the band's text roles
   rather than re-binding to ink. */
.panel--flush { padding: 0; border-radius: 0; }
.panel__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: clamp(16px, 3vw, 48px);
  align-items: end;
  padding-bottom: clamp(24px, 3vw, 40px);
}
.panel__head .h-chapter { max-width: 24ch; margin-top: 14px; }
.panel__head .lede { margin-top: 16px; }
/* The reference sets this link underlined and hard against the heading's
   baseline, which is why the head is a two-column grid aligned to `end`. */
.panel__cta {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding-bottom: 6px;
  border-bottom: 1px solid var(--rule-strong);
  font-family: var(--mono);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-1);
  white-space: nowrap;
  transition: border-color .18s ease, gap .18s ease;
}
.panel__cta:hover,
.panel__cta:focus-visible {
  color: var(--action-hover-text);
  border-color: var(--action-hover-text);
}

/* Column positions are taken off the reference, measured as a share of the
   panel's content width rather than eyeballed:

     thumbnail   0    -> 15.7%
     open span   15.7 -> 42.1%     <- the reference's signature; it is what makes
     number      42.1 -> 50.0%        this read as an editorial index, not a list
     title/copy  50.0 -> 100%

   Hence column-gap:0 — the gaps ARE the columns, so the four edges land on those
   percentages instead of drifting by whatever the gap happens to be. At the
   1360px wrap that resolves to 200 / 372 / 108 / 680.

   The thumbnail is a flat 15.7% and not a vw-based clamp. A percentage track in
   grid resolves against the grid container's CONTENT box, which is what the
   reference proportion was measured against; `15.7vw` resolves against the
   viewport, so once the 1360px wrap and its gutters are in play it lands at
   16.3% of the content — close, but not the number. A percentage needs no cap
   either: the wrap is capped, so the column tops out at 206px on its own.

   It was capped at 200px while the sources were 300px-wide originals. They are
   800px exports now, so quality no longer limits the size. Device pixels
   available at the 206px maximum: ~3.9x desktop for the 3:2 photographs, ~3.2x
   for the 16:9 one (it is `cover`-cropped on the width, so its height is the
   binding axis). Going bigger means re-exporting past 800px — and note the
   MOBILE tile is the real constraint, not this one: it is 320x213 there against
   206x137 here, so mobile sees ~2.5x where desktop sees ~3.9x. */
.steps {
  margin: 0;
  padding: 0;
  list-style: none;
  /* Closes the sequence — the reference rules under its last row too. */
  border-bottom: 1px solid var(--rule);
}
.step {
  display: grid;
  grid-template-columns:
    15.7%                         /* thumbnail */
    minmax(0, 1fr)                /* open span */
    clamp(56px, 8vw, 108px)       /* number */
    minmax(0, 50%);               /* title + copy */
  column-gap: 0;
  align-items: start;
  padding-block: clamp(24px, 3vw, 40px);
  /* A rule above every row, as in the reference. An earlier pass ruled only
     rows 1 and 3 to avoid a "ruled table" look; that was solved by the open
     span and the taller row padding, not by removing the rules — with the
     columns this far apart the hairlines are what tie a row together. */
  border-top: 1px solid var(--rule);
}
/* Explicit placement: column 2 is deliberately empty and no element may drift
   into it. */
.step__media { grid-column: 1; }
.step__n     { grid-column: 3; }
.step__body  { grid-column: 4; }

/* 3:2 matches the sources' own aspect, so the crop takes the whole frame instead
   of cutting the sides off. */
.step__media {
  aspect-ratio: 3 / 2;
  overflow: hidden;
  border-radius: 12px;
  background: var(--tint);
}
.step__media > img { width: 100%; height: 100%; object-fit: cover; }
.step__n {
  justify-self: start;
  /* Sits the numeral's baseline on the title's. The title's line box is taller,
     so without this the number rides ~4px high. */
  padding-top: 4px;
  font-family: var(--mono);
  font-size: clamp(1.0625rem, 1.55vw, 1.3125rem);
  font-weight: 500;
  line-height: 1.15;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.step__title {
  font-family: var(--sans);
  font-weight: 600;
  font-size: clamp(1.1875rem, 1.8vw, 1.5rem);
  line-height: 1.25;
  letter-spacing: -.015em;
  color: var(--text-1);
}
.step__body { display: grid; gap: 14px; }
/* 56ch is the reference's copy measure carried across proportionally (36% of its
   content width). Note the size is NOT: the reference sets this copy at ~14px
   and this site holds body copy at the 16px floor, so the same measure reads as
   fewer words per line here. The floor wins — it is a project rule. */
.step__body p { color: var(--text-2); max-width: 56ch; }

/* Proof strip */
.proof {
  display: grid;
  gap: 18px;
  justify-items: start;
  margin-top: clamp(52px, 7vw, 90px);
  padding-top: clamp(30px, 4vw, 46px);
}
.proof p { color: var(--text-2); max-width: 76ch; }

/* ==========================================================================
   FEATURE — full-bleed photograph with the brand's shaped panel over it

   Rebuilt to follow the reference section ("Find Your Dream Home With
   Crestbrick") as closely as the type allows. Measured off it over CDP rather
   than eyeballed:

     * The photograph is the BAND's own background, edge to edge — not a rounded
       card sitting inside the grid. The reference's section carries it as a
       background image and leaves its left column empty so the photo shows
       through; the band is only as tall as the panel plus ~45px.
     * The panel is floated right, 43.7% of the row wide, 140px clear of the
       right edge, and its heading is set at 42px — smaller than this page's
       other chapter heads, because it is a narrow column, not a full measure.
     * Its silhouette is the Crestbrick leaf: an S-curve scooped out of the
       top-left and bottom-right, ordinary rounded corners on the other two.

   The reference ships that silhouette as a 1960x3328 PNG stretched to the
   column. Here it is traced from that PNG to a pair of corner paths (verified
   at IoU 0.998 against it) and applied as a mask, which costs ~1.1KB instead
   of 10MB, recolours with the surface instead of hard-coding #134428, and
   never resamples.

   Why the mask is built from four layers rather than one stretched shape: the
   scoops must stay a FIXED size. A single shape scaled to the box makes the
   scoop depth a function of how many lines the copy runs to, and the panel's
   top padding — which exists purely to clear the top-left scoop — would have to
   chase it. Two rectangles cover everything except the two corner boxes, and
   the two traced corners fill in what belongs to the shape inside them:

       rect A ---> x >= S, y <= 100%-S      (leaves the TL and BR boxes out)
       rect B ---> x <= 100%-S, y >= S
       leaf TL --> the shape's share of the TL box
       leaf BR --> the shape's share of the BR box

   Composited with the default `add`, so no mask-composite support is needed
   (unlike the notch on hero-option-2, which subtracts and does need it). The
   rectangles overrun by 1px so their edge never lands mid-pixel against a
   corner layer and leave a hairline seam.
   ========================================================================== */
.feature {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  /* Height of the photograph's own strip once the layout stacks — see the
     max-width:900px block. */
  --m-photo: clamp(150px, 46vw, 280px);
}
/* The band is the panel plus a margin, exactly as the reference sets it. A full
   pad-section here would put ~500px of dead photograph above and below it. */
.feature.section { padding-block: clamp(26px, 3.2vw, 46px); }

/* The photograph. Behind the panel rather than beside it, so it is the ground
   for the whole band; the panel is opaque, so no text ever depends on it.

   The negative left inset is what keeps the subject clear of the panel. The
   panel's left edge lands at ~52% of the band at every desktop width (measured
   at 1280/1440/1920), and the photograph's family sits at 30-53% of its own
   width, so with a flush box the group runs straight under the panel. Widening
   the box to the LEFT is the only lever that moves the subject left: the image
   is `cover`, and at every desktop width the band is wider in ratio than the
   file, so the fit is height-cropped and object-position's X does nothing at
   all. Shifted, the family reads at 19-46% of the band — clear of the panel by
   ~80px at 1440 — at the cost of the leftmost 13% of the frame, which is empty
   house wall. .feature already clips (overflow:hidden), so nothing bleeds. */
.feature__bg { position: absolute; inset: 0 0 0 -13%; z-index: -1; }
.feature__bg > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 46%;
}

/* Opt OUT of that shift, for a photograph whose subject is too wide to shift.
   The -13% above buys clearance by making the visible strip left of the panel
   NARROWER — the box grows, so the same 0-52% of the band covers less of the
   file. Measured against the band it lands in: flush shows 0-52% of the file,
   -13% shows 11.5-57.5%, i.e. 52% of the frame against 46%.

   That is the right trade for a subject occupying a third of its frame, which
   is what /about's CrestCARE photograph used to be. It is the wrong one for the
   Property Experience Centre group shot that replaced it: ~30 people spanning
   56% of the frame. 56 will not fit in either window, so somebody is always
   covered — but at -13% the frame's LEFT edge slices through the people
   standing there, while flush keeps that edge whole and lets the right of the
   group pass behind the panel's curve instead. One reads as a mistake, the
   other as depth.

   Scoped rather than changed in place because index.html's .feature band is
   tuned to the -13%, and its Base31 version is still in the history waiting to
   come back. */
.feature--wide-group .feature__bg { inset: 0; }

.feature__panel {
  /* Scoop width. The reference's is 206px against a 490px-wide panel; the ratio
     is held down the scale rather than the absolute value, so the shape reads
     the same on a phone. --scoop-h keeps the traced 393.9:420.8 box ratio, and
     --swoop is the same trace's other two corners (88px at the reference size,
     i.e. .44 of the scoop). */
  --scoop:   clamp(78px, 13.6vw, 200px);
  --scoop-h: calc(var(--scoop) * .936);
  --swoop:   calc(var(--scoop) * .44);

  position: relative;
  z-index: 2;
  width: 40%;
  min-width: 320px;
  max-width: 520px;
  margin-left: auto;
  margin-right: clamp(0px, 7vw, 100px);
  /* Both vertical pads are cut from the scoops, not chosen. Top clears the
     top-left one outright — that is the dead area the reference fills with the
     monogram. Bottom clears the bottom-right one where it matters: the last row
     is the button, which sits bottom-LEFT and is never in the way, but the
     paragraph above it runs the full measure, and the traced curve starts eating
     into the right edge about a fifth of the way down its box. Half the scoop's
     height plus a line of air puts that paragraph clear of it — without this the
     last two or three words are masked away. */
  padding: calc(var(--scoop-h) + clamp(8px, 1vw, 16px)) clamp(26px, 2.6vw, 40px) calc(var(--scoop-h) * .5 + clamp(14px, 1.6vw, 22px)) clamp(26px, 2.8vw, 42px);
  display: grid;
  gap: 18px;
  align-content: start;
  border-radius: 0 var(--swoop) 0 var(--swoop);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M100 0Q99.74 6.74 99.08 11.24Q98.42 15.73 97.76 18.54Q97.11 21.35 95.39 26.26Q93.68 31.18 91.84 34.97Q90 38.76 87.89 42.13Q85.79 45.51 81.45 50.7Q77.11 55.9 74.34 58.43Q71.58 60.96 68.55 63.2Q65.53 65.45 61.05 67.98Q56.58 70.51 53.95 71.63Q51.32 72.75 45 74.44Q38.68 76.12 29.34 76.83Q20 77.53 16.32 78.93Q12.63 80.34 10.53 81.88Q8.42 83.43 6.58 85.53Q4.74 87.64 3.29 90.31Q1.84 92.98 0.92 96.49L0 100L100 100Z' fill='%23000'/%3E%3C/svg%3E"), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M0 100Q0.26 93.26 0.92 88.76Q1.58 84.27 2.24 81.46Q2.89 78.65 4.61 73.74Q6.32 68.82 8.16 65.03Q10 61.24 12.11 57.87Q14.21 54.49 18.55 49.3Q22.89 44.1 25.66 41.57Q28.42 39.04 31.45 36.8Q34.47 34.55 38.95 32.02Q43.42 29.49 46.05 28.37Q48.68 27.25 55 25.56Q61.32 23.88 70.66 23.17Q80 22.47 83.68 21.07Q87.37 19.66 89.47 18.12Q91.58 16.57 93.42 14.47Q95.26 12.36 96.71 9.69Q98.16 7.02 99.08 3.51L100 0L0 0Z' fill='%23000'/%3E%3C/svg%3E"), linear-gradient(#000, #000), linear-gradient(#000, #000);
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M100 0Q99.74 6.74 99.08 11.24Q98.42 15.73 97.76 18.54Q97.11 21.35 95.39 26.26Q93.68 31.18 91.84 34.97Q90 38.76 87.89 42.13Q85.79 45.51 81.45 50.7Q77.11 55.9 74.34 58.43Q71.58 60.96 68.55 63.2Q65.53 65.45 61.05 67.98Q56.58 70.51 53.95 71.63Q51.32 72.75 45 74.44Q38.68 76.12 29.34 76.83Q20 77.53 16.32 78.93Q12.63 80.34 10.53 81.88Q8.42 83.43 6.58 85.53Q4.74 87.64 3.29 90.31Q1.84 92.98 0.92 96.49L0 100L100 100Z' fill='%23000'/%3E%3C/svg%3E"), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M0 100Q0.26 93.26 0.92 88.76Q1.58 84.27 2.24 81.46Q2.89 78.65 4.61 73.74Q6.32 68.82 8.16 65.03Q10 61.24 12.11 57.87Q14.21 54.49 18.55 49.3Q22.89 44.1 25.66 41.57Q28.42 39.04 31.45 36.8Q34.47 34.55 38.95 32.02Q43.42 29.49 46.05 28.37Q48.68 27.25 55 25.56Q61.32 23.88 70.66 23.17Q80 22.47 83.68 21.07Q87.37 19.66 89.47 18.12Q91.58 16.57 93.42 14.47Q95.26 12.36 96.71 9.69Q98.16 7.02 99.08 3.51L100 0L0 0Z' fill='%23000'/%3E%3C/svg%3E"), linear-gradient(#000, #000), linear-gradient(#000, #000);
  -webkit-mask-size: var(--scoop) var(--scoop-h), var(--scoop) var(--scoop-h),
                     calc(100% - var(--scoop) + 1px) calc(100% - var(--scoop-h) + 1px),
                     calc(100% - var(--scoop) + 1px) calc(100% - var(--scoop-h) + 1px);
          mask-size: var(--scoop) var(--scoop-h), var(--scoop) var(--scoop-h),
                     calc(100% - var(--scoop) + 1px) calc(100% - var(--scoop-h) + 1px),
                     calc(100% - var(--scoop) + 1px) calc(100% - var(--scoop-h) + 1px);
  -webkit-mask-position: left top, right bottom, right top, left bottom;
          mask-position: left top, right bottom, right top, left bottom;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
/* 42px at the reference's own breakpoint. The page's h-chapter clamp tops out at
   56px, which in a 420px measure ran this heading to five lines. */
.feature__panel .h-chapter { font-size: clamp(1.75rem, 2.9vw, 2.625rem); max-width: 16ch; }
.feature__panel p { color: var(--text-2); }
.feature__panel .lede { color: var(--text-1); max-width: none; }
/* The monogram lives in the cleared area above the copy, right of the scoop —
   the same place the reference puts it. */
.feature__panel .mono-mark--panel {
  top: clamp(18px, 2.4vw, 34px);
  right: clamp(18px, 2.4vw, 34px);
}

/* Mirrored variant — photo weighted right, panel on the left. Kept for reuse;
   with a full-bleed ground it is only the panel that moves. */
.feature--flip .feature__panel {
  margin-left: clamp(0px, 7vw, 100px);
  margin-right: auto;
  border-radius: var(--swoop) 0 var(--swoop) 0;
  -webkit-mask-position: right top, left bottom, left top, right bottom;
          mask-position: right top, left bottom, left top, right bottom;
}

@media (max-width: 900px) {
  /* Stacked: the photograph becomes a band of its own above the panel, which is
     what the reference's mobile column does, and the panel takes the full
     measure underneath it. The ground stops being full-bleed here — it is capped
     to the strip plus the overlap, so the photo is not 80% hidden behind an
     opaque panel. At 380px that strip is ~236px against a 380px width, which is
     close enough to the file's own 3:2 that almost none of the frame is cropped
     and the family stays in it. */
  .feature.section { padding-block: var(--m-photo) clamp(26px, 6vw, 44px); }
  .feature__bg { inset: 0 0 auto; height: calc(var(--m-photo) + clamp(58px, 16vw, 110px)); }
  .feature__bg > img { object-position: 40% 55%; }
  .feature__panel {
    /* Re-cut against the panel's own width rather than the viewport's. Once the
       panel goes full-measure, 13.6vw is only ~14% of it and the scoop reads as a
       nick instead of the shape; ~23% keeps the silhouette recognisable, which is
       the same share it holds at desktop. */
    --scoop: clamp(78px, 22vw, 190px);
    width: 100%;
    min-width: 0;
    max-width: none;
    margin-inline: 0;
  }
  .feature--flip .feature__panel { margin-inline: 0; }
}

/* ==========================================================================
   14. TESTIMONIALS — a horizontal rail of cards, no background band

      Layout follows the supplied reference: a light ground, equal-height cards
      in a row that bleeds off the right edge so it reads as scrollable, and real
      event photography interleaved between the quotes.

      Three things about it are deliberate:

      * The rail is a NATIVE scroll container, not a transform-animated track.
        Touch swipe, trackpad, shift+wheel, keyboard arrows, and the browser's own
        scroll-into-view for focused cards all work for free, and the auto-advance
        below just calls scrollBy() on it. A translateX marquee would have fought
        every one of those.
      * Cards are EQUAL height, set by the longest quote, with the attribution
        pinned to the bottom. These reviews run 12 to 95 words, so the short ones
        carry whitespace above their signature — which is what the reference does
        with its own short card, not an accident.
      * The reference sets its attribution in a script face. This page ships only
        DM Serif Display and Manrope, so that contrast is carried by DM Serif
        italic against Manrope body copy instead of by adding a third font.
   ========================================================================== */
.testi-rail { position: relative; }

/* The viewport is full-bleed: it escapes .wrap so cards run to the window edge
   and the row visibly continues. Padding-inline re-establishes the text column's
   left edge, so the first card lines up under the heading. */
.testi-rail__vp {
  /* Distance from the window edge to the text column, so the first card starts
     under the heading rather than at the window edge. (This used to be mirrored
     as scroll-padding, because snap positions measure against scroll padding
     rather than box padding — no longer needed now that snap is gone.) */
  --rail-inset: max(var(--gutter), calc((100vw - var(--maxw)) / 2 + var(--gutter)));
  overflow-x: auto;
  overflow-y: hidden;
  /* No scroll-snap. It was right for a rail that stepped card to card, but it
     fights a continuous drift — every frame the drift writes a new scrollLeft,
     snap yanks it back to the nearest card edge, and the motion judders. */
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding-inline: var(--rail-inset);
  padding-block: 4px;
}
.testi-rail__vp::-webkit-scrollbar { display: none; }
/* Focusing the rail must be visible — it is a keyboard-scrollable region. */
.testi-rail__vp:focus-visible { outline: 3px solid var(--accent); outline-offset: 4px; }

.testi-rail__track {
  display: flex;
  gap: clamp(14px, 1.5vw, 22px);
  margin: 0;
  padding: 0;
  list-style: none;
  width: max-content;
}

.testi-rail__item {
  flex: 0 0 auto;
  scroll-snap-align: none;
  /* Width is set by the longest review, not by taste: at 358px its 676 characters
     ran to 22 lines and dragged the whole row to 745px tall. 396px gives ~41
     characters a line, which lands the row near the reference's card proportion
     (~0.71 w/h). */
  width: clamp(272px, 76vw, 396px);
  /* A floor only. The track is a flex row, so `align-items: stretch` already
     equalises every card to the tallest — this just stops a rail of nothing but
     photos from collapsing. */
  min-height: clamp(420px, 38vw, 520px);
  display: flex;
}

/* ---- Quote card ---- */
.tcard {
  flex: 1;
  display: grid;
  grid-template-rows: auto 1fr auto;
  gap: 18px;
  margin: 0;
  padding: clamp(22px, 2.2vw, 28px);
  background: var(--card-bg);
  border: 1px solid var(--card-bd);
  border-radius: var(--radius-lg);
  box-shadow: var(--card-shadow);
}
/* The reference opens each card with a small round avatar. These are Google
   reviews with no reviewer photo (and, per the attribution note in the markup, no
   permission for one), so the 5-star rating takes that position: same role in the
   layout, real information instead of a placeholder. */
.tcard__stars { color: var(--accent); font-size: .9375rem; letter-spacing: .24em; }
.tcard__quote {
  align-self: start;
  font-size: 1rem;              /* 16px floor — this is body copy, not a caption */
  line-height: 1.5;
  color: var(--text-2);
}
.tcard__by { display: grid; gap: 3px; }
.tcard__name {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: 1.3125rem;
  line-height: 1.2;
  color: var(--text-1);
}
.tcard__role {
  font-family: var(--mono);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* ---- Photo card ----
   Real event photography, carrying no overlaid text: these are tour and viewing
   photos, not video testimonials, so there is no name to sit on them and nothing
   is invented to fill the space. */
.tphoto {
  flex: 1;
  position: relative;
  margin: 0;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--tint);
}
.tphoto > img {
  width: 100%; height: 100%;
  object-fit: cover;
  /* Nudged warm and slightly down-contrast so nine phone photos from different
     rooms and light temperatures sit together instead of clashing card to card. */
  filter: saturate(.94) contrast(.97);
}

/* ---- Controls: prev/next only. A pointer user should not have to drag a
   scrollbar that is deliberately hidden. (The pause toggle that used to sit here
   was removed by request; see the note in the page's rail script about what now
   carries WCAG 2.2.2.) ---- */
.testi-rail__ctrls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-top: clamp(20px, 2.4vw, 28px);
}
.rail-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap); height: var(--tap);
  padding: 0;
  background: none;
  border: 1px solid var(--btn2-bd);
  border-radius: var(--radius-pill);
  color: var(--text-1);
  cursor: pointer;
  transition: background-color .18s ease, border-color .18s ease;
}
.rail-btn:hover,
.rail-btn:focus-visible {
  background: var(--action-hover-bg);
  border-color: var(--action-hover-bg);
  color: var(--action-hover-fg);
}
.rail-btn svg { width: 18px; height: 18px; }

/* ==========================================================================
   15. ENQUIRY FORM
   ========================================================================== */
/* The reference's "Schedule Your Free Consultation Today" block: one rounded
   card, a shade lighter than the band it sits on, holding the heading on the
   left and the form on the right, vertically centred against each other.
   Measured off it — max-width 1109px, radius 25px, asymmetric padding that runs
   heavier on the left and bottom than the top and right. The card is a nested
   [data-surface="forest"], which is what gives it its lighter green and rebinds
   the full text set; it never names a colour itself. */
.cta-card {
  max-width: 1110px;
  margin-inline: auto;
  padding: clamp(30px, 4vw, 44px) clamp(24px, 3.4vw, 40px) clamp(34px, 4.4vw, 48px) clamp(24px, 4.4vw, 60px);
  border-radius: var(--radius-lg);
  display: grid;
  grid-template-columns: minmax(0, .92fr) minmax(0, 1.08fr);
  gap: clamp(32px, 4.4vw, 64px);
  align-items: center;
}
/* The reference runs this band at 80px top and bottom and lets its footer start
   immediately after, rather than stacking a full section pad onto a full footer
   pad. Without this the card and the footer sat ~224px apart and the band read as
   two unrelated blocks. */
.section--cta {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding-block: clamp(56px, 6vw, 80px);
}
/* The band's photographic ground, and the parallax the reference gets from
   background-attachment: fixed. Done as a transform on an oversized child rather
   than a fixed background: `fixed` is ignored on iOS Safari (the image just sits
   still) and forces a full-layer repaint on every scroll frame everywhere else.
   A translate3d on its own compositing layer costs nothing and behaves the same
   on every platform. */
.cta-bg { position: absolute; inset: 0; z-index: -1; }
/* The scrim sits on the clipping wrapper, not on the moving layer, so it neither
   travels with the parallax nor zooms with the slides — one steady tone over
   whatever is behind it. It is deliberately much lighter than the .88 flat wash
   the single-photograph version used: these three photographs ARE the three
   markets, so their colour is the point, and nothing in this band reads against
   them anyway (the card is opaque forest green, and every field and label lives
   on the card). Top-to-bottom it deepens slightly, which keeps the horizon of a
   bright skyline from competing with the card's upper edge. */
.cta-bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(rgba(var(--scrim-rgb), .42), rgba(var(--scrim-rgb), .58));
}
.cta-bg__img {
  position: absolute;
  /* Taller than the band on both sides, so there is room to travel without ever
     exposing an edge. The JS moves it within +/- this inset. */
  inset: -14% 0;
  overflow: hidden;
  transform: translate3d(0, var(--par, 0px), 0);
  will-change: transform;
}

/* Three photographs cross-fading with a slow push-in, one market each. The cycle
   is 27s — 8s of hold per city, a ~1.1s dissolve between them — which is slow
   enough that the movement reads as drift rather than animation while someone is
   filling in the form beside it.
   The zoom is on the slide itself and the parallax translate is on the parent, so
   the two never fight over one transform. Each slide's scale resets between 37%
   and 42% of the cycle, i.e. entirely inside its own invisible stretch. Only a
   light desaturation, unlike the .32 the old single photo needed: nothing here
   has a hot subject colour to pull the eye off the card. */
.cta-slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  opacity: 0;
  filter: saturate(.88) contrast(.97);
  /* linear, not the default ease: the push-in should read as one steady drift,
     and an eased scale between the keyframes visibly slows in the middle of each
     hold. No will-change here on purpose — three ~2000px images pinned as
     composited layers is real memory on a phone, and a transform/opacity
     animation is promoted for its own duration anyway. */
  animation: cta-slide 27s linear var(--slide-delay, 0s) infinite;
}
/* Delay only — the framing is centred for all three on purpose. Every one of
   these files is wider in ratio than this layer is at any viewport (2.29, 1.78
   and 3.0 against a layer that runs ~1.37 at desktop and far taller on a phone),
   so `cover` crops them SIDEWAYS and the Y half of object-position is inert. What
   the visitor actually sees is the frame around the card — a ~165px gutter each
   side at 1440 plus a thin strip top and bottom — and centred, each photograph
   puts something worth seeing there: Marina Bay's towers and the Gardens' domes,
   the Petronas spires above the card, the Thames and St Paul's. */
.cta-slide:nth-child(2) { --slide-delay: 9s; }
.cta-slide:nth-child(3) { --slide-delay: 18s; }

@keyframes cta-slide {
  0%      { opacity: 0; transform: scale(1); }
  4%      { opacity: 1; }
  33.333% { opacity: 1; }
  37.333% { opacity: 0; transform: scale(1.1); }
  42%     { opacity: 0; transform: scale(1); }
  100%    { opacity: 0; transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .cta-bg__img { transform: none; }
  /* No drift and no dissolve — the band keeps Singapore as a still photograph. */
  .cta-slide { animation: none; }
  .cta-slide:first-child { opacity: 1; }
}

.cta-intro { position: relative; }
/* The reference caps this heading at 48px — smaller than the page's other chapter
   heads, because it sits beside a form rather than over a column of prose, and at
   56px it pushed to three lines and unbalanced the card. */
.cta-intro .h-chapter {
  font-size: clamp(1.875rem, 3.4vw, 3rem);
  line-height: 1.16;
  max-width: 20ch;
}
.cta-intro .lede { max-width: 42ch; }

/* ---- The interest chooser: step 1 of the enquiry ---------------------------
   These four selectors had NO rule in this file. index.html carried them in an
   inline <style> lifted from the pre-base31 homepage, still written against
   css/styles.css tokens — border: 1px solid var(--line), background var(--paper),
   color var(--ink). None of those three exist here (base31 names them --rule,
   --c-paper, --c-ink-1), and an unresolved var() in a shorthand invalidates the
   whole declaration, so the eight buttons lost their fill, their border AND their
   colour at once and rendered as bare bold text on the card. The rule belongs in
   this file, in this file's tokens.

   The treatment is the hero's own .btn--outline, token for token: transparent
   fill, a --btn2-bd hairline, --btn2-fg text, and --action-hover-bg / -fg on
   interaction, which on this surface resolves to lime on deep green. Eight solid
   cream slabs was the first attempt and it was too loud — the chooser outweighed
   the headline beside it. Ghosted, the pills recede to the card and only the one
   under the cursor comes forward, in the same lime the hero's buttons use. That
   is a reuse of the existing action language, not a new one: nothing here
   invents a colour, and a change to --action-hover-bg still moves both.
   Shape follows the same family — --radius-pill and roomier 22px flanks, since a
   999px radius needs the horizontal air. Weight drops 700 -> 600: at 700 on a
   transparent ground the labels read as headings rather than options.
   min-height is the 44px tap floor; the padding clears it on one line, but
   "Property portfolio review" is the wrapping case. */
.cta-step { transition: opacity .3s ease; }
.cta-step[hidden] { display: none; }
.cta-choices {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
  margin-top: 22px;
}
.cta-choice {
  font: inherit;
  font-weight: 600;
  font-size: .98rem;
  text-align: left;
  min-height: var(--tap);
  padding: 15px 22px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--btn2-bd);
  background: transparent;
  color: var(--btn2-fg);
  cursor: pointer;
  transition: background-color .18s ease, color .18s ease, border-color .18s ease;
}
/* :active is listed with the two, not left to a separate rule, because it is the
   only one of the three that fires on a phone — there is no hover on touch, and
   the button hides its own step on click, so the tap flash IS the confirmation
   that the choice registered. */
.cta-choice:hover,
.cta-choice:focus-visible,
.cta-choice:active {
  background: var(--action-hover-bg);
  color: var(--action-hover-fg);
  border-color: var(--action-hover-bg);
}
/* Underlined, no chrome — it is a way back, not a second action competing with
   the eight pills. */
.cta-back {
  font: inherit;
  font-size: .85rem;
  background: none;
  border: 0;
  padding: 0;
  margin-bottom: 16px;
  color: var(--text-3);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.cta-back:hover, .cta-back:focus-visible { color: var(--text-1); }

.form { display: grid; gap: 16px; }
/* .form is a grid, so a button child stretches to the full column by default and
   the pill turned into a 450px-wide slab. The reference uses a fixed-width pill
   centred under the fields (measured: 225x48). */
.form > .btn { justify-self: center; }
.form__row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.field { display: grid; gap: 7px; min-width: 0; }
.field > label {
  font-family: var(--mono);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-3);
}
.field input, .field select, .field textarea {
  width: 100%;
  min-height: 50px;
  padding: 13px 14px;
  border: 1px solid var(--field-bd);
  border-radius: var(--radius);
  background: var(--field-bg);
  color: var(--field-fg);
  font-family: var(--sans);
  font-size: 1rem;             /* 16px — no iOS zoom-on-focus */
}
.field textarea { min-height: 92px; resize: vertical; }
.field input::placeholder, .field textarea::placeholder { color: var(--text-3); }
.field .req { color: var(--accent); }
.field__optional { text-transform: none; letter-spacing: .02em; opacity: .8; }

.hp-field { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.consent {
  font-size: .875rem;          /* fine print, but on an AA-verified role */
  line-height: 1.55;
  color: var(--text-2);
}
.consent a { text-decoration: underline; text-underline-offset: 2px; }
.consent a:hover,
.consent a:focus-visible { color: var(--action-hover-text); }

/* ==========================================================================
   16. FOOTER
   ========================================================================== */
/* The reference's footer has no top pad of its own — the band above it carries
   the separation. See .section--cta. */
.footer { padding-top: clamp(40px, 5vw, 64px); }
/* The reference's footer: brand block on the left, link columns in the middle,
   and the contact block set apart on the right behind a vertical hairline
   (measured: border-left 1px rgba(235,235,235,.46), padding-left 60px). The rule
   is what makes the right-hand column read as a separate panel rather than a
   fourth list, so it earns its place. */
.footer__top {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, .82fr) minmax(0, 1.05fr);
  gap: clamp(28px, 3.4vw, 48px);
  padding-bottom: clamp(40px, 5vw, 64px);
}
.footer__col--aside {
  padding-left: clamp(24px, 3.4vw, 60px);
  border-left: 1px solid var(--rule-strong);
}
.footer__brand { display: grid; gap: 16px; align-content: start; max-width: 42ch; }
.footer__brand > a { display: inline-flex; align-items: center; min-height: var(--tap); }
.footer__brand img { height: 30px; width: auto; }
.footer__brand p { color: var(--text-2); font-size: 1rem; }
.footer__col { display: grid; gap: 12px; align-content: start; }
.footer__col h5 {
  font-family: var(--mono);
  font-size: .75rem;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--text-3);
}
.footer__col a {
  display: inline-flex; align-items: center;
  min-height: var(--tap);
  font-size: 1rem;
  color: var(--text-2);
}
.footer__col a:hover,
.footer__col a:focus-visible { color: var(--action-hover-text); }

.footer__socials { display: flex; gap: 10px; margin-top: 4px; }
.footer__socials a {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap); height: var(--tap);       /* 44px, was 38px */
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}
.footer__socials a:hover,
.footer__socials a:focus-visible {
  background: var(--action-hover-bg);
  border-color: var(--action-hover-bg);
}
.footer__socials img { width: 18px; height: 18px; }

/* The legal bar — one row, one hairline, one type treatment.
   It was two rows (.footer__trust + .footer__legal) sitting behind three
   hairlines: a rule above the trust row, between the two, and none below, so the
   eye read three bands of chrome under the columns. Inside them were four
   competing treatments at the same 1rem as the link columns above — bold ink
   licence number, a middot in its own span, body prose, and an uppercase
   letterspaced underlined link that read as a button. Nothing receded, so it all
   demanded to be read.
   Now: the facts run as one muted line, the three links group opposite them, and
   both sit at 14px. That is DELIBERATELY under this page's 16px body floor — the
   floor is for body copy, and this is microcopy carrying a copyright, a UEN and a
   licence number that nobody reads as prose. It is the same call the sitewide
   .footer__disclaimer already makes at 15.2px (see the open item in CLAUDE.md);
   if that policy lands the other way, this is one value to change.
   Total height falls from ~150px to ~78px on desktop, and the links keep their
   44px tap targets, which is what sets the bar's height rather than padding. */
.footer__legal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 4px clamp(20px, 3vw, 44px);
  padding-block: clamp(12px, 1.4vw, 17px);
  border-top: 1px solid var(--rule);
  font-size: .875rem;
  line-height: 1.55;
  color: var(--text-3);
}
/* No max-width: the flex row already caps it at whatever the link group leaves,
   and a ch cap made it wrap early and strand the last fact on its own line. */
.footer__legal-facts { margin: 0; min-width: 0; }
/* The links keep the muted-but-brighter role so the group reads as the only
   actionable thing in the bar without needing a second type style. */
.footer__legal-links { display: flex; flex-wrap: wrap; align-items: center; gap: 2px clamp(16px, 2vw, 26px); }
.footer__legal-links a {
  display: inline-flex; align-items: center; gap: 5px;
  min-height: var(--tap);
  color: var(--text-2);
}
.footer__legal-links a:hover,
.footer__legal-links a:focus-visible {
  color: var(--action-hover-text);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* ==========================================================================
   17. REVEAL (main.js adds .in)
   ========================================================================== */
.reveal { opacity: 0; transform: translateY(15px); transition: opacity .6s ease, transform .6s ease; }
.reveal.in, .reveal.is-visible, .reveal.visible { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  .figure > img { transition: none; }
}
/* .in is added by main.js. With scripting off it never arrives, and every
   revealed section would stay at opacity 0 — invisible text is the same defect
   as unreadable text, so it gets the same guarantee. */
@media (scripting: none) {
  .reveal { opacity: 1; transform: none; }
}

/* ==========================================================================
   18. RESPONSIVE — mobile-first checks at 380px
   ========================================================================== */
@media (max-width: 1024px) {
  /* The reference's open middle column needs real width to read as composition.
     Below ~1024px it is just a hole that squeezes the copy, so the row tightens
     to thumbnail / number / copy with an ordinary gap. */
  .step {
    grid-template-columns: clamp(140px, 19vw, 214px) clamp(40px, 5vw, 56px) minmax(0, 1fr);
    column-gap: clamp(18px, 2.4vw, 28px);
  }
  .step__n    { grid-column: 2; }
  .step__body { grid-column: 3; }

  .cta-card { grid-template-columns: 1fr; align-items: start; }
  .cta-choices { grid-template-columns: minmax(0, 1fr); }
  .cta-intro .h-chapter, .cta-intro .lede { max-width: none; }
  .footer__top { grid-template-columns: 1fr 1fr; }
  .footer__brand { grid-column: 1 / -1; }
  /* Once the columns wrap, a left border no longer divides anything — it just
     hangs beside a block. It becomes a top rule instead. */
  .footer__col--aside {
    padding-left: 0;
    border-left: 0;
    padding-top: clamp(24px, 4vw, 32px);
    border-top: 1px solid var(--rule-strong);
    grid-column: 1 / -1;
  }
}

@media (max-width: 900px) {
  .nav__toggle { display: inline-flex; order: 3; }
  .nav__account { order: 2; }
  .nav__links {
    order: 4;
    display: none;
    flex-direction: column;
    align-items: stretch;
    width: 100%;
    gap: 0;
    padding-block: 8px 16px;
    border-top: 1px solid var(--rule);
  }
  .nav__links.open { display: flex; }
  .nav-outer:has(.nav__links.open) { background-color: var(--bg); }
  .nav { flex-wrap: wrap; }
  .nav__links > a, .nav__dd-btn { width: 100%; padding-block: 10px; min-height: 48px; }
  .nav__dd { width: 100%; }
  .nav__dd-menu {
    position: static;
    display: none;
    width: 100%;
    min-width: 0;
    margin-top: 2px;
    border: 0;
    border-left: 1px solid var(--rule);
    box-shadow: none;
    background: transparent;
  }
  .nav__dd.open .nav__dd-menu { display: block; }

  .cols { --n: 1 !important; }
  .cols > * { border-right: 0; border-bottom: 1px solid var(--rule); }
  .cols > *:last-child { border-bottom: 0; }
  .figure-grid { --n: 2 !important; }
  .band { min-height: clamp(380px, 62vw, 520px); }
}

@media (max-width: 700px) {

  /* Thumbnail on its own row; number and title/copy share the next row so 01
     sits on the same baseline as “Personalised Guidance”. */
  .step {
    grid-template-columns: 38px minmax(0, 1fr);
    row-gap: 14px;
  }
  .step__media { grid-column: 1 / -1; grid-row: 1; width: 100%; max-width: min(100%, 320px); }
  .step__n { grid-column: 1; grid-row: 2; justify-self: start; padding-top: 2px; }
  .step__body { grid-column: 2; grid-row: 2; }
  .panel__head { grid-template-columns: minmax(0, 1fr); align-items: start; }
  .panel__cta { white-space: normal; }
  .figure-grid { --n: 1 !important; }
  .form__row { grid-template-columns: 1fr; }
  .footer__top { grid-template-columns: 1fr; }
  /* The hero sizes to its copy on a phone rather than holding a viewport.
     .hero__panel's asymmetric pad (a big top, a small bottom) is what seats the
     cluster low in the frame, which is right for .hero--slides: /about's hero is
     a photograph that has to be seen, so the copy sits out of its way.

     .hero--centered does NOT take that pad. It used to — the line was repeated
     here to beat §9's `.hero--centered .hero__panel` — and the result was a
     cluster 105px off centre on every centred hero (measured: 149px of air above
     the first ink, 44px below the last). Dropping the override lets §9's
     symmetric clamp(40px, 6vh, 64px) through, so a centred hero is centred at
     380px for the same reason it is at 1440px. */
  .hero { min-height: auto; }
  .hero__panel { padding-block: clamp(96px, 20vh, 150px) 44px; }
}

@media (max-width: 420px) {
  :root { --gutter: 16px; }
  .display { font-size: clamp(2.125rem, 11vw, 2.75rem); }
  .h-chapter { font-size: clamp(1.75rem, 8.4vw, 2.125rem); }
  .stat__n { font-size: clamp(2.125rem, 12vw, 2.75rem); }
  .label { letter-spacing: .13em; }
  .hero__footnote { letter-spacing: .12em; }
  .hero--centered .label,
  .hero--centered .hero__footnote { letter-spacing: .13em; }
}

@media print {
  .nav-outer, .testi-rail__ctrls { display: none; }
  /* A horizontal scroller prints as one visible card and eight lost ones. Unroll
     it so the whole set is on the page. */
  .testi-rail__vp { overflow: visible; padding-inline: 0; }
  .testi-rail__track { display: block; width: auto; }
  .testi-rail__item { width: auto; min-height: 0; break-inside: avoid; margin-bottom: 12px; }
  [data-surface] { background: #fff !important; color: #000 !important; }
}

/* ==========================================================================
   19. REGION PAGES — the shared spine for /singapore, /malaysia, /uk

   Every region page runs the same six chapters in the same order:

     1  hero (title)                    .hero            — media surface
     2  why [region]                    .feature / .duo  — deep / paper
     3  pod-based investing             .pod-grid        — deep
     4  project history                 .tile-grid       — paper
     5  on the ground (event photos)    .shots           — sand
     6  enquiry                         .section--cta    — deep

   Nothing here names a colour: every component draws from the surface roles
   defined in §2, so the same markup works on paper, cream, sand, deep or
   forest without a per-page override. That is the invariant that made the
   old styles.css region pages unreadable — `style="color:var(--cream)"` on a
   heading that had landed on a light band — and it must not come back.
   ========================================================================== */

/* --- .duo — heading left, prose right ------------------------------------
   The two-column chapter opener the old pages built with `.wrap.grid-2`.
   Ratio is off-centre on purpose: the heading column is a short measure
   (18ch) and the prose column carries the argument. */
.duo {
  display: grid;
  grid-template-columns: minmax(0, .86fr) minmax(0, 1.14fr);
  gap: clamp(24px, 4vw, 72px);
  align-items: start;
}
.duo > * { min-width: 0; }
.duo .h-chapter { max-width: 18ch; }
.duo .lede { max-width: 56ch; }
.duo p { color: var(--text-2); }

/* --- .pod-grid / .pod — pod-based investing ------------------------------
   Three stages, each a card on the band. Cards rather than the hairline
   .cols grid because the three are a *mechanism* (pool → select → access),
   not three equal facts, and the card edge is what lets the stage number
   sit inside its own box. */
.pod-grid {
  display: grid;
  grid-template-columns: repeat(var(--n, 3), minmax(0, 1fr));
  gap: var(--gap);
}
.pod {
  display: grid;
  gap: 14px;
  align-content: start;
  padding: clamp(22px, 2.6vw, 34px);
  border: 1px solid var(--card-bd);
  border-radius: var(--radius-lg);
  background: var(--card-bg);
}
.pod__n {
  font-family: var(--mono);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--accent);
}
.pod p { color: var(--text-2); }
/* A pod that declares its own [data-surface] takes THAT surface's ground, the way
   the .value cards in §13 do. Necessary because .pod names --card-bg, which is the
   ground for a card sitting ON a surface, not for one that IS a surface: without
   this, data-surface="lime" would paint the lime surface's --card-bg (paper) and the
   pod would come out white. Border goes transparent and §13's shadow comes in, so a
   coloured pod matches the /about values cards it is made consistent with.
   §13's rule still holds — the component never names a colour, the instance does. */
.pod[data-surface] {
  background: var(--bg);
  border-color: transparent;
  box-shadow: 0 2px 12px -6px rgba(12, 42, 30, .2);
}
.pod__foot {
  margin-top: 4px;
  padding-top: 14px;
  border-top: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* --- .sell-grid — the selling-approach chapter -------------------------
   Photograph, then copy beneath it. No overlay, so the whole frame stays visible
   and the text is ink on the section's own ground, which is where the surface
   already guarantees contrast — nothing here has to be measured against a
   photograph.

   That is the reason this reads better than the overlaid version it replaces: an
   overlay has to darken the image to protect the type, and the darkening has to
   be sized to the tallest caption, so the picture pays for the copy. Separated,
   neither constrains the other.

   3/2 is a compromise, on purpose. Two of the three photographs are 4:3 and the
   Matterport still is 16:9; 3/2 sits between them, so each is cropped by about a
   tenth rather than one of them losing a quarter of its frame. */
.sell-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(18px, 2.2vw, 32px);
  margin-top: clamp(28px, 4vw, 48px);
}
.sell-card {
  margin: 0;
  display: grid;
  gap: clamp(14px, 1.4vw, 20px);
  align-content: start;
}
.sell-card > img {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--radius-lg);
  background: var(--tint);
}
/* --- the tour card ------------------------------------------------------
   One of the three frames is the Matterport scan, and it behaves like the thing
   it depicts: the dollhouse orbits on a loop, and the frame opens the real tour.

   The loop is a PING-PONG render of 36 captured frames — forward, then back
   minus both endpoints — so it cycles with no cut to hide. A one-way orbit would
   need a seamless 360 to loop, and the model drifts in frame across a full turn.

   The live tour is NOT an iframe sitting in the page. It is a WebGL application;
   embedding it in the grid would load and run it on every visit to this page for
   a frame the size of a postcard. The iframe is built on click, inside a native
   <dialog>, and destroyed on close — so the cost is paid only by someone who
   asked for it, and nothing keeps rendering behind a closed dialog. */
.sell-card__media {
  display: block;
  position: relative;
  isolation: isolate;
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--tint);
  cursor: pointer;
  line-height: 0;
}
.sell-card__media > video {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
}
.sell-card__cue {
  position: absolute; z-index: 2;
  left: clamp(12px, 1.2vw, 16px); bottom: clamp(12px, 1.2vw, 16px);
  display: inline-flex; align-items: center; gap: 8px;
  min-height: var(--tap);
  padding: 0 18px;
  border-radius: var(--radius-pill);
  background: rgba(var(--photo-cue-rgb), .58);
  color: var(--c-cream-1);
  font-family: var(--mono); font-size: .8125rem; font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase;
  line-height: 1;
  transition: background-color .18s ease, color .18s ease;
}
.sell-card__media:hover .sell-card__cue,
.sell-card__media:focus-visible .sell-card__cue {
  background: var(--action-hover-bg);
  color: var(--action-hover-fg);
}
.sell-card__media:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }

/* The dialog. ::backdrop cannot see the page's custom properties, so its colour
   is the one literal here and it is deliberate. */
.tour-modal {
  width: min(94vw, 1200px);
  max-height: 92vh;
  padding: 0;
  border: 0;
  border-radius: var(--radius-lg);
  background: var(--c-deep);
  overflow: hidden;
}
.tour-modal::backdrop { background: rgba(6, 20, 14, .82); }
.tour-modal__frame { position: relative; aspect-ratio: 16 / 10; width: 100%; }
.tour-modal__frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.tour-modal__close {
  position: absolute; z-index: 3; top: 12px; right: 12px;
  width: var(--tap); height: var(--tap);
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0;
  border: 1px solid rgba(246, 242, 239, .5);
  border-radius: var(--radius-pill);
  background: rgba(6, 20, 14, .72);
  color: var(--c-cream-1);
  font-size: 1.25rem; line-height: 1;
  cursor: pointer;
}
.tour-modal__close:hover, .tour-modal__close:focus-visible {
  background: var(--c-lime); color: var(--c-deep); border-color: var(--c-lime);
}
@media (max-width: 700px) { .tour-modal__frame { aspect-ratio: 3 / 4; } }

.sell-card figcaption { display: grid; gap: 7px; }
.sell-card__n {
  font-family: var(--mono); font-size: .8125rem; font-weight: 500;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--accent);
}
.sell-card__title {
  font-family: var(--serif); font-weight: 400;
  font-size: clamp(1.25rem, 1.7vw, 1.5rem);
  line-height: 1.18;
  color: var(--text-1);
}
.sell-card__text { font-size: .9375rem; line-height: 1.6; color: var(--text-2); }

/* --- .tile-grid / .tile — project history -------------------------------
   auto-fill rather than a fixed --n: the UK page runs 13 projects in one
   group and Malaysia runs three, and both should fill their row. The
   `min(100%, 260px)` floor is what keeps a single tile from overflowing a
   380px viewport — a bare `minmax(260px, 1fr)` cannot shrink below 260px
   and pushes the grid wider than its container. */
.tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr));
  gap: var(--gap);
}
.tile {
  display: grid;
  grid-template-rows: auto 1fr;
  overflow: hidden;
  border: 1px solid var(--card-bd);
  border-radius: var(--radius-lg);
  background: var(--card-bg);
  box-shadow: var(--card-shadow);
}
.tile__media { aspect-ratio: 4 / 3; overflow: hidden; background: var(--tint); }
.tile__media > img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform .6s cubic-bezier(.2, .7, .3, 1);
}
.tile:hover .tile__media > img,
.tile:focus-visible .tile__media > img { transform: scale(1.04); }
.tile__body {
  display: grid;
  gap: 10px;
  align-content: start;
  padding: clamp(16px, 1.9vw, 24px);
}
.tile__meta { display: flex; flex-wrap: wrap; gap: 8px; }
.tile__name {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(1.1875rem, 1.7vw, 1.4375rem);
  line-height: 1.2;
  color: var(--text-1);
}
.tile p { color: var(--text-2); font-size: 1rem; }
.tile__foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 8px;
  margin-top: 4px;
  padding-top: 14px;
  border-top: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-3);
}
/* A whole tile that is a link keeps the surface's ink; only the affordance
   in the foot moves on hover, so the card does not flash lime. */
a.tile:hover .tile__foot,
a.tile:focus-visible .tile__foot { color: var(--action-hover-text); }

/* --- .group-head — a city divider inside a chapter -----------------------
   "London — 13 projects". A hairline under it, not a card, so the groups
   read as one list broken into three rather than three lists. */
.group-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px 16px;
  margin-top: clamp(40px, 5vw, 64px);
  padding-bottom: 14px;
  border-bottom: 1px solid var(--rule);
}
.group-head:first-of-type { margin-top: 0; }
.group-head + .tile-grid { margin-top: clamp(20px, 2.4vw, 28px); }
.group-head__count {
  font-family: var(--mono);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* --- .shots — on-the-ground photographs ---------------------------------
   The chapter that answers "were you actually there?". Portrait frames,
   because these are phone photographs from tours and events and cropping
   them to landscape throws away the people. One frame may span two columns
   (.shot--wide) so the row is not a contact sheet.

   Captions sit ON the photograph behind the same bottom-weighted ramp
   .figure uses, so a bright sky in the top of the frame is never what the
   caption has to read against. */
.shots {
  display: grid;
  grid-template-columns: repeat(var(--n, 4), minmax(0, 1fr));
  gap: var(--gap);
}
.shot {
  position: relative;
  isolation: isolate;
  display: grid;
  align-items: end;
  aspect-ratio: 4 / 5;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--tint);
}
.shot > img {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; object-fit: cover;
  transition: transform .7s cubic-bezier(.2, .7, .3, 1);
}
.shot:hover > img { transform: scale(1.04); }
.shot--wide { grid-column: span 2; aspect-ratio: 8 / 5; }
.shot figcaption {
  position: relative;
  z-index: 2;
  padding: clamp(14px, 1.6vw, 22px);
  font-family: var(--mono);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--c-cream-1);
}
.shot:has(figcaption)::before {
  content: "";
  position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(180deg, rgba(var(--scrim-rgb), 0) 42%, rgba(var(--scrim-rgb), .84) 100%);
  pointer-events: none;
}

/* --- .cta-still — one photograph behind an enquiry band ------------------
   The homepage's band cross-fades three photographs, one per market, so
   .cta-slide's keyframes hold each file visible for only a third of the
   cycle. A region page has ONE market, and a lone .cta-slide would spend two
   thirds of its life invisible. This is the still version: same crop, same
   grade, no animation. */
.cta-still {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  filter: saturate(.88) contrast(.97);
}

/* --- .note — a disclaimer set apart from the copy it qualifies ----------- */
.note {
  margin-top: clamp(22px, 3vw, 34px);
  padding: 14px 18px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  font-size: 1rem;
  line-height: 1.55;
  color: var(--text-2);
}

/* --- responsive ----------------------------------------------------------
   The `--n` overrides below are `!important` for the same reason .cols and
   .figure-grid are: the column count is set per instance as an inline
   `style="--n:3"`, and an inline custom property beats any rule in this file
   however the media query is written. Without it these grids stay three- and
   four-across on a phone. Measured before the fix: the third .pod on
   /singapore was 85px wide at 320px and spilled 4.4px past the viewport, and
   it was equally unusable — just not overflowing — at 380px. */
@media (max-width: 1024px) {
  /* Two-up, not one-up: a single pod across a 1024px tablet is a 976px line
     length, which is roughly twice a readable measure. It only goes full-width
     at 700px, below. */
  .pod-grid { --n: 2 !important; }
  .shots { --n: 3 !important; }
}
@media (max-width: 900px) {
  .duo { grid-template-columns: minmax(0, 1fr); }
  .duo .h-chapter, .duo .lede { max-width: none; }
}
@media (max-width: 700px) {
  .pod-grid { --n: 1 !important; }
  .sell-grid { grid-template-columns: minmax(0, 1fr); }
  /* Two portrait frames still read at 380px (~166px each); one per row would
     make this chapter as tall as the rest of the page put together. The wide
     frame keeps its two-column span but turns landscape, so it is not a 2-up
     stranded in a 1-up. */
  .shots { --n: 2 !important; }
  .shot--wide { grid-column: span 2; aspect-ratio: 4 / 3; }
}

/* ==========================================================================
   MONOGRAM WATERMARK — must stay LAST in this file.
   A single-purpose utility, kept from index.html's vocabulary. The notch/swoop
   corner treatments that used to live here were removed: every corner is now
   rounded uniformly via --radius / --radius-lg.
   ========================================================================== */
/* Brick monogram watermark. Masked so it always tints to the surface accent
   rather than shipping a second coloured asset per background. */
.mono-mark {
  position: absolute;
  pointer-events: none;
  user-select: none;
  aspect-ratio: 712 / 1000;
  background-color: var(--accent);
  opacity: .18;
  -webkit-mask-image: url(../assets/brand/brick-icon-small.png);
          mask-image: url(../assets/brand/brick-icon-small.png);
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-size: contain;     mask-size: contain;
  -webkit-mask-position: center;  mask-position: center;
}
/* On the feature panel the monogram is not a watermark — the reference sets it
   as a visible lime mark in the cleared corner above the copy, and it is the only
   thing holding that space. .18 read as a smudge there. */
.mono-mark--panel {
  top: clamp(20px, 3vw, 34px);
  right: clamp(20px, 3vw, 34px);
  width: clamp(34px, 4.4vw, 58px);
  opacity: .62;
}
/* The watermark /about's hero carried before this rebuild: a large brick
   monogram bled off the right edge at 6% opacity. Reproduced at the old page's
   own geometry (right:-6%, top:2%, up to 760px square) rather than reusing
   .mono-mark--lg, which is a small corner mark and reads as a smudge here.
   z-index 1 is load-bearing: .hero::before is the scrim and carries z-index 1,
   so a mark left on the default `auto` paints UNDER it and is invisible —
   measured, that is exactly what happened on the first pass. */
.hero .mono-mark--hero {
  z-index: 1;
  right: -6%;
  top: 2%;
  width: min(60vw, 760px);
  aspect-ratio: 1 / 1;
  opacity: .07;
}

.mono-mark--lg { width: clamp(56px, 7vw, 104px); opacity: .22; }
/* Large watermark anchored to the bottom of a text column — used where there is
   genuine empty ground. Never place a mark over interactive controls: at the
   top-right of the enquiry panel it crossed the choice buttons' borders. */
.mono-mark--corner {
  left: 0;
  bottom: 0;
  width: clamp(88px, 11vw, 168px);
  opacity: .13;
}
