/* ============================================================
   MASTER MODULAR HOMES — Design System
   V20 · solid black · white text · earthy green · antique gold
   Type: Archivo, one family, everywhere (V21 — DVELE reference)
   ============================================================

   V20 COLOUR DIRECTION (Modular Update V20, DVELE reference)
   ----------------------------------------------------------
   Primary black   PANTONE Black 6 C   #101820   the whole page ground
   Earthy green    PANTONE 5743 C      #3E4827   ONE accent section per page
   Antique gold    (existing brand)    #B3945E   accents, rules, CTAs

   Token roles are unchanged from the cream system, so every rule that
   already said "paint this var(--ink) and put var(--paper) text on it"
   still reads correctly — the values behind the names are what moved:

     --ink*     the DARK values. Page ground, raised surfaces, chips.
     --paper*   the LIGHT values. Text, hairline highlights, knockouts.
     --card / --paper-2 / --paper-3   surfaces, now dark and only a few
                percent apart, so sections join rather than band. That
                connectedness is the point of the reference.

   Two tokens changed meaning and both are recorded here because they
   are the ones that will trip up the next person:

     --gold-deep  was #8A6C3A, "legible gold on cream". On #101820 it
                  measures 3.4:1 and fails body text, so it is retired
                  and now carries the LIGHT gold used for small text on
                  dark. Every one of its 56 call sites wanted "the gold
                  that is readable against the current ground", which
                  is exactly what it still means.
     --white      was #FFFFFF used as a raised fill under cream. Pure
                  white is now only ever ink ON something, never a
                  ground, so the fills that used it point at --ink-2.
   ============================================================ */

/* ---------- Tokens ---------- */
:root{
  /* Palette — grounds */
  --ink:        #101820;   /* PANTONE Black 6 C — the page ground */
  --ink-2:      #16212C;   /* raised surface: cards on the ground, drawers */
  --ink-3:      #0B1116;   /* recessed surface: footer, wells, code */
  --ink-soft:   #D8D3C7;   /* secondary text on a dark ground */

  /* Palette — earthy green, used selectively (one section per page) */
  --green:      #3E4827;   /* PANTONE 5743 C */
  --green-2:    #333B20;   /* deeper green, for wells inside a green band */
  --green-3:    #4B573000; /* reserved */
  --line-green: rgba(139,158,96,.34);

  /* Palette — light values (text and knockouts) */
  --paper:      #F4F1EA;   /* primary text on dark */
  --paper-2:    #131D26;   /* alt section ground — a whisper off --ink */
  --paper-3:    #18242F;   /* deepest surface / media placeholder */
  --card:       #16212C;   /* card surface */
  --white:      #FFFFFF;   /* ink only, never a ground */

  --gold:       #B3945E;   /* brand antique gold — unchanged */
  /* V23 — ONE GOLD FOR TEXT.
     There were three golds in circulation and two of them were being used for
     static text: --gold-deep on eyebrows and accents, --gold-glow on a further
     twenty-four rules including hero eyebrows, footer headings, spec labels
     and the enquiry aside. Side by side they read as two different colours,
     which is what the client saw. They are now the same value, so every piece
     of gold text on the site is #C9A96C whichever token a rule happens to name.
     --gold-hi exists only for hover and focus, where getting brighter is the
     entire point. --gold (#B3945E) stays as the brand value for lines,
     borders, icon strokes and button fills — it is never text. */
  --gold-deep:  #C9A96C;   /* THE gold for text */
  --gold-glow:  #C9A96C;   /* alias — same colour, kept so 44 rules need no edit */
  --gold-hi:    #E2CFA4;   /* hover / focus only */
  --line:       rgba(244,241,234,.15);     /* light hairline */
  --line-gold:  rgba(179,148,94,.42);      /* gold hairline */
  --line-soft:  rgba(244,241,234,.085);
  /* WCAG 1.4.11 asks 3:1 for a UI component boundary. --line measures
     1.5:1 on the ground and is decorative only; use this for a control. */
  --line-ui:    rgba(244,241,234,.38);
  --wash-gold:  rgba(179,148,94,.13);

  --muted:      #AFA99D;   /* secondary text on the ground   6.9:1 */
  --muted-2:    #9A948A;   /* tertiary text on the ground    5.9:1 */
  --muted-dk:   #C5BAA2;   /* warm secondary on ink/green    9.1:1 */

  /* SMRT Built blue, sampled from the supplied lockup (#0038A0). The
     brand file is never recoloured; this lift is for UI text only,
     where the brand blue measures 1.8:1 on the black ground. */
  --navy:       #22386C;
  --navy-on-dark: #7C9BE0;

  /* Type — V21: ONE family, Archivo, the way the reference runs it.
     The three token names are kept because ~180 rules reference them and
     each still means something different (display size / reading size /
     technical register); they simply resolve to the same face now, and the
     register is carried by weight and tracking instead of by a font swap. */
  --display: "Archivo", system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  --body:    "Archivo", system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
  --mono:    "Archivo", system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;

  /* Metrics */
  /* V22 — content width, matched to the reference.
     The reference runs its content close to the full window with a modest,
     nearly constant gutter; this was 1440px capped inside an 88px gutter,
     which at 1920 left 240px of dead black down each side and made every
     page read narrower than the photography behind it. Content now reaches
     1760px and the gutter tightens as the window grows rather than widening
     with it. Mobile keeps a 20px gutter — below that, text touches the
     bezel. */
  --edge: clamp(20px, 3.6vw, 72px);   /* page gutter */
  --maxw: 1760px;
  --maxw-text: 1180px;                /* long-form measure, unchanged in feel */
  /* V22 — section rhythm, reduced on client feedback ("reduce excessive
     vertical gaps and oversized padding"). This was clamp(76px,11vh,168px):
     on a 1080-tall window that is 119px of padding at BOTH ends of every
     section, so 238px of empty black between one section and the next. The
     new value gives ~155px there and scales down properly on tablet and
     phone, where 11vh was producing more air than the content it separated. */
  --sp:  clamp(56px, 7.2vh, 112px);  /* section rhythm */

  --ease: cubic-bezier(.22,.61,.36,1);
  /* V24 — the curve the reference build lifts its intro curtain on. */
  --ease-in-out: cubic-bezier(.65,0,.35,1);
  --ease-out: cubic-bezier(.16,1,.3,1);

  /* V20 — logo transition + cinematic intro.  Retimed in V21.
     --mmh-tr-out MUST finish before the script navigates (OUT = 300ms in
     mmh_v20_transition). At 340ms it did not: the document was replaced with
     the curtain still at ~88% opacity, so the outgoing page was visible
     through it and then jumped to solid black on the new one. 260ms leaves
     40ms of fully covered time before the swap. */
  --mmh-tr-out:   260ms;   /* curtain in, covering the outgoing page */
  --mmh-tr-hold:   60ms;   /* mark held while the next page is fetched */
  --mmh-tr-in:    520ms;   /* legacy — kept, no longer drives the lift */
  --mmh-tr-lift:  750ms;   /* V27: curtain lifting off the incoming page.
                              Identical to the first-visit intro's own lift,
                              because they are now the same gesture. */
  --mmh-intro-max: 2200ms; /* hard ceiling on the first-visit intro */
}

/* ---------- Reset ---------- */
*,*::before,*::after{ box-sizing:border-box; }
*{ margin:0; }
html{ -webkit-text-size-adjust:100%; scroll-behavior:smooth; scroll-padding-top:96px; }
@media (prefers-reduced-motion: reduce){ html{ scroll-behavior:auto; } }
/* V101 — Lenis, the page-wide momentum scroll. Its own three rules, plus one
   of ours.

   ⚠ NATIVE SMOOTH SCROLL HAS TO GO OFF WHILE LENIS IS RUNNING. Both animate
   the same scroll position and they pull against each other: an anchor click
   gets two easings at once and lands short of its target. Lenis puts `.lenis`
   on <html> when it starts, so this switches off only for the people who
   actually have it. Anyone on reduced motion, or on a touch screen, or with
   the script unavailable, keeps native smooth exactly as before. */
html.lenis{ scroll-behavior:auto; }
html.lenis, html.lenis body{ height:auto; }
.lenis:not(.lenis-autoToggle).lenis-stopped{ overflow:clip; }
.lenis [data-lenis-prevent],
.lenis [data-lenis-prevent-wheel],
.lenis [data-lenis-prevent-touch]{ overscroll-behavior:contain; }
/* Lenis ships this so the wheel is never swallowed by an embedded frame. It
   would also have killed the 3D townhouse tour, which is an iframe the whole
   page exists to let people drag around. The tour is exempt, and its stage
   carries data-lenis-prevent so the wheel inside it zooms the scene instead of
   scrolling the page past it. */
.lenis.lenis-smooth iframe{ pointer-events:none; }
.lenis.lenis-smooth .tv-frame{ pointer-events:auto; }
.lenis.lenis-autoToggle{ transition-property:overflow; transition-duration:1ms; transition-behavior:allow-discrete; }
body{
  font-family:var(--body);
  background:var(--ink);
  color:var(--paper);
  line-height:1.6;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
  overflow-x:hidden;
}
/* V18.1 §11 fix: overflow-x:hidden forces body's overflow-y to compute to
   `auto`, which makes <body> a scroll container — and that silently disables
   EVERY position:sticky on the page (the Build Journey pin, among others).
   `clip` blocks horizontal scroll without creating a scroll container. */
@supports (overflow-x: clip){
  body{ overflow-x:clip; }
}
img,svg{ display:block; max-width:100%; }
a{ color:inherit; text-decoration:none; }
button{ font:inherit; color:inherit; cursor:pointer; background:none; border:none; }
ul{ list-style:none; padding:0; }
::selection{ background:var(--gold); color:var(--ink); }

/* ---------- Type scale ---------- */
h1,h2,h3,h4{ font-family:var(--display); font-weight:340; line-height:1.03; letter-spacing:-.014em; color:var(--paper); }
.display-xl{ font-size:clamp(2.6rem, 6.6vw, 5.4rem); line-height:1.04; letter-spacing:-.02em; font-weight:300; }
.display-l { font-size:clamp(2.4rem, 5.6vw, 4.6rem); }
.display-m { font-size:clamp(1.9rem, 3.6vw, 3.05rem); }
.display-s { font-size:clamp(1.5rem, 2.4vw, 2.05rem); }
em, .italic{ font-style:italic; }
.serif-accent{ font-style:italic; color:var(--gold-deep); font-weight:340; }

p{ color:var(--muted); font-size:clamp(1rem,1.05vw,1.12rem); }
.lead{ font-size:clamp(1.12rem,1.5vw,1.34rem); line-height:1.5; color:var(--ink-soft); font-weight:400; }

/* Eyebrow / mono spec label */
.eyebrow{
  font-family:var(--mono); font-size:.72rem; letter-spacing:.26em;
  text-transform:uppercase; color:var(--gold-deep); font-weight:400;
  display:inline-flex; align-items:center; gap:.7em;
}
.eyebrow::before{
  content:""; width:26px; height:1px; background:var(--gold); opacity:.8;
}
.eyebrow.no-tick::before{ display:none; }
/* V116 — cards 87-91 write several model-page headings in capitals: STANDARD
   INCLUSIONS & PROJECT REQUIREMENTS, THE LODGE CLASSIC — PLANS & DESIGN,
   EXPLORE THE LODGE CLASSIC, EXPERIENCE THE LODGE CLASSIC IN PERSON. The caps
   are applied here rather than typed into the copy, so the heading is still
   stored and read out as words by a screen reader, and the client can have
   sentence case back by deleting one rule. */
.md-caps{ text-transform:uppercase; }
.mono{ font-family:var(--mono); letter-spacing:.02em; }
.spec-code{ font-family:var(--mono); font-size:.72rem; letter-spacing:.18em; color:var(--muted-2); text-transform:uppercase; }

/* ---------- Layout ---------- */
.wrap{ max-width:var(--maxw); margin-inline:auto; padding-inline:var(--edge); }
.section{ padding-block:var(--sp); position:relative; }
/* V46.2 — client audit: "the correct spacing on each section is the showroom
   section". Measured every top-level section-to-section gap across the site
   (script: qa/spacing-audit.mjs) against #showroom's own gap to its neighbours
   (152px against the oversized .builder-xp band, 130px against a plain
   section — both just var(--sp) on each abutting edge, no override). The
   gap was already 130px almost everywhere two plain `.section` blocks meet
   — home, the static marketing pages, the townhouse gallery. The one real
   deviation was `.section.tight`, which knocked the gap down to 98–114px
   wherever it was used: every content-dense section on the model and
   townhouse detail pages (by far the highest-traffic pages on the site),
   plus several sections on About, Contact, FAQ, SMRT BUILT and Installation
   Story. `.tight` set nothing but padding-block (confirmed — grep for
   `.tight` in this file turns up only this rule and its 900px-breakpoint
   twin below), so bringing every section to the Showroom rhythm needed no
   markup change in any of the ~10 template files that use the class: just
   let `.tight` fall through to the same `padding-block:var(--sp)` the base
   `.section` rule already sets. The class stays in the markup — inert now
   for spacing, harmless to leave — rather than stripping it from every
   file that references it. */
.bg-paper2{ background:var(--paper-2); }
.bg-paper3{ background:var(--paper-3); }
/* ────────────────────────────────────────────────────────────────────────
   WHERE THE GREEN GOES  (v2.29.0)

   The brief asks for #3E4827 "selectively for suitable sections", and to
   "avoid overusing the green". The first pass mapped it one-to-one onto
   every dark-inverse section the cream design happened to have, which put
   NINE separate green areas on a single model page — the hero panel, the
   Future Expansion box, five key-figure tiles, the enquiry aside and a
   closing band. That is what read as too much; the hue was never the
   problem.

   The rule was one sentence, and it decided every case:

       GREEN MARKS THE ONE BLOCK PER PAGE WHERE THE VISITOR IS BEING ASKED
       TO DECIDE OR ACT.

     · Model and townhouse pages — the spec panel beside the hero. It holds
       the specifications and both calls to action, and it is the strongest
       green on the site: a solid field of colour against a photograph.
     · Every other page — the closing invitation band. **WITHDRAWN V67.1:
       the client asked for every call to action to be navy, on all pages and
       on future work, so these five bands now use `.bg-ink .cta-band`. The
       green is gone from them for good — do not put it back.**
     · Pages built around a form and nothing else (Finance, Contact, the
       showrooms, the Online Builder) — the enquiry aside, via
       `'green' => true` on the enquiry-form template part.

   V67.1 — the SECOND bullet is withdrawn too (see above). What is left of
   the green after V67.1 is: the model/townhouse hero spec panel, and the
   "Built with experience" band, which the homepage carries and which is NOT a
   call to action — the client pointed at the homepage as the reference and it
   keeps that band green, so it stays.

   V46 — the THIRD bullet is withdrawn. The client asked for every
   enquiry-form section on the site to share one design and colour, so the
   enquiry aside no longer varies by page: it is the same dark navy
   (`.enq-aside`, no modifier) everywhere, on the eight pages that used to
   set `'green' => true` as much as on the model pages that never did.
   `.enq-aside--green` is retired, not renamed — nothing calls it. The
   first two bullets are UNCHANGED and untouched by this: the model-hero
   spec panel and each page's own closing band are a different component
   from the enquiry form, and this instruction was specifically about the
   enquiry form.

   Everything else that used to be green is now a raised dark surface, and
   gold keeps what the brief gives it: accents, buttons, lines, icons and
   branding details.

   To move the green on a page, move the class. Nothing else keys off it.

   V46.2 — proof of that last line. The homepage's own SECOND bullet
   instance (its closing Showroom band) moved: the client asked it to match
   the other enquiry-form sections instead (plain `bg-ink`, not green), and
   asked for the green to go to Builder Experience — a component reused by
   the townhouse page too, which was NOT asked to change — instead. Done
   entirely in tpl-home.php: `bg-green` → `bg-ink` on #showroom, and a new
   `bg` arg on the Builder Experience template part (default `bg-ink`,
   preserving the townhouse page; the homepage's call passes `bg-green`).
   No CSS changed for this — every colour in both sections was already
   written against the dark-surface palette (var(--paper), var(--gold),
   var(--gold-glow)), not against bg-ink or bg-green specifically, which is
   exactly what "move the class, nothing else keys off it" promised.
   ──────────────────────────────────────────────────────────────────────── */

/* The dark-inverse section. No longer green — just a step up from the page
   ground, with the same feathered seam so it still joins rather than bands. */
.bg-ink{ background:var(--paper-2); color:var(--paper); }

/* The green band. Edgeless on purpose: a gradient rises through the section
   and dissolves into the black above and below, so the green never presents
   a rectangle. A hard-edged block at this size was the other half of "too
   much" — see the v2.29.0 record. */
/* V23 (revised) — THE GREEN IS NOT A BACKGROUND AT ALL.

   Three treatments have now been built and shown to the client:

     V20  a flat green band                → "it feels too much"
     V22  a green band with faded edges    → "doesn't look right, doesn't feel
                                              premium"
     V23  a solid, full-height green ground → "No. I don't like this."

   That is not three failures of execution. It is one finding, arrived at three
   times: a muted olive CANNOT read as premium as an isolated area on a black
   page, at any size or edge treatment. On the reference site the olive works
   because it is the ground of the ENTIRE site — the eye adapts to it and it
   becomes the environment. Drop the same colour into the middle of a black
   page and it is a patch, and a patch of desaturated olive next to true black
   reads as dirty rather than earthy. More of it makes that worse, not better.

   So the green comes off backgrounds entirely. It stays in the palette where
   it has always worked — hairlines, ticks, icon strokes, the wash behind a
   quiet panel — and the sections that carried it return to the raised dark
   ground used everywhere else on the site, which the client has never once
   objected to in seven rounds of review.

   Committing the whole site to the olive is the one way a green ground works,
   and it is on the table — but it is a repaint of every page, not a section
   change, and it is not something to do without the client seeing it first. */
/* V66 — client, on the SMRT Built Townhouse page: "The section on the enquire
   is also changing to green. Can we make sure it won't change and only the
   Built with experience. Backed by expertise background will change."

   The green was never this section's own fill. `.bg-green` painted
   `--paper-2` here and then handed the colour to the SCROLL GROUND (see the
   ground system below): a single fixed backdrop behind the whole page whose
   colour is set by whichever section owns the middle of the viewport. So when
   the Built-with-experience band reached the middle, the ENTIRE PAGE turned
   olive — the section above it, the section below it, and the margins around
   the Enquire card — and stayed olive until the next section took the ground
   over. That is exactly what she screenshotted.

   The band now paints its own olive, bounded by its own box. This makes the
   green LESS pervasive, not more: instead of a viewport-high wash it is a
   band the height of the section, which is what "ONE accent section per page"
   in the palette note at the top of this file always described. */
.bg-green{
  position:relative; color:var(--paper);
  padding-block:calc(var(--sp) * 1.35);
  background:var(--green);
  isolation:isolate;
}
/* V67.1 · EVERY CALL-TO-ACTION BAND IS NAVY, NOT GREEN.
   Client, 7 Sep 2026: "The call to action should be navy for all pages and
   for future updates." That is a STANDING RULE, not a one-off — a new closing
   band on any future page uses `.bg-ink .cta-band`, never `.bg-green`.

   The five closing bands (Our Process, FAQ, What Sets Us Apart, SMRT Built,
   How It's Done) now carry `.bg-ink` — measured on the live homepage as the
   ground its own call to action uses (#showroom, rgb(19,29,38) = --paper-2).
   `.bg-ink` already has the full paired rule set `.bg-green` had (headings,
   copy, eyebrow, .btn.ghost, .tlink), so the swap carries the styling with
   it, and it correctly does NOT carry the gold lift — that lift exists only
   because olive is a lighter ground (rule #75), and applying it on navy would
   make these bands' gold brighter than every other panel on the site.

   `.cta-band` exists so the ONLY thing that changes is the colour. .bg-ink on
   its own has no padding or hairlines, and #131D26 against the page's #101820
   is a very small step — the band would have quietly stopped reading as a
   band at all. Rule #57 is explicit that too-subtle is its own failure mode
   with this client. So the geometry `.bg-green` gave these sections is kept
   verbatim: same padding-block, same hairline top and bottom. */
.cta-band{
  padding-block:calc(var(--sp) * 1.35);
  border-top:1px solid var(--line-soft);
  border-bottom:1px solid var(--line-soft);
}

/* A gold hairline top and bottom is what separates it now — the same device
   the cards and panels use, at section scale. */
.bg-green::after{ content:none; }
.bg-green{
  border-top:1px solid var(--line-soft);
  border-bottom:1px solid var(--line-soft);
}

/* Anything sitting ON the green needs its secondary and tertiary text lifted.
   The green is a LIGHTER ground than the page black (relative luminance .039
   against .011): --muted-2 measures 5.9:1 on the black and 3.2:1 on the
   green, which is a fail. These three lifts are what keep every green block
   at AA, and they must travel with the green wherever it is applied. */
/* V23 — the gold lift here is NOT decorative and must not be flattened.
   When the two text golds were collapsed into one (#C9A96C), this line
   silently became a no-op, and every eyebrow, spec label and phone number on
   a green ground dropped to 4.34:1 — a fail, on twenty-two elements across
   sixteen pages. The green is a lighter ground than the black; text on it
   needs a brighter gold to hold the same contrast. #E2CFA4 measures 8.5:1
   here. It is the same gold to the eye because the ground beneath it is
   different — that is colour management, not a second gold. */
/* V66 — `.bg-green` IS BACK ON THIS LIFT, and it has to be.
   The note above was written when `.bg-green` had stopped being green and had
   become "a dark ground like any other", so it was taken off the lift and only
   the model hero panel kept it. Making the band paint real olive again brings
   the lighter ground back with it, and measuring proved the warning above was
   not theoretical: every eyebrow and spec label on the band dropped to
   **4.34:1** (fails AA for normal text) and the `.tlink` arrow on the closing
   bands of five pages to **1.84:1**, because `--gold-deep` and `--gold-glow`
   both resolve to #C9A96C, which is tuned for the near-black page ground.
   #E2CFA4 measures 8.5:1 on the olive. Same gold to the eye; different ground
   underneath. If the band ever stops being green, take it off this rule
   again — but never take it off while the olive is painting. */
.md-hero .m-info,
.bg-green{
  --muted:     #CFC9BE;
  --muted-2:   #BDB7AC;
  --gold-deep: var(--gold-hi);
  --gold-glow: var(--gold-hi);
}
.bg-green .ticked::before,.bg-green .ticked::after{ border-color:var(--line-green); }
.bg-ink h1,.bg-ink h2,.bg-ink h3,.bg-ink h4,
.bg-green h1,.bg-green h2,.bg-green h3,.bg-green h4{ color:var(--paper); }
.bg-ink p,.bg-green p{ color:var(--muted-dk); }
.bg-ink .eyebrow,.bg-green .eyebrow{ color:var(--gold-glow); }
.bg-ink .eyebrow::before,.bg-green .eyebrow::before{ background:var(--gold-glow); }

.grid{ display:grid; gap:clamp(20px,2.4vw,40px); }
.two{ grid-template-columns:repeat(2,1fr); }
.three{ grid-template-columns:repeat(3,1fr); }
.four{ grid-template-columns:repeat(4,1fr); }
.center{ text-align:center; }
/* ⚠ V167 — CENTRING THE TEXT IS NOT CENTRING THE BLOCK, and the difference
   shows the moment the block is narrower than its parent.

   `.lead` carries `max-width:58ch` and `.measure` carries `60ch`. Inside a
   `.center` wrapper the TEXT centres, but the box itself still sits hard
   against the left edge, so the copy reads visibly off to one side under a
   heading that really is centred.

   Measured on /custom-configurations/, the "Looking for a different
   arrangement?" band at 1440: the lead's centre sat 62px left of the section's
   centre, while the h2 above it and the disclaimer below it were dead on. Its
   box was 532px wide with margin-left and margin-right both 0.

   This was already patched once as a one-off further up the file
   (`.cob--cta .cob-cta-inner .measure{ margin-inline:auto }`), which is the
   tell that it wanted fixing at `.center` instead of per band. Scoped to
   `.center`, so nothing outside a deliberately centred wrapper moves. */
.center .lead,
.center .measure,
.center p[class*="fineprint"],
.center .disclaimer{ margin-inline:auto; }
.mx-auto{ margin-inline:auto; }
.measure{ max-width:60ch; }
.measure-s{ max-width:44ch; }

/* Section header block */
.sec-head{ display:flex; flex-direction:column; gap:1.1rem; max-width:60ch; }
.sec-head.center{ align-items:center; text-align:center; margin-inline:auto; }
.sec-head .eyebrow{ margin-bottom:.2rem; }

/* Blueprint corner ticks */
/* V27.1 item 4 — THE CORNER TICKS ARE GONE.
   Client: "remove the unnecessary icon". They were two 1px L-shaped brackets
   floated in the top corners of a section, and she is right that they read as
   an icon rather than as structure — a mark with no meaning attached to it.
   `.ticked` is left as a live class on several sections and templates, so the
   rules stay and simply draw nothing; removing the class from the markup
   would touch a dozen files to achieve the same picture. */
.ticked{ position:relative; }
.ticked::before,.ticked::after{ content:none !important; }
.ticked::before,.ticked::after{
  content:""; position:absolute; width:14px; height:14px; pointer-events:none;
  border:1px solid var(--line-gold);
}
.ticked::before{ top:calc(var(--sp) * .42); left:calc(var(--edge) - 8px); border-right:0; border-bottom:0; }
.ticked::after{ top:calc(var(--sp) * .42); right:calc(var(--edge) - 8px); border-left:0; border-bottom:0; }
@media (max-width:800px){ .ticked::before,.ticked::after{ display:none; } }

/* Dimension divider line */
.dim-rule{ display:flex; align-items:center; gap:1rem; color:var(--muted-2); }
.dim-rule::before,.dim-rule::after{ content:""; height:1px; background:var(--line-gold); flex:1; }
.dim-rule span{ font-family:var(--mono); font-size:.68rem; letter-spacing:.24em; text-transform:uppercase; white-space:nowrap; }

/* ---------- Buttons ---------- */
.btn{
  --bg:var(--paper); --fg:var(--ink); --bd:var(--paper);
  display:inline-flex; align-items:center; justify-content:center; gap:.7em;
  padding:1.02em 1.7em; font-family:var(--body); font-weight:600; font-size:.86rem;
  letter-spacing:.02em; background:transparent; color:var(--fg);
  border:1px solid var(--bd); border-radius:0; position:relative; overflow:hidden; z-index:0;
  transition:color .5s var(--ease), background .5s var(--ease), border-color .5s var(--ease);
  white-space:nowrap;
}
.btn .arrow{ transition:transform .45s var(--ease); }
.btn:hover .arrow{ transform:translateX(4px); }
.btn::before{
  content:""; position:absolute; inset:0; background:var(--bg); z-index:-2;
}
.btn::after{
  content:""; position:absolute; inset:0; background:var(--gold);
  transform:scaleX(0); transform-origin:left; z-index:-1;
  transition:transform .5s var(--ease);
}
.btn:hover::after{ transform:scaleX(1); }
.btn > *{ position:relative; z-index:1; }
.btn:hover{ color:var(--ink); border-color:var(--gold); }
.btn.gold{ --bg:var(--gold); --fg:var(--ink); --bd:var(--gold); }
.btn.gold::after{ background:var(--ink); }
.btn.gold:hover{ color:var(--paper); border-color:var(--gold-hi); }
.btn.ghost{ --bg:transparent; --fg:var(--paper); --bd:var(--line-ui); }
.btn.ghost::after{ background:var(--gold); }
.btn.ghost:hover{ color:var(--ink); border-color:var(--gold); }
.bg-ink .btn.ghost,.bg-green .btn.ghost{ --fg:var(--paper); --bd:rgba(255,255,255,.32); }
.bg-ink .btn.ghost::after,.bg-green .btn.ghost::after{ background:var(--gold); }
.bg-ink .btn.ghost:hover,.bg-green .btn.ghost:hover{ color:var(--ink); }

/* Finance chart loop */
.fin-bar{ transform-origin:bottom; transform:scaleY(0); animation:finBar 6.5s var(--ease-out) infinite; }
.fin-bar.b2{ animation-delay:.22s; } .fin-bar.b3{ animation-delay:.44s; } .fin-bar.b4{ animation-delay:.66s; }
@keyframes finBar{ 0%{ transform:scaleY(0);} 12%,90%{ transform:scaleY(1);} 98%,100%{ transform:scaleY(0);} }
/* V122 — 430 -> 410: the length of the redrawn path. See the note in
   template-parts/section-finance.php. */
.fin-line{ stroke-dasharray:410; stroke-dashoffset:410; animation:finLine 6.5s var(--ease) infinite; }
@keyframes finLine{ 0%,16%{ stroke-dashoffset:410;} 52%,90%{ stroke-dashoffset:0;} 98%,100%{ stroke-dashoffset:410;} }
.fin-dot{ opacity:0; animation:finDot 6.5s var(--ease) infinite; }
@keyframes finDot{ 0%,50%{ opacity:0;} 56%,88%{ opacity:1;} 96%,100%{ opacity:0;} }
@media (prefers-reduced-motion:reduce){
  .fin-bar,.fin-line,.fin-dot,.hero-scroll .chev{ animation:none; transform:none; stroke-dashoffset:0; opacity:1; }
}

/* Page hero with background image (designs etc.) */
.page-hero-band{ position:relative; overflow:hidden; }
.page-hero-band > .photo{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; object-position:center center; z-index:0; }
.page-hero-band .scrim{ position:absolute; inset:0; z-index:1; pointer-events:none;
  /* V14 (#4): wash reduced further so the professional photography dominates.
     A soft left wash is kept only under the heading, and a lighter base blend. */
  background:
    radial-gradient(112% 90% at 20% 52%, rgba(16,24,32,.42) 0%, rgba(16,24,32,.28) 30%, rgba(16,24,32,.10) 54%, rgba(16,24,32,0) 74%),
    linear-gradient(180deg, rgba(16,24,32,0) 0%, rgba(16,24,32,0) 52%, rgba(16,24,32,.16) 80%, rgba(16,24,32,.52) 97%, var(--ink) 100%); }
.page-hero-band .page-hero{ position:relative; z-index:2; }

/* V68 — --wash: a hero whose photograph is BRIGHT and whose copy is long.
   The default scrim above was deliberately weakened in V14 so the photography
   dominates; that is right for the darker heroes it was tuned against and it
   is NOT touched here. The contact pages' new Image 1 is a dusk shot with a
   pale sky and a lit interior, and the client's copy is a heading plus two
   paragraphs, so the text now runs across the bright half of the frame.

   Measured on the real render, worst-case pixel under each text box:
     before   eyebrow 1.00:1   h1 1.93:1   lead 2.55:1     (invisible)
     after    minimum 6.57:1 across 1440/1280/1024/834/390/360, both pages
   AA wants 4.5:1 for the eyebrow and lead, 3:1 for the h1.

   Do not "lighten this back" without re-running the measurement — the whole
   left column of copy depends on it. */
.page-hero-band--wash .scrim{
  background:
    linear-gradient(96deg, rgba(16,24,32,.97) 0%, rgba(16,24,32,.95) 46%, rgba(16,24,32,.82) 62%, rgba(16,24,32,.34) 78%, rgba(16,24,32,0) 94%),
    linear-gradient(180deg, rgba(16,24,32,0) 0%, rgba(16,24,32,0) 52%, rgba(16,24,32,.16) 80%, rgba(16,24,32,.52) 97%, var(--ink) 100%);
}
@media (max-width:820px){
  /* Stacked: the copy sits over the whole frame, so the wash goes vertical. */
  .page-hero-band--wash .scrim{
    background:linear-gradient(180deg, rgba(16,24,32,.90) 0%, rgba(16,24,32,.86) 55%, rgba(16,24,32,.80) 100%);
  }
}

/* V142 — --washL: a BRIGHT hero whose copy is one short heading.
   --wash above is tuned for the contact pages, where the client's copy is a
   heading plus two paragraphs running across the left half, so it holds .95
   opacity all the way to 46% and .82 to 62%. Put the Our Process hero under
   it and the reading is correct but the picture is wrong: Max chose that
   photograph for the three people at the island, and --wash sits on top of
   them at near-full strength. The heading there is two words.

   So this releases earlier and harder. Same colour, same base blend, only the
   stops move.

   ⚠ HOW THIS WAS MEASURED, because two easier methods both lied.
   Sampling the H1's bounding box is wrong: that box is the full 936px wrap
   width and the words fill only its left third, so the worst pixel came back
   from bare wall at x~900 where there is no text, and --washL read a flat
   1.00:1. Wrapping the words in an inline span to shrink-wrap them is also
   wrong here, because mmh.js re-splits headings into per-line spans and the
   wrapper is rebuilt. What is measured below is the page rendered twice at
   each width, once with the heading and once with it hidden, and only those
   background pixels that a glyph actually covers:

              default    --wash    --washL
     1440       2.26      16.08      4.28
     1280       2.26      16.06      4.41
     1024       2.47      16.22      3.89
      834       2.70      16.08      4.40
      390       2.09      12.19      8.19
      360       1.92      12.22      8.27

   AA wants 3:1 for a heading this size. The default scrim FAILS at every
   width on this photograph, --wash passes with four times the ink it needs,
   and --washL clears it everywhere while leaving the subjects visible.
   Re-measure the same way if the stops move again. */
.page-hero-band--washL .scrim{
  background:
    linear-gradient(96deg, rgba(16,24,32,.93) 0%, rgba(16,24,32,.86) 18%, rgba(16,24,32,.60) 30%, rgba(16,24,32,.24) 41%, rgba(16,24,32,.06) 50%, rgba(16,24,32,0) 60%),
    linear-gradient(180deg, rgba(16,24,32,0) 0%, rgba(16,24,32,0) 52%, rgba(16,24,32,.16) 80%, rgba(16,24,32,.52) 97%, var(--ink) 100%);
}
@media (max-width:820px){
  /* Stacked, same reasoning as --wash: the heading sits over the whole frame. */
  .page-hero-band--washL .scrim{
    background:linear-gradient(180deg, rgba(16,24,32,.80) 0%, rgba(16,24,32,.74) 55%, rgba(16,24,32,.70) 100%);
  }
}

/* Text link with underline sweep */
.tlink{
  display:inline-flex; align-items:center; gap:.55em; font-weight:600; font-size:.9rem;
  color:var(--paper); position:relative; padding-bottom:3px;
}
.tlink::after{ content:""; position:absolute; left:0; bottom:0; height:1px; width:100%;
  background:var(--gold); transform:scaleX(1); transform-origin:right;
  transition:transform .45s var(--ease); }
.tlink:hover::after{ transform:scaleX(0); transform-origin:left; }
.tlink .arrow{ transition:transform .4s var(--ease); color:var(--gold-deep); }
.tlink:hover .arrow{ transform:translateX(4px); }
.bg-ink .tlink,.bg-green .tlink{ color:var(--paper); }

:where(a,button,input,textarea,select,summary):focus-visible{
  outline:2px solid var(--gold); outline-offset:3px;
}

/* ============================================================
   PROMO BAR + HEADER
   ============================================================ */
.promo{
  background:var(--ink-3); color:var(--paper); font-family:var(--mono);
  border-bottom:1px solid var(--line-gold);
  font-size:.72rem; letter-spacing:.12em; text-transform:uppercase;
  display:flex; align-items:center; justify-content:center; gap:1.2em;
  padding:.62em 1em; position:relative; text-align:center;
}
.promo a{ color:var(--gold-glow); text-decoration:underline; text-underline-offset:3px; }
.promo .pulse{ width:7px; height:7px; border-radius:50%; background:var(--gold); flex:none;
  box-shadow:0 0 0 0 rgba(179,148,94,.7); animation:pulse 2.4s infinite; }
@keyframes pulse{ 70%{ box-shadow:0 0 0 8px rgba(179,148,94,0);} 100%{ box-shadow:0 0 0 0 rgba(179,148,94,0);} }
.promo .close{ position:absolute; right:14px; top:50%; transform:translateY(-50%);
  color:var(--muted-dk); font-size:1rem; line-height:1; padding:4px; }
.promo .close:hover{ color:var(--paper); }
.promo[hidden]{ display:none; }

.site-head{
  position:sticky; top:0; z-index:60;
  background:transparent;
  border-bottom:1px solid transparent;
  transition:border-color .4s var(--ease), background .4s var(--ease);
}
.site-head.scrolled{ border-bottom-color:var(--line-soft); }
/* V47 — client: refine the header further toward the DVELE reference:
   "logo size/presence, menu typography and spacing between the navigation
   elements... more premium, spacious and integrated." DVELE's bar isolates
   the wordmark hard left and clusters the menu + phone + CTA as one group
   hard right, with generous, even air between them — not the even three-way
   split `space-between` was giving us (brand | links | cta each anchored to
   its own third of the bar, so the gap brand→links and links→cta shrinks or
   grows independently of each other and never reads as one composed group).
   `flex-start` plus a single `margin-left:auto` on `.nav-links` below
   pins the cluster together at its natural width and pushes it as a whole
   to the right. The gap that separates brand from that cluster isn't set
   here, though — `.site-head .wrap.nav` further down (3 classes vs. this
   rule's 1) wins the `gap` property outright regardless of source order
   (project rule #17), so that's where the bumped value actually lives —
   see the comment there. */
.nav{ display:flex; align-items:center; justify-content:flex-start; height:76px; gap:clamp(1rem,2vw,1.6rem); }
.brand{ display:flex; align-items:center; gap:.7rem; flex:none; }
.brand .mark{ width:30px; height:30px; flex:none; }
.brand .wordmark{ font-family:var(--display); font-weight:400; font-size:1.16rem; letter-spacing:.01em; line-height:1; }
.brand .wordmark small{ display:block; font-family:var(--mono); font-size:.54rem; letter-spacing:.34em;
  text-transform:uppercase; color:var(--gold-deep); margin-top:3px; }
.nav-links{ display:flex; flex-wrap:nowrap; align-items:center; gap:clamp(.7rem,1.35vw,1.3rem); }
.nav-links a{ font-size:.855rem; font-weight:500; color:var(--paper); position:relative; padding:.4em 0; white-space:nowrap; }
.nav-links a::after{ content:""; position:absolute; left:0; bottom:0; width:100%; height:1px;
  background:var(--gold); transform:scaleX(0); transform-origin:left; transition:transform .4s var(--ease); }
.nav-links a:hover::after,.nav-links a.active::after{ transform:scaleX(1); }
.nav-cta{ display:flex; align-items:center; gap:1.3rem; flex:none; }
.nav-cta .btn{ padding:.82em 1.35em; font-size:.82rem; }
/* V47 — the one auto margin that does the clustering described above. Scoped
   to `.site-head` because `.nav-links` is also the class the blog
   pagination controls reuse (`.post-pagination .nav-links`, unrelated,
   further down this file) — this must not touch that one. */
.site-head .nav-links{ margin-left:auto; }
.burger{ display:none; width:44px; height:44px; flex-direction:column; justify-content:center; gap:5px; align-items:flex-end; }
.burger span{ height:1.5px; background:var(--paper); transition:.4s var(--ease); }
.burger span:nth-child(1){ width:26px; } .burger span:nth-child(2){ width:20px; } .burger span:nth-child(3){ width:26px; }

/* Mobile drawer */
.drawer{ position:fixed; inset:0; z-index:70; visibility:hidden; pointer-events:none; }
.drawer .scrim{ position:absolute; inset:0; background:rgba(6,9,12,.62); opacity:0; transition:opacity .5s var(--ease); }
.drawer .panel{
  position:absolute; top:0; right:0; height:100%; width:min(420px,88vw);
  background:var(--ink-2); border-left:1px solid var(--line-gold);
  transform:translateX(100%); transition:transform .55s var(--ease-out);
  display:flex; flex-direction:column; padding:26px var(--edge) 40px; overflow-y:auto;
}
.drawer.open{ visibility:visible; pointer-events:auto; }
.drawer.open .scrim{ opacity:1; }
.drawer.open .panel{ transform:translateX(0); }
.drawer .d-top{ display:flex; justify-content:space-between; align-items:center; margin-bottom:2.4rem; }
.drawer .d-top .close{ font-size:1.6rem; line-height:1; }
.drawer nav a{ display:flex; align-items:baseline; gap:1rem; padding:1.05rem 0; border-bottom:1px solid var(--line-soft);
  font-family:var(--display); font-size:1.7rem; font-weight:340; }
.drawer nav a .num{ font-family:var(--mono); font-size:.7rem; color:var(--gold-deep); letter-spacing:.1em; }
.drawer .d-cta{ margin-top:2rem; }
.drawer .d-cta .btn{ width:100%; }
.drawer .d-contact{ margin-top:auto; padding-top:2rem; font-family:var(--mono); font-size:.74rem;
  letter-spacing:.08em; color:var(--muted); line-height:2; }

/* ============================================================
   HERO
   ============================================================ */
/* V30 — content is BOTTOM-anchored, per the PIQUE reference. Measured off
   pique.com.au at 1536x730: the banner fills the viewport and its caption
   block is absolutely positioned against the bottom edge, with the headline
   sitting ~70px up in the page gutter. `align-items:flex-end` reproduces that
   WITHOUT taking .hero-foot out of flow, so on a short viewport the hero grows
   with its content instead of the caption overlapping the image edge. */
.hero{ position:relative; min-height:clamp(620px,94vh,960px); display:flex; align-items:flex-end;
  overflow:hidden; background:var(--ink);
  /* V31 — the hold moved UP to .hero from .hero-slider. The progress track
     now lives in .hero-aside, which is NOT a descendant of .hero-slider, so a
     custom property declared there could not reach it and the bar would have
     silently fallen back to its 3000ms default. Declared on the common
     ancestor, one value feeds the crossfade, the JS interval and the bar. */
  --hero-hold:3000ms; --hero-fade:1000ms; }
.hero-sky{ position:absolute; inset:0; z-index:0; }
.hero-sky svg{ width:100%; height:100%; object-fit:cover; }

/* ---- Homepage image slider ----
   V30 · client: 3 seconds per slide, transitions kept smooth.
   Hold and fade are separate numbers and BOTH are declared here, so the CSS
   and the JS cannot drift apart — mmh.js reads --hero-hold off this element
   rather than carrying its own copy of the interval. The fade is deliberately
   shorter than the hold; if it were not, the next slide would start arriving
   before the current one had finished. */
.hero-slider{ overflow:hidden; }
.hero-slide{ position:absolute; inset:0; opacity:0; visibility:hidden;
  transition:opacity var(--hero-fade) var(--ease,ease), visibility 0s linear var(--hero-fade);
  will-change:opacity; }
.hero-slide.is-active{ opacity:1; visibility:visible;
  transition:opacity var(--hero-fade) var(--ease,ease), visibility 0s linear 0s; }
.hero-slide picture{ position:absolute; inset:0; display:block; }
.hero-slide .photo{ width:100%; height:100%; object-fit:cover; object-position:center center; display:block;
  transform:scale(1.075); }
/* The drift runs longer than hold+fade so it never visibly stops mid-slide. */
.hero-slide.is-active .photo{ transform:scale(1); transition:transform 8s linear; }
@media (prefers-reduced-motion:reduce){
  .hero-slide{ transition:none; }
  .hero-slide .photo,.hero-slide.is-active .photo{ transform:none; transition:none; }
}
.hero-scrim{ position:absolute; inset:0; z-index:1; pointer-events:none;
  /* ────────────────────────────────────────────────────────────────────────
     THE HERO SCRIM — three rounds of client notes, one principle.

     The notes, in order:
       V14  reduce the wash, the photography should dominate
       V20  "we don't want a super or darkened black overlay"
       V30  "add a subtle dark gradient/overlay where required for readability"
       V31  "slightly decrease the black overlay to see more of the images"
       V33  the same again

     ⚠ TWO THINGS WERE LEARNED THE HARD WAY, AND BOTH ARE COUNTER-INTUITIVE.

     1 · THE DIAL IS WHERE THE BAND STOPS, NOT HOW DARK IT STARTS.
     V30's first attempt decayed smoothly from the bottom edge, so by the
     eyebrow — which sits 29.7% up from the bottom at 1440x900 — it had fallen
     to ~19% of peak. Raising the PEAK from .72 to .97 moved the worst measured
     eyebrow contrast only from 1.15 to 1.34:1. Holding a moderate alpha
     THROUGH the content zone and releasing it after took the same element to
     4.24 with a LOWER peak.

     2 · CUTTING THE BAND ALONE DOES NOT TRADE IMAGE FOR CONTRAST. IT JUST
     LOSES CONTRAST. V31's first attempt cut the band .72 -> .50 and left the
     pools alone: the eyebrow fell to 2.20:1 and the headline to 4.89 — worse
     than the V20 scrim this whole line of work replaced. The band had been
     doing the pools' job as well as its own.

     So every lightening since has been: BAND DOWN, POOLS UP. The band spans
     the full width across the bottom third, most of which has no text over it
     at all; the pools sit only where the type is. The photograph gets lighter
     everywhere the eye is looking at the building, and the words keep their
     ground.

       band        .72 (V30) -> .58 (V31) -> .44 (V33)    39% lighter than V30
       lead pool   .50 -> .84 -> .93,  extent 70% -> 98%
       aside pool   —  -> .60 -> .70

     Measured across all EIGHT slides at 1440x900, swept against four
     alternatives before choosing this one:
       headline 7.23:1 · description 12.95:1 · counter 10.56:1
       buttons  6.74:1 · eyebrow      4.41:1
     against V31's 7.97 / 13.17 / 11.17 / 7.94 / 4.39. The eyebrow — the one
     element that has never cleared AA, small gold caps over a blown-out pool
     on the palest slides — is UP, with the wide band 24% lighter than V31.
     Handover's recorded worst case, for scale, was 1.47:1.
     ──────────────────────────────────────────────────────────────────────── */
  background:
    /* 1 · Bottom footing, full width. Both bottom corners carry text. Held to
       32%, the top of the content block, then released by 62%. */
    /* V35 — THE BAND IS HALVED AND THE POOLS DO THE WORK.
       Third request to lighten this. The previous rounds scaled everything
       down together, which is the wrong lever: the band spans the FULL WIDTH
       of the frame and most of that width has no text over it at all, so every
       point of band alpha darkens far more photograph than it protects. It is
       cut roughly in half here and the contrast is paid for by the pools,
       which sit only where the type is. */
    /* ⚠ V97.7 — LIGHTENING THIS FURTHER WAS TRIED AND REVERTED. MEASURED.
       Bianca, 15 Sep: "lessen the black overlay on the hero sliding section
       so the background images can be seen clearly." The band went to .13,
       the pools came down with it, and the headline's worst case across the
       eight slides fell from 3.01:1 to 2.59:1 — below the 3.0 that WCAG AA
       requires for large text. Three further combinations were measured,
       moving weight out of the band and into tighter pools; the best of them
       reached 2.56.

       THE SCRIM IS AT ITS FLOOR AND HAS BEEN SINCE V35. Four rounds of
       lightening have already spent every point of alpha that was not load
       bearing. What is left is holding the headline at 3.01:1 on slide 4,
       which is AA large by one hundredth.

       Going lighter now needs a STRUCTURAL change, not a smaller number: the
       headline needs its own local backing so the whole frame no longer has
       to be darkened to protect it. Flagged to Bianca with the figures rather
       than shipped quietly at 2.59. Do not reduce these without re-running
       the contrast measurement across all eight slides — slide 4 is the one
       that fails first. */
    linear-gradient(0deg,
      rgba(16,24,32,.17) 0px,
      rgba(16,24,32,.16) 170px,
      rgba(16,24,32,.145) 280px,
      rgba(16,24,32,.10) 380px,
      rgba(16,24,32,.04) 470px,
      rgba(16,24,32,0)   560px),
    /* 2 · The pool under the LEAD column. Tall enough to reach the eyebrow at
       the top of the block — a pool sized to the headline alone stops short of
       the element that needs it most, and the description pushes the eyebrow
       higher still. */
    radial-gradient(70% 800px at 22% 100%,
      rgba(16,24,32,.47) 0%,
      rgba(16,24,32,.27) 52%,
      rgba(16,24,32,0)   88%),
    /* 3 · The pool under the ASIDE — the buttons and the control row. */
    radial-gradient(48% 430px at 84% 100%,
      rgba(16,24,32,.36) 0%,
      rgba(16,24,32,.20) 55%,
      rgba(16,24,32,0)   88%),
    /* 4 · A whisper at the very top so the transparent header bar keeps its
       contrast over a bright sky. Zero by 18%. */
    linear-gradient(180deg, rgba(16,24,32,.19) 0%, rgba(16,24,32,.06) 9%, rgba(16,24,32,0) 18%),
    /* 5 · V97.8 — THE SETTLE, copied from .cib-hero > .scrim.
       Bianca, 15 Sep, pointing at the join and at /can-i-build/: "can we try
       to do the same as what you did on the page".

       That page was rendered and MEASURED rather than read off its CSS, and
       what it actually does is three things: the hero photograph settles into
       the page ground at its base, a clean 65px band of ground follows, then
       the next section starts with a hard top edge. This layer is the first
       of the three.

       ⚠ IT IS SHORTER THAN CAN I BUILD'S. That page ramps from 62% of the
       band, which works there because its hero is shorter and its copy is
       vertically centred. On this hero the same ramp would start 344px above
       the base and run straight through the headline — and the headline is
       already at 3.01:1, AA large by one hundredth. This one starts at 84%,
       which is 145px, below everything except the buttons and the control
       row, both of which are solid fills or were measured after. Contrast
       re-run across all eight slides after adding it. */
    linear-gradient(180deg,
      rgba(16,24,32,0)   84%,
      rgba(16,24,32,.20) 91%,
      rgba(16,24,32,.62) 97%,
      var(--ink)         100%);
}
/* V55 — client: "reduce the hero background gradient darkness," in the same
   message as "remove the text shadow on the hero texts." Both together are
   the darkest-to-lightest move this scrim has ever made: the text-shadow was
   the mechanism that let the band/pools be cut as far as V35 already cut
   them ("THE SCRIM OF LAST RESORT" note above — it is what let the wide band
   stay light). Removing it while cutting the band and pools again is not two
   independent dials, it is the same contrast budget spent twice. Band cut
   ~27%, pools ~24-25% here; re-measured against all eight slides with
   herosweep.mjs after both changes — worst case landed at 2.18:1, reported
   to the client plainly rather than left undisclosed.

   V56 — client, again: "reduce a little the darkness of the hero section
   ... so the background can be seen more clearly." A further ~15% cut on
   top of V55's, same shape (band down, pools down together). This is now
   the fourth consecutive round of cutting this exact scrim (V55 was the
   third counting V30-V35 as rounds one, and this makes V56 the fourth
   overall on the darkness itself), each time already measured below AA
   before this round even started. Re-measured again after this cut — see
   the V56 release notes for the actual number, not a guess forward from
   V55's. Flagged to the client alongside the number: the text-shadow that
   used to carry this component through cuts like this is already gone
   (removed at their own request in V55), so there is no cheap lever left
   to absorb a further cut — only a deeper cut and a lower number, or
   bringing some of that shadow back. */

/* ── V33 · THE SCRIM IS A DIFFERENT PROBLEM ON A PHONE ────────────────────
   Everything above is tuned for the desktop composition: two columns of type
   in the bottom corners, so two pools sit where the type is and the wide band
   stays light. Measured at 390x844 that scrim puts the eyebrow at 1.05:1 and
   the headline at 1.92:1 — far worse than anything on desktop, and it was
   invisible until the contrast probe stopped sampling the whole element box
   and started sampling only pixels a glyph actually covers.

   Two reasons, both structural rather than a matter of degree:

   1 · The copy is HIGHER. Measured from the hero's bottom edge across 14
       viewport sizes, the eyebrow sits 272-296px up in the two-column layout
       and 337-444px up once the columns stack. As a PERCENTAGE of the frame
       that same measurement swings from 31% to 72% — which is why every stop
       in this scrim is now in px. A percentage stop moves with the viewport;
       the copy block does not.
   2 · The pools are in the WRONG PLACE. Both are anchored to a corner — 22%
       and 84% — for a two-column layout that no longer exists at this width.
       A single full-width column needs a single full-width ramp.

   ⚠ AND THE BREAKPOINT MUST BE THE LAYOUT'S. This block first shipped at
   820px, which was a guess. The hero stacks at 1199px (the site's drawer-nav
   breakpoint). Between the two, 821-1199px, the copy was already stacked and
   high while the scrim was still the two-column one — measured, iPad Pro
   portrait (1024x1366) put the headline at 3.05:1. Two breakpoints for one
   piece of geometry is one breakpoint too many.

   ⚠ AND NO RAMP ALONE FIXES THE GOLD. Measured against the actual pixel: the
   worst ground under the eyebrow is a sunlit timber panel, and taking gold
   #D9C08A to 4.5:1 against it needs roughly 0.63 of additional alpha — which
   veils the lower half of the photograph almost to black. That is the
   opposite of the instruction. So the ramp does what a ramp can (it carries
   the WHITE headline and description to AA on its own) and the eyebrow, the
   one element gold cannot carry at 10px over photography, takes paper on this
   breakpoint. The gold hairline beside it stays gold, so the brand cue is
   still made — by the rule, which has no legibility to lose. */
@media (max-width:1199px){
  .hero-scrim{
    background:
      /* One full-width ramp, held THROUGH the copy block and released above
         it — the same "hold then release" that works on desktop, re-cut for
         where the copy actually is at this width. */
      linear-gradient(0deg,
        rgba(16,24,32,.37) 0px,
        rgba(16,24,32,.36) 180px,
        rgba(16,24,32,.34) 300px,
        rgba(16,24,32,.31) 400px,
        rgba(16,24,32,.28) 470px,
        rgba(16,24,32,.15) 545px,
        rgba(16,24,32,0)   640px),
      linear-gradient(180deg, rgba(16,24,32,.21) 0%, rgba(16,24,32,.08) 10%, rgba(16,24,32,0) 20%);
  }
}
/* ⚠ The eyebrow's colour override does NOT live here.
   `.hero .hero-eyebrow{ color:var(--gold-hero) }` is declared much later in
   this file at equal specificity (0,2,0), so a copy written at this point in
   the cascade loses to it silently — the media query matches, the rule
   applies, and the later rule overwrites it. The mobile colour is set
   directly beneath that declaration instead. */
.hero-caption{ font-family:var(--mono); font-size:.72rem; letter-spacing:.14em; text-transform:uppercase;
  color:var(--muted-dk); margin-top:1.8rem; }
/* The film grain was a cream-era texture. Over clean architectural
   photography on a black ground it reads as compression noise, so it is
   dialled almost out rather than removed — the element stays in the
   markup and one number brings it back. */
.hero-grain{ position:absolute; inset:0; z-index:1; opacity:.02; pointer-events:none; mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.32'/%3E%3C/svg%3E"); }
.hero-inner{ position:relative; z-index:2; width:100%; padding-block:clamp(84px,11vh,132px); }

/* ---- V30 hero foot: lead bottom-LEFT, actions + controls bottom-RIGHT ----
   Client: "Position the hero content bottom-left, similar to PIQUE. Buttons
   and small text to the bottom-right."

   Two columns on one baseline. The lead column is `1fr` and the aside is
   `auto`, so the headline takes whatever the buttons do not need and wraps
   before it can ever collide with them. `align-items:end` puts both columns on
   the same bottom edge regardless of how many lines the headline runs to. */
.hero-foot{ position:relative; z-index:2; width:100%;
  /* V134 — STILL TWO COLUMNS. Max, 22 September, asks for three things across
     the bottom: "Left: Homepage CTA buttons. Centre: Background slideshow
     controls. Right: Video panels + 3D walkthrough CTA/HIDE controls."

     ⚠ THE CONTROLS ARE NOT A COLUMN, AND THE FIRST ATTEMPT PROVED WHY.
     A third grid column took 317px out of the headline's column at 1440 and
     the headline was CLIPPED — "Premium Modula" with the rest cut off. The
     hero heading is split into per-line spans for the reveal animation, so it
     cannot reflow into a narrower box; it just overflows.

     The controls are therefore centred over the foot instead, absolutely, so
     they cost the headline nothing and land on the true horizontal centre of
     the hero rather than wherever a content-sized column happened to fall.
     See .hero-slider-ui below. */
  display:grid; grid-template-columns:1fr auto; align-items:end;
  gap:clamp(24px,4vw,72px);
  padding-top:clamp(120px,16vh,200px);
  padding-bottom:clamp(38px,6.5vh,72px); }
.hero-lead{ min-width:0; }
.hero-aside{ display:flex; flex-direction:column; align-items:flex-end; gap:clamp(14px,2vh,22px); }

/* 1.6rem put the eyebrow at the very top edge of the scrim's held zone. 1.05
   drops it ~10px into the dense part, which is worth ~0.3 of contrast ratio
   for nothing but a tighter, more deliberate-looking pairing with the
   headline it labels. */
.hero-eyebrow{ margin-bottom:1.05rem; }
/* A shadow EDGE, not a glow. One tight offset dark enough to separate the
   glyph from a blown-out highlight behind it. The eyebrow is small gold caps
   and is the one hero element the scrim cannot get to AA on the palest
   slides, so it carries the heavier of the two. */
/* ── V35 · THE SCRIM OF LAST RESORT: A SHADOW ─────────────────────────────
   Fourth request to lighten the hero. The band-and-pools lever is spent — six
   candidate scrims were measured for contrast AND for how much of the
   photograph each veils, and the shipped one already sat on the efficient
   frontier: everything lighter failed AA, everything that passed veiled as
   much or more.

   A text-shadow is the same idea taken to its limit. The progression has been
   band (darkens the whole width) → pools (darken where the text block is) →
   shadow (darkens only the pixels touching each letter). It buys contrast at
   the finest grain there is, and costs the photograph almost nothing, which is
   exactly the trade being asked for.

   Kept TIGHT on purpose: a 1–2px offset plus a small ambient spread. A large
   soft glow is what makes shadowed type look cheap, and it also stops helping
   — contrast comes from the pixels immediately beside the stroke.

   ⚠ THE CONTRAST PROBE COULD NOT SEE THIS UNTIL IT WAS FIXED. It ghosted the
   type with `visibility:hidden`, which removes the shadow along with the
   glyphs, so a well-shadowed headline scored identically to an unshadowed one.
   It now ghosts with `color:transparent`, which drops the fill and leaves the
   shadow painting. See qa/herosweep.mjs.

   V55 — client: "remove the text shadow on the hero texts," in the same
   message as "reduce the hero background gradient darkness." Removed below,
   as asked. Leaving the paragraphs above intact rather than deleting them:
   this was the mechanism that let the scrim be cut as far as it already was
   across V30-V35, and it is now gone at the same time the scrim is being cut
   AGAIN — the actual measured contrast after both changes together is in the
   V55 release notes, not asserted here. If a slide reads as too washed-out
   once this ships, this history is why, and the shadow is the fastest lever
   back. */
.hero h1{ max-width:12ch; color:var(--paper); }
.hero .btn.ghost{ --bd:rgba(255,255,255,.52); }
.hero h1 .line{ display:block; overflow:hidden; padding-bottom:.12em; margin-bottom:-.07em; }
.hero h1 .line > span{ display:block; transform:translateY(103%); animation:heroUp 1.1s var(--ease-out) forwards; }
.hero h1 .line:nth-child(1) > span{ animation-delay:.18s; }
.hero h1 .line:nth-child(2) > span{ animation-delay:.30s; }
.hero h1 .line:nth-child(3) > span{ animation-delay:.42s; }
.hero h1 .line:nth-child(4) > span{ animation-delay:.54s; }
@keyframes heroUp{ to{ transform:translateY(0); } }

/* ---- V31 · the hero description ----
   V55 — client asked to remove "Factory-built in weeks. Delivered
   Australia-wide." outright (tpl-home.php). That paragraph was the ONLY use
   of `.hero-desc` in the theme (grepped) — the rule below has nothing left
   to style, so it goes with the markup rather than sitting as dead CSS. */

.hero-actions{ display:flex; flex-wrap:wrap; gap:1rem; margin-top:2.2rem; align-items:center; }
/* V134 — THE LEFT BUTTONS GET A CEILING, so they cannot reach the centred
   slideshow controls.
   Moving the controls out of the aside gave the headline column back its full
   width, and the two buttons — which had been wrapping onto separate lines in
   a 415px column — promptly sat side by side and ran under the control row.
   Caught in a screenshot, not in the numbers: the boxes did not overlap, the
   ink did.
   430px is measured: EXPLORE THE COLLECTION is 186px and BOOK A SHOWROOM
   VISIT is 250px, so at 430 they cannot share a line and the stack returns.
   Scoped above the stacking breakpoint, since below it the buttons go full
   width by design. */
@media (min-width:1200px){
  .hero-lead .hero-actions{ max-width:430px; }
}
/* Inside the V30 aside the actions are a grid child on their own baseline —
   the 2.2rem top margin was spacing them off a headline that is now in the
   other column, and it pushed the whole aside off the shared bottom edge.

   They STACK on desktop, and that is a considered choice rather than a
   fallback. Side by side the pair is ~520px wide, which leaves the headline
   column about 770px at 1440 — and "Premium Modular Granny Flat" set nowrap
   at the display size needs ~850px. The line was being clipped by its own
   reveal mask (.line{overflow:hidden}), which hides an overflow rather than
   showing it, so it read as a missing word rather than as a layout fault.
   Stacking takes the corner block to ~260px and hands the headline back its
   full size — and the headline is the page's primary statement, so it is the
   one that should win the argument for space. */
.hero-aside .hero-actions{ margin-top:0; flex-direction:column; align-items:stretch; gap:.7rem; width:max-content; }
.hero-aside .hero-actions .btn{ justify-content:space-between; }
@media (max-width:1199px){
  .hero-aside .hero-actions{ flex-direction:row; align-items:center; gap:1rem; width:auto; }
  .hero-aside .hero-actions .btn{ justify-content:center; }
}
.hero-meta{ position:absolute; right:var(--edge); bottom:clamp(48px,8vh,96px); z-index:2;
  text-align:right; display:flex; flex-direction:column; gap:.4rem; }
.hero-meta .k{ font-family:var(--mono); font-size:.66rem; letter-spacing:.22em; text-transform:uppercase; color:var(--muted-2); }
.hero-meta .v{ font-family:var(--display); font-size:clamp(1.4rem,2.2vw,2rem); font-weight:340; }
.hero-scroll{ position:absolute; right:clamp(14px,2vw,30px); top:50%; transform:translateY(-50%); z-index:3;
  display:inline-block; text-align:center;
  background:var(--ink-3); color:var(--paper); border:1px solid var(--line-gold);
  padding:1.5rem .95rem; border-radius:999px;
  writing-mode:vertical-rl; font-family:var(--mono); font-size:.62rem; letter-spacing:.3em;
  text-transform:uppercase; text-decoration:none; cursor:pointer; white-space:nowrap;
  box-shadow:0 14px 34px rgba(22,19,13,.28); transition:background .4s var(--ease), transform .4s var(--ease); }
.hero-scroll:hover{ background:var(--gold); color:var(--ink); border-color:var(--gold); transform:translateY(-50%) scale(1.04); }
/* Arrow flows inline right after the word — same vertical line, just below the final "l". */
.hero-scroll .chev{ display:inline-block; writing-mode:horizontal-tb; margin-top:.55rem; animation:chevNudge 2.2s var(--ease) infinite; }
@keyframes chevNudge{ 0%,100%{ transform:translateY(0);} 50%{ transform:translateY(5px);} }

/* ---- Homepage slider controls (V30: prev / counter / tick rail / next) ----
   The thumbnail strip is gone. Eight photographic thumbnails at 38x26 read as
   clutter under a full-bleed hero and, at 62vw, were the widest thing in the
   bottom-right corner — where the client now wants the buttons. A counter plus
   a tick rail states the same two facts (where you are, how many there are) in
   a fraction of the width and in the site's own technical register. */
.hero-slider-ui{ position:relative; z-index:3;
  /* V134 — CENTRED OVER THE FOOT, not a grid column. It used to be the last
     child of .hero-aside, directly beneath the video panels, which is exactly
     why it read as belonging to them. Now it is pinned to the horizontal
     centre of .hero-foot, on the same baseline as the buttons on the left and
     the media group on the right.

     Measured at 1440: the row is 317px wide centred on 720, so it spans
     561-878. The buttons on the left end at 320 and the media group starts at
     899. Twenty-one pixels of clearance on the right and 241 on the left —
     "enough separation between these elements so it remains visually clear
     that the centre controls operate the background slideshow". */
  position:absolute; left:50%; transform:translateX(-50%);
  bottom:clamp(38px,6.5vh,72px);
  display:flex; align-items:center; gap:.85rem;
  /* V134 — WAS width:100%. That resolved against .hero-aside, which no longer
     contains this row, so 100% would now resolve against a grid column sized
     by this element itself: circular, and the rail collapsed to the width of
     eight 6px marks. A measured width instead. 300px holds the counter, the
     eight-segment rail and the three buttons at 1440 without the rail
     becoming the widest thing in the hero. */
  width:clamp(230px, 22vw, 320px); }
/* V33 — the hairline above the actions is REMOVED at the client's request
   ("remove the line above Explore the Collection, it's unnecessary").
   It was there to close the corner block into one object; the buttons and the
   control row already read as a group without it, and over photography one
   more horizontal element is one more thing competing with the roofline. */
.hsl-arrow{ flex:none; width:28px; height:28px; border-radius:50%; border:1px solid rgba(255,255,255,.42);
  background:rgba(11,17,22,.58); color:#fff; display:inline-flex; align-items:center; justify-content:center;
  cursor:pointer; -webkit-backdrop-filter:blur(4px); backdrop-filter:blur(4px);
  transition:background .3s var(--ease), border-color .3s var(--ease), color .3s var(--ease); }
.hsl-arrow:hover{ background:var(--gold); border-color:var(--gold); color:var(--ink); }
.hsl-arrow svg{ width:13px; height:13px; }

/* Counter — the technical register: 600 uppercase, tracked out, tabular. */
.hsl-count{ margin:0; flex:none; display:inline-flex; align-items:baseline; gap:.28em;
  font-family:var(--mono); font-size:.72rem; font-weight:600; letter-spacing:.15em;
  font-variant-numeric:tabular-nums; color:var(--paper); }
.hsl-count .hsl-sep{ opacity:.5; }
.hsl-count .hsl-total{ opacity:.62; }

/* ---- V31 · a continuous progress track, not eight discrete ticks ----
   The client kept the counter and asked for a more premium treatment. Eight
   separate marks stated the position eight times and read as a widget; one
   track that FILLS across the hold states the same position continuously and
   tells you how long is left, which the ticks never did.

   Each segment is still a button, so per-slide jumping survives — the control
   is one object made of eight parts rather than eight objects. */
.hsl-rail{ display:flex; align-items:center; gap:3px; }
.hsl-seg{ flex:1 1 auto; min-width:14px; max-width:34px; height:24px; padding:0; border:0;
  background:none; cursor:pointer; display:inline-flex; align-items:center; }
.hsl-seg i{
  display:block; width:100%; height:2px; border-radius:2px;
  background:rgba(255,255,255,.26);
  position:relative; overflow:hidden;
  transition:background .35s var(--ease);
}
/* The fill lives in ::after so the track's own tint stays visible behind it. */
.hsl-seg i::after{
  content:""; position:absolute; inset:0;
  background:var(--gold);
  transform:scaleX(0); transform-origin:left center;
}
.hsl-seg:hover i{ background:rgba(255,255,255,.5); }
/* Slides already shown read as complete. */
.hsl-seg.is-done i::after{ transform:scaleX(1); opacity:.55; }
/* The live segment fills across the hold. The duration is --hero-hold, the
   same custom property the JS reads for its interval, so the bar cannot
   finish early or late. Restarted by the JS toggling .is-active. */
.hsl-seg.is-active i::after{
  animation:hslFill var(--hero-hold,3000ms) linear forwards;
}
@keyframes hslFill{ from{ transform:scaleX(0); } to{ transform:scaleX(1); } }
/* Paused: the bar holds wherever it is rather than snapping back. */
.hero-slider-ui.is-paused .hsl-seg.is-active i::after{ animation-play-state:paused; }
.hsl-seg:focus-visible{ outline:2px solid var(--gold-hi); outline-offset:3px; border-radius:3px; }

/* Under reduced motion there is no autoplay, so a filling bar would be a
   permanently empty one. The live segment simply reads as full. */
@media (prefers-reduced-motion:reduce){
  .hsl-seg.is-active i::after{ animation:none; transform:scaleX(1); }
}

/* Play / pause — the same pill as the arrows; only one icon shows at a time. */
.hsl-controls{ display:flex; align-items:center; gap:.4rem; flex:none; }
.hsl-play .hsl-i-play{ display:none; }
.hero-slider-ui.is-paused .hsl-play .hsl-i-pause{ display:none; }
.hero-slider-ui.is-paused .hsl-play .hsl-i-play{ display:block; }
.hsl-play svg{ width:12px; height:12px; }

@media (max-width:640px){
  .hsl-arrow{ width:26px; height:26px; }
  .hsl-seg{ min-width:10px; height:22px; }
  .hero-slider-ui{ gap:.55rem; }
}
@media (prefers-reduced-motion:reduce){ .hero-scroll .chev{ animation:none; } }

@media (max-width:860px){
  .hero-meta{ display:none; }
}

/* ---- V30 hero foot, responsive ----
   Two columns need room for a headline AND two buttons on one line. Below
   1024px they stack: lead first, then actions and controls, both still
   bottom-left-to-right in reading order. The aside stops right-aligning at the
   same breakpoint — a right-aligned button pair under a left-aligned headline
   reads as a mistake, not as a composition. */
/* 1200px is the site's existing desktop breakpoint (below 1201 the nav becomes
   the drawer), so the hero changing shape at the same width reads as one
   decision rather than two. */
@media (max-width:1199px){
  .hero-foot{ grid-template-columns:1fr; align-items:start; gap:clamp(20px,3.4vh,32px); }
  .hero-aside{ align-items:flex-start; }
  /* ⚠ V134 — THE COLUMN NUMBERS HAVE TO BE UNSET HERE, NOT JUST IGNORED.
     The desktop rules pin the controls to column 2 and the aside to column 3.
     This block drops the grid to ONE column, so those two pins created two
     implicit columns off the right-hand edge and the media group and the
     control row left the viewport. Caught by measuring element positions at
     900 and 390, where the CTA sat at x=307 in a 390-wide window — not by
     looking at the desktop view, where everything was correct.

     Order is set explicitly rather than left to the DOM, because the DOM now
     reads lead, controls, aside so that the desktop arrangement matches
     keyboard order. Stacked, the client's own arrangement is headline, then
     videos, then the slideshow controls under them. */
  /* Stacked, the centred overlay makes no sense and would sit on top of the
     content, so it goes back into the flow underneath everything. */
  .hero-slider-ui{ position:static; transform:none; order:3; width:100%; max-width:360px; }
  .hero-aside{ order:2; }
  .hero-lead{ order:1; }
}
@media (max-width:640px){
  .hero-foot{ padding-top:clamp(96px,20vh,150px); padding-bottom:clamp(28px,5vh,46px); }
  .hero-actions{ width:100%; }
  .hero-actions .btn{ flex:1 1 auto; justify-content:center; }
}
/* The vertical SCROLL pill is centred on the right edge. Once the actions move
   under the headline on a narrow screen they reach the right edge too, and the
   pill lands on top of them. It is decorative — the hero has a scroll cue in
   the slide advance itself — so it goes rather than fighting for the corner. */
@media (max-width:768px){
  .hero > .hero-scroll{ display:none; }
}

/* ============================================================
   MARQUEE (trust / logos strip)
   ============================================================ */
.marquee{ border-block:1px solid var(--line-soft); overflow:hidden; padding-block:1.5rem; background:var(--ink); }
.marquee-track{ display:flex; gap:4rem; width:max-content; animation:marq 34s linear infinite; }
.marquee:hover .marquee-track{ animation-play-state:paused; }
.marquee-track span{ font-family:var(--display); font-style:italic; font-size:1.15rem; color:var(--muted-2); display:flex; align-items:center; gap:4rem; white-space:nowrap; }
.marquee-track span::after{ content:"◆"; font-style:normal; font-size:.5rem; color:var(--gold); }
@keyframes marq{ to{ transform:translateX(-50%); } }

/* ============================================================
   MODEL CARDS
   ============================================================ */
.model-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:clamp(18px,2vw,30px); }
.model-card{ position:relative; background:var(--card); border:1px solid var(--line-soft);
  display:flex; flex-direction:column; transition:border-color .5s var(--ease), transform .5s var(--ease-out), box-shadow .5s var(--ease); }
.model-card:hover{ border-color:var(--line-gold); transform:translateY(-4px);
  box-shadow:0 30px 60px -34px rgba(0,0,0,.72); }
.model-media{ position:relative; aspect-ratio:16/11; overflow:hidden; background:var(--paper-3); }
.model-media svg{ width:100%; height:100%; }
.model-media .badge{ position:absolute; top:14px; left:14px; z-index:3; font-family:var(--mono);
  font-size:.6rem; letter-spacing:.16em; text-transform:uppercase; color:var(--ink);
  background:var(--gold-glow); padding:.42em .7em; display:inline-flex; align-items:center; gap:.4em; }
.model-media .badge.fixed{ background:transparent; color:var(--muted); border:1px solid var(--line); }
.model-media .code{ position:absolute; bottom:12px; right:14px; z-index:3; font-family:var(--mono);
  font-size:.6rem; letter-spacing:.14em; color:var(--muted-2); }
.model-media .imgnote{ position:absolute; bottom:12px; left:14px; z-index:3; font-family:var(--mono);
  font-size:.54rem; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); opacity:.7; }
.model-body{ padding:1.4rem 1.5rem 1.6rem; display:flex; flex-direction:column; gap:.5rem; flex:1; }
.model-body .name{ font-family:var(--display); font-size:1.5rem; font-weight:360; letter-spacing:-.01em; }
.model-body .specs{ font-family:var(--mono); font-size:.74rem; letter-spacing:.06em; color:var(--muted); }
.model-body .foot{ margin-top:auto; padding-top:1.1rem; display:flex; align-items:center; justify-content:space-between;
  border-top:1px solid var(--line-soft); }
.model-body .price{ font-family:var(--display); font-size:1.02rem; color:var(--paper); }
.model-body .price small{ font-family:var(--mono); font-size:.6rem; letter-spacing:.14em; text-transform:uppercase; color:var(--muted-2); display:block; }

/* ============================================================
   EXPANSION VISUALIZER — signature
   ============================================================ */
.evz{ display:grid; grid-template-columns:1.05fr .95fr; gap:clamp(28px,4vw,72px); align-items:start; }

/* V15 (#2) — Future-Proof module picker. These classes were used in markup but
   never styled, so the buttons rendered unstyled. Two-column grid of module
   buttons, matched to the .evz-btn look, so the section reads as one system. */
.msz-pick-grid{ display:grid; grid-template-columns:1fr 1fr; gap:.55rem; margin:1.8rem 0 .4rem; }
.msz-pick{ display:flex; align-items:center; justify-content:space-between; gap:1rem;
  padding:.8rem .95rem; border:1px solid var(--line); background:var(--card); cursor:pointer;
  text-align:left; border-radius:2px;
  transition:border-color .3s var(--ease), background .3s var(--ease), transform .3s var(--ease); }
.msz-pick:hover{ border-color:var(--line-gold); }
.msz-pick[aria-pressed="true"]{ border-color:var(--gold); background:var(--wash-gold); }
.msz-pick .pl{ display:flex; flex-direction:column; gap:.18rem; min-width:0; }
.msz-pick .pn{ font-family:var(--display); font-size:1rem; font-weight:360; line-height:1.12; }
.msz-pick .pc{ font-family:var(--mono); font-size:.56rem; letter-spacing:.12em; text-transform:uppercase; color:var(--muted-2); }
.msz-pick .ps{ flex:none; font-family:var(--mono); font-size:.74rem; font-weight:700; color:var(--gold-deep); }
.msz-pick[aria-pressed="true"] .pn{ color:var(--gold-deep); }
@media (max-width:600px){ .msz-pick-grid{ grid-template-columns:1fr; } }
.evz-stage{ position:relative; background:var(--card); border:1px solid var(--line-gold);
  aspect-ratio:5/4; overflow:hidden; }
.evz-stage .grid-bg{ position:absolute; inset:0; opacity:.5;
  background-image:linear-gradient(var(--line-soft) 1px,transparent 1px),linear-gradient(90deg,var(--line-soft) 1px,transparent 1px);
  background-size:34px 34px; }
.evz-plan{ position:absolute; inset:0; }
.evz-stage .stagelabel{ position:absolute; top:16px; left:18px; font-family:var(--mono); font-size:.62rem;
  letter-spacing:.18em; text-transform:uppercase; color:var(--gold-deep); z-index:4; }
.evz-stage .compass{ position:absolute; bottom:16px; right:18px; font-family:var(--mono); font-size:.58rem;
  letter-spacing:.12em; color:var(--muted-2); z-index:4; display:flex; align-items:center; gap:.4em; }
/* animated module reveal */
.mod{ opacity:0; transform:translateY(8px) scale(.985); transform-origin:left center;
  transition:opacity .6s var(--ease), transform .6s var(--ease-out); }
.mod.show{ opacity:1; transform:none; }
.mod .ghost{ stroke-dasharray:5 5; }

.evz-panel .steps{ display:flex; gap:.6rem; margin:1.6rem 0 1.9rem; }
.evz-btn{ flex:1; padding:.9rem .6rem; border:1px solid var(--line); background:var(--card);
  display:flex; flex-direction:column; gap:.35rem; align-items:flex-start; text-align:left;
  transition:border-color .35s var(--ease), background .35s var(--ease), transform .35s var(--ease); }
.evz-btn .n{ font-family:var(--mono); font-size:.62rem; letter-spacing:.14em; color:var(--muted-2); }
.evz-btn .t{ font-family:var(--display); font-size:1.18rem; font-weight:360; line-height:1; }
.evz-btn small{ font-family:var(--mono); font-size:.6rem; color:var(--muted-2); letter-spacing:.04em; }
.evz-btn[aria-pressed="true"]{ border-color:var(--gold); background:var(--wash-gold); }
.evz-btn[aria-pressed="true"] .n{ color:var(--gold-deep); }
.evz-btn:hover{ border-color:var(--line-gold); }
.evz-readout{ display:flex; gap:2.2rem; padding-top:1.6rem; border-top:1px solid var(--line); }
.evz-readout .r .k{ font-family:var(--mono); font-size:.62rem; letter-spacing:.14em; text-transform:uppercase; color:var(--muted-2); display:block; margin-bottom:.3rem; }
.evz-readout .r .val{ font-family:var(--display); font-size:clamp(1.6rem,2.4vw,2.2rem); font-weight:340; line-height:1; }
.evz-readout .r .val em{ font-size:.5em; color:var(--muted-2); font-style:normal; font-family:var(--mono); letter-spacing:.06em; }
.count{ font-variant-numeric:tabular-nums; }

/* ============================================================
   PROCESS TIMELINE
   ============================================================ */
/* V120 — THE TRACK COUNT NOW FOLLOWS THE CARD COUNT.
   Bianca, 17 Sep, on the Finance page's "Three steps to an indicative number":
   "Can we improve the 3rd image section". The component was hard-coded to four
   columns and that section has three cards, so a quarter of the row was empty
   and the three cards sat squashed to the left of a gap. `auto-fit` sizes the
   track to what is actually in the grid, so three cards fill the row and a
   four-card .proc elsewhere is unchanged. Nothing else about the component
   moves: same borders, same padding, same numerals. */
.proc{ display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:0;
  border-top:1px solid var(--line-gold); }
.proc-step{ position:relative; padding:2rem 1.4rem 2.4rem; border-right:1px solid var(--line-soft); border-bottom:1px solid var(--line-soft); }
/* The right-hand border belongs to the last card in the row, whatever the row
   holds. :nth-child(4n) was the four-column assumption written a second time. */
.proc-step:last-child{ border-right:0; }
.proc-step .n{ font-family:var(--mono); font-size:.66rem; letter-spacing:.16em; color:var(--gold-deep); }
.proc-step .big{ font-family:var(--display); font-size:2.6rem; font-weight:300; color:rgba(179,148,94,.34);
  line-height:1; margin:.2rem 0 1rem; transition:color .5s var(--ease); }
.proc-step:hover .big{ color:var(--gold); }
.proc-step h4{ font-size:1.16rem; margin-bottom:.5rem; }
.proc-step p{ font-size:.9rem; }
.proc-step .ico{ width:34px; height:34px; margin-bottom:1rem; color:var(--gold-deep); }
.bg-ink .proc,.bg-green .proc{ border-top-color:var(--line-gold); }
.bg-ink .proc-step,.bg-green .proc-step{ border-color:rgba(255,255,255,.09); }
.bg-ink .proc-step .ico,.bg-green .proc-step .ico{ color:var(--gold-glow); }
.bg-ink .proc-step .big,.bg-green .proc-step .big{ color:rgba(255,255,255,.12); }
.bg-ink .proc-step:hover .big,.bg-green .proc-step:hover .big{ color:var(--gold); }

/* ============================================================
   FEATURE SPLIT (future-proof / finance)
   ============================================================ */
.split{ display:grid; grid-template-columns:1fr 1fr; gap:clamp(28px,4vw,80px); align-items:center; }
.split.rev .split-media{ order:2; }
.split-media{ position:relative; aspect-ratio:4/5; background:var(--paper-3); border:1px solid var(--line-soft); overflow:hidden; }
.split-media.tall{ aspect-ratio:3/4; }
.split-media svg{ width:100%; height:100%; }
.split-copy .stat-row{ display:flex; gap:2.4rem; margin-top:1.8rem; }
.stat .num{ font-family:var(--display); font-size:clamp(2rem,3vw,2.8rem); font-weight:320; line-height:1; }
.stat .lbl{ font-family:var(--mono); font-size:.64rem; letter-spacing:.14em; text-transform:uppercase; color:var(--muted-2); margin-top:.4rem; }
.tick-list{ display:flex; flex-direction:column; gap:.9rem; margin-top:1.7rem; }
.tick-list li{ display:flex; gap:.9rem; align-items:flex-start; font-size:1rem; color:var(--ink-soft); }
.tick-list li::before{ content:""; flex:none; width:18px; height:18px; margin-top:3px;
  background:var(--gold); clip-path:polygon(41% 66%,78% 26%,88% 36%,41% 86%,14% 56%,24% 46%); }
.bg-ink .tick-list li,.bg-green .tick-list li{ color:var(--paper); }

/* ============================================================
   REFERRAL / QR BANNER
   ============================================================ */
.referral{ position:relative; overflow:hidden; }
.referral .amt{ font-family:var(--display); font-weight:300; font-size:clamp(4rem,13vw,11rem); line-height:.86;
  letter-spacing:-.04em; color:var(--gold); }
.referral .amt sup{ font-size:.34em; vertical-align:top; top:.2em; position:relative; }
.editable{ position:relative; }
.editable::after{ content:"CMS-editable"; position:absolute; top:-6px; right:-8px; transform:translateX(100%);
  font-family:var(--mono); font-size:.5rem; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2);
  border:1px dashed var(--line-gold); padding:2px 5px; white-space:nowrap; }
@media(max-width:900px){ .editable::after{ display:none; } }

.qr-card{ display:grid; grid-template-columns:auto 1fr; gap:1.6rem; align-items:center;
  background:var(--ink-2); border:1px solid rgba(255,255,255,.1); padding:1.6rem; }
.qr-card .qr{ width:120px; height:120px; background:var(--paper); padding:10px; }
.qr-card .qr svg{ width:100%; height:100%; }
.qr-card h4{ color:var(--paper); font-size:1.2rem; margin-bottom:.4rem; }
.qr-card p{ color:var(--muted-dk); font-size:.86rem; }

/* ============================================================
   GALLERY
   ============================================================ */
.gal-tabs{ display:flex; flex-wrap:wrap; gap:.5rem; }
.gal-tab{ font-family:var(--mono); font-size:.72rem; letter-spacing:.12em; text-transform:uppercase;
  padding:.6em 1.1em; border:1px solid var(--line); color:var(--muted); transition:.35s var(--ease); }
.gal-tab[aria-pressed="true"]{ background:var(--gold); color:var(--ink); border-color:var(--gold); }
.gal-tab:hover{ border-color:var(--gold); color:var(--paper); }
.gal-grid{ display:grid; grid-template-columns:repeat(12,1fr); gap:14px; margin-top:2rem; grid-auto-flow:dense; }
.gal-item{ position:relative; overflow:hidden; background:var(--paper-3); border:1px solid var(--line-soft);
  min-height:200px; transition:opacity .4s var(--ease), transform .5s var(--ease); }
.gal-item svg{ width:100%; height:100%; position:absolute; inset:0; }
.gal-item .cap{ position:absolute; bottom:12px; left:14px; z-index:2; font-family:var(--mono); font-size:.6rem;
  letter-spacing:.1em; text-transform:uppercase; color:var(--paper); background:color-mix(in srgb,var(--ink) 78%,transparent);
  padding:.3em .6em; }
.gal-item.wide{ grid-column:span 6; } .gal-item.tall{ grid-column:span 4; grid-row:span 2; }
.gal-item.reg{ grid-column:span 4; } .gal-item.big{ grid-column:span 8; grid-row:span 2; }
.gal-item[hidden]{ display:none; }

/* Uniform gallery — every tile the same size (townhouse + completed galleries).
   Overrides any big/wide/tall accents so the grid stays consistent and appealing. */
.gal-grid--uniform{ grid-auto-flow:row; }
.gal-grid--uniform .gal-item,
.gal-grid--uniform .gal-item.reg,
.gal-grid--uniform .gal-item.wide,
.gal-grid--uniform .gal-item.tall,
.gal-grid--uniform .gal-item.big{
  grid-column:span 4; grid-row:auto; aspect-ratio:4/3; min-height:0;
}
@media (max-width:900px){
  .gal-grid--uniform .gal-item,
  .gal-grid--uniform .gal-item.reg,
  .gal-grid--uniform .gal-item.wide,
  .gal-grid--uniform .gal-item.tall,
  .gal-grid--uniform .gal-item.big{ grid-column:span 6; }
}
@media (max-width:560px){
  .gal-grid--uniform .gal-item,
  .gal-grid--uniform .gal-item.reg,
  .gal-grid--uniform .gal-item.wide,
  .gal-grid--uniform .gal-item.tall,
  .gal-grid--uniform .gal-item.big{ grid-column:1 / -1; }
}

/* ============================================================
   FORMS
   ============================================================ */
.form-card{ background:var(--card); border:1px solid var(--line-soft); padding:clamp(1.6rem,3vw,2.6rem); }
.bg-ink .form-card,.bg-green .form-card{ background:rgba(0,0,0,.24); border-color:rgba(255,255,255,.14); }
.field{ display:flex; flex-direction:column; gap:.5rem; margin-bottom:1.15rem; }
.field label{ font-family:var(--mono); font-size:.66rem; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); }
.bg-ink .field label,.bg-green .field label{ color:var(--muted-dk); }
.field input,.field select,.field textarea{
  font-family:var(--body); font-size:1rem; color:var(--paper); background:var(--ink-2);
  border:1px solid var(--line-ui); padding:.9em 1em; border-radius:0; width:100%;
  transition:border-color .35s var(--ease); }
.bg-ink .field input,.bg-ink .field select,.bg-ink .field textarea,.bg-green .field input,.bg-green .field select,.bg-green .field textarea{ background:rgba(255,255,255,.04); color:var(--paper); border-color:rgba(255,255,255,.14); }
.field input:focus,.field select:focus,.field textarea:focus{ outline:none; border-color:var(--gold); }
.field textarea{ resize:vertical; min-height:120px; }
.field.row2{ display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
.form-note{ font-family:var(--mono); font-size:.64rem; letter-spacing:.06em; color:var(--muted-2); margin-top:.4rem; }
/* time chips for showroom */
.chip-row{ display:flex; flex-wrap:wrap; gap:.55rem; }
.chip{ font-family:var(--mono); font-size:.74rem; letter-spacing:.05em; padding:.6em 1em; border:1px solid var(--line);
  color:var(--paper); background:var(--ink-2); transition:.3s var(--ease); }
.bg-ink .chip,.bg-green .chip{ background:rgba(255,255,255,.04); color:var(--muted-dk); border-color:rgba(255,255,255,.14); }
.chip[aria-pressed="true"]{ background:var(--gold); color:var(--ink); border-color:var(--gold); }
.chip:hover{ border-color:var(--gold); }
.form-success{ display:none; align-items:center; gap:.7rem; font-family:var(--mono); font-size:.8rem;
  letter-spacing:.06em; color:var(--gold-deep); padding:1rem; border:1px dashed var(--line-gold); margin-top:1rem; }
.form-success.show{ display:flex; }

/* V47 — client: "The form on homepage is not the same with the other form
   colors of the other pages." Every other enquiry form on the site
   (.enq-panel / .enq-formwrap, template-parts/enquiry-form.php) is solid
   dark navy — --card and --ink-2 are both #16212C — with a gold hairline
   (--line-gold) and --muted label text, regardless of what background the
   section around it sits on. This page's booking form (#book, in
   tpl-home.php) is markup-different — a standalone .form-card, not the
   shared two-column panel — and it sits inside the "one green block per
   page" closing band (see WHERE THE GREEN GOES above, still correct: the
   green stays). But .form-card falls under the generic `.bg-green
   .form-card` rule two blocks up, which paints it as translucent glass
   over the green instead of the same navy card every other form uses. An
   ID selector outranks two classes on specificity alone, so #book wins
   here regardless of source order (project rule #17) — this restores the
   same navy/gold look as every other page's form without touching the
   green copy column beside it or the generic bg-green/bg-ink rules, which
   nothing else on the site currently uses on a .form-card. */
#book.form-card{ background:var(--card); border-color:var(--line-gold); }
#book.form-card .field label{ color:var(--muted); }
#book.form-card .field input,
#book.form-card .field select,
#book.form-card .field textarea{ background:var(--ink-2); color:var(--paper); border-color:var(--line-ui); }
#book.form-card .chip{ background:var(--ink-2); color:var(--paper); border-color:var(--line); }

/* ============================================================
   CATALOGUE (designs.html)
   ============================================================ */
.page-hero{ padding-block:clamp(96px,13vh,150px); position:relative; }
.page-hero .eyebrow{ margin-bottom:1.4rem; }
.page-hero h1{ color:var(--paper); }
.page-hero .lead{ color:var(--paper); }
.filter-bar{ display:flex; flex-wrap:wrap; gap:.5rem; align-items:center; padding-block:1.4rem;
  border-block:1px solid var(--line-soft); position:sticky; top:76px; z-index:40; background:var(--ink);
  backdrop-filter:blur(10px); }
.filter-bar .fl-label{ font-family:var(--mono); font-size:.64rem; letter-spacing:.16em; text-transform:uppercase; color:var(--muted-2); margin-right:.6rem; }
.fbtn{ font-family:var(--mono); font-size:.72rem; letter-spacing:.08em; padding:.55em 1em; border:1px solid var(--line); color:var(--muted); transition:.3s var(--ease); }
.fbtn[aria-pressed="true"]{ background:var(--gold); color:var(--ink); border-color:var(--gold); }
.fbtn:hover{ border-color:var(--gold); color:var(--paper); }
.fbtn .ct{ opacity:.5; margin-left:.5em; }
.model-card[hidden]{ display:none; }

/* ============================================================
   MODEL DETAIL (model.html)
   ============================================================ */
.md-hero{ display:grid; grid-template-columns:1.4fr 1fr; min-height:70vh; }
/* V50 · item 1 — a grid item's default `min-width:auto` lets its column
   track be sized by the item's own min-content width rather than the
   container. `.m-info`'s content (the CTA row's two buttons, un-wrapped
   below their own combined min-content) was the one actually driving it —
   confirmed with computed `grid-template-columns`, which read 397.03px on a
   390px-wide Man Cave page (the model with the longest CTA-button pair):
   the single `1fr` track was sized to fit `.m-info`'s content, not the
   viewport, and `.m-main` (and its `<img>`) just matched whatever width
   that produced. `min-width:0` on both grid items is the standard fix for
   this exact class of overflow; applied to both since either one's content
   could end up the wider of the two on a different model. */
.md-hero .m-main{ position:relative; background:var(--paper-3); overflow:hidden; min-width:0; }
.md-hero .m-info{ min-width:0; }
.md-hero .m-main svg{ width:100%; height:100%; position:absolute; inset:0; object-fit:cover; }
.md-hero .m-info{ background:var(--green); color:var(--paper); padding:clamp(2rem,4vw,4rem); display:flex; flex-direction:column; justify-content:center; }
.md-hero .m-info .eyebrow{ color:var(--gold-glow); }
.md-hero .m-info h1{ color:var(--paper); margin:1.2rem 0 1rem; }
.md-hero .m-info p{ color:var(--muted-dk); }
/* V50 · item 1 — wrap unconditionally, not just under the 560px query
   below. That query exists for the model pages' 2–3 stats, which don't need
   it above 560px; the townhouse hero reuses this same markup for 4 stats
   (beds, baths, cars, total area — see townhouse-body.php), and four
   fixed-2rem-gap items need more room than three. Measured overflowing by a
   few px around 865–878px, a width no named breakpoint hits but real
   in-between laptop/tablet sizes can. `flex-wrap:wrap` costs nothing when
   there's room — the row only actually wraps once it has to — so there's no
   reason to gate it to one guessed width per content shape. */
.md-hero .strip{ display:flex; flex-wrap:wrap; gap:2rem; margin-top:2rem; padding-top:2rem; border-top:1px solid rgba(255,255,255,.14); }
.md-hero .strip .s .v{ font-family:var(--display); font-size:1.7rem; font-weight:340; }
.md-hero .strip .s .l{ font-family:var(--mono); font-size:.6rem; letter-spacing:.12em; text-transform:uppercase; color:var(--gold-glow); }
.md-hero .m-info .cta-row{ display:flex; gap:.8rem; margin-top:2.2rem; flex-wrap:wrap; }

/* ── V114 · THE HERO SPEC PANEL IS CHARCOAL, NOT GREEN ────────────────────
   Two cards ask for the same thing, and the screencast names the target
   colour by pointing at another element on the same page:

     Townhouse card, item 3, and "for all the modular home gallery pages":
       "The right side of the hero has a green background. Use the same
        charcoal-grey background as the lower '20 WEEKS' panel. Make the hero
        property specifications and delivery information bold. Keep the
        existing layout."

   The "20 WEEKS" panel is `.th-highlight`, which is `var(--ink-2)`. So this
   panel takes the same token rather than a new hex, and the two stay matched
   if the token ever moves.

   THIS RETIRES THE LAST OF THE GREEN ON MODEL AND TOWNHOUSE PAGES. The long
   note at the top of this file ("GREEN MARKS THE ONE BLOCK PER PAGE...") had
   already lost two of its three bullets to V67.1 and V46; the model/townhouse
   hero spec panel was the one instance left. It is now gone too. The
   "Built with experience" band on the HOMEPAGE is untouched — it is not a
   call to action and the client pointed at it as the reference.

   Layout is deliberately not altered: only the ground colour and two font
   weights change, which is what "keep the existing layout" asks for. */
.md-hero .m-info{ background:var(--ink-2); }
/* "property specifications" = the bed/bath/car/area stat strip. */
/* V116 · card 87 item 2: "Make bedroom, bathroom and floor-area values bold."
   The VALUE goes to 700; the label under it keeps the 600 it was given in V114.
   With the spec line and the strapline removed from the hero, this strip is now
   the only place the three figures appear on the page, which is why they are
   asked for in bold. */
.md-hero .strip .s .v{ font-weight:700; }
.md-hero .strip .s .l{ font-weight:600; }
/* "delivery information" = the weeks-from-order badge sitting under it. */
.md-hero .m-info .th-weeks-badge{ font-weight:600; }
.spectable{ width:100%; border-collapse:collapse; }
.spectable tr{ border-bottom:1px solid var(--line-soft); }
.spectable td{ padding:1rem 0; font-size:1rem; }
.spectable td:first-child{ font-family:var(--mono); font-size:.72rem; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); width:44%; }
.spectable td:last-child{ color:var(--paper); }
.floorplan{ background:var(--card); border:1px solid var(--line-gold); aspect-ratio:16/10; position:relative; }
.floorplan svg{ width:100%; height:100%; }
.floorplan .fp-tag{ position:absolute; top:14px; left:16px; font-family:var(--mono); font-size:.6rem; letter-spacing:.14em; text-transform:uppercase; color:var(--gold-deep); }

/* ============================================================
   FOOTER
   ============================================================ */
.site-foot{ background:var(--ink-3); color:var(--paper); border-top:1px solid var(--line-gold);
  padding-block:clamp(64px,9vh,120px) 2.4rem; }
/* V89 — the four columns are re-proportioned 1.15 / 1 / 1 / 1.55.
   The brand column was set to 1.6fr when it still carried the company
   description under the logo; V67 deleted that paragraph and left the widest
   column holding a logo and three social buttons. Meanwhile the last column has
   grown — both showroom labels now end "— By Appointment" (V89) — and at 1fr it
   wrapped each one onto three lines and broke the email address mid-word.
   Measured at 1280 the contacts column goes 250px → 353px on this change; the
   labels come back to two lines and info@modulargrannyflat.com.au holds one. */
.foot-top{ display:grid; grid-template-columns:1.15fr 1fr 1fr 1.55fr; gap:2.4rem; padding-bottom:3.4rem; border-bottom:1px solid rgba(255,255,255,.12); }
.foot-brand .wordmark{ font-family:var(--display); font-size:1.7rem; }
.foot-brand p{ color:var(--muted-dk); margin-top:1rem; max-width:34ch; font-size:.94rem; }
.foot-social{ display:flex; gap:.7rem; list-style:none; margin:1.4rem 0 0; padding:0; }
.foot-social a{ display:inline-flex; align-items:center; justify-content:center; width:40px; height:40px;
  border:1px solid rgba(255,255,255,.18); border-radius:50%; color:var(--paper);
  transition:background .3s var(--ease,ease), border-color .3s var(--ease,ease), color .3s var(--ease,ease); }
.foot-social a svg{ width:19px; height:19px; }
.foot-social a:hover{ background:var(--gold); border-color:var(--gold); color:var(--ink); }
.foot-col h5{ font-family:var(--mono); font-size:.64rem; letter-spacing:.18em; text-transform:uppercase; color:var(--gold-glow); margin-bottom:1.2rem; font-weight:400; }
.foot-col a{ display:block; padding:.42rem 0; color:var(--muted-dk); font-size:.94rem; transition:color .3s var(--ease); }
.foot-col a:hover{ color:var(--paper); }
/* V67 — the company description under the footer logo is removed and the
   logo asked to sit lower ("drop logo down further", annotated PDF p.10).
   A top margin does that on the desktop four-column footer, where the brand
   column stands beside three headed columns; below 900px the brand column
   goes full width and the same margin just reads as a gap, so it is undone. */
.foot-brand--drop .foot-logo{ margin-top:clamp(1rem,3.4vw,3.2rem); }
@media (max-width:900px){ .foot-brand--drop .foot-logo{ margin-top:0; } }
/* V67 — the SMRT BUILT block that used to sit here carried the 1px rule
   between the warranty strip and the copyright row. It was removed on the
   client's instruction, so the rule moves onto .foot-bottom rather than
   disappearing with it. */
.foot-bottom{ display:flex; flex-wrap:wrap; justify-content:space-between; gap:1rem;
  padding-top:2.2rem; margin-top:2.6rem; border-top:1px solid rgba(255,255,255,.1);
  font-family:var(--mono); font-size:.66rem; letter-spacing:.08em; color:var(--muted-dk); text-transform:uppercase; }
.foot-bottom a{ color:var(--muted-dk); }
.foot-bottom a:hover{ color:var(--gold-hi); }
.foot-credit{ color:var(--muted-dk); }
.foot-credit b{ color:var(--gold-glow); font-weight:400; }

/* ============================================================
   REVEAL ANIMATION
   ============================================================ */
.reveal{ opacity:0; transform:translateY(26px); transition:opacity .9s var(--ease-out), transform .9s var(--ease-out); }
.reveal.in{ opacity:1; transform:none; }
.reveal.d1{ transition-delay:.08s; } .reveal.d2{ transition-delay:.16s; }
.reveal.d3{ transition-delay:.24s; } .reveal.d4{ transition-delay:.32s; }
@media (prefers-reduced-motion: reduce){
  .reveal{ opacity:1 !important; transform:none !important; }
  .hero h1 .line > span{ transform:none !important; }
  .mod{ opacity:1 !important; transform:none !important; }
  *{ animation:none !important; }
}

/* ============================================================
   RESPONSIVE
   ============================================================ */
@media (max-width:1080px){
  .model-grid,.three{ grid-template-columns:repeat(2,1fr); }
  .proc{ grid-template-columns:repeat(2,1fr); }
  .proc-step:nth-child(4n){ border-right:1px solid var(--line-soft); }
  .proc-step:nth-child(2n){ border-right:0; }
  .foot-top{ grid-template-columns:1fr 1fr; }
}
@media (max-width:860px){
  .evz{ grid-template-columns:1fr; }
  .split,.split.rev .split-media{ grid-template-columns:1fr; order:0; }
  .split-media{ aspect-ratio:16/11; }
  .md-hero{ grid-template-columns:1fr; }
  .md-hero .m-main{ min-height:52vh; }
  .two{ grid-template-columns:1fr; }
}
@media (max-width:620px){
  .model-grid,.three,.four{ grid-template-columns:1fr; }
  .proc{ grid-template-columns:1fr; }
  .proc-step{ border-right:0 !important; }
  .foot-top{ grid-template-columns:1fr 1fr; }
  .foot-brand{ grid-column:1 / -1; }
  .evz-panel .steps{ flex-direction:column; }
  .gal-item.wide,.gal-item.tall,.gal-item.reg,.gal-item.big{ grid-column:1 / -1; grid-row:auto; }
  .field.row2{ grid-template-columns:1fr; }
  .hero-scroll{ display:none; }
  .promo{ font-size:.6rem; letter-spacing:.05em; padding-right:2.4em; padding-left:1em; }
  .promo a{ display:none; }
}

/* ---- Photo overlays on placeholder slots (swap-ready) ---- */
.photo{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; z-index:1; }
.model-media .badge,.model-media .code,.model-media .imgnote{ z-index:3; }
.gal-item .cap{ z-index:3; }
.md-hero .m-main .photo{ z-index:1; }

/* ============================================================
   MMH CHILD THEME — ADDITIONS (v1.0.0)
   Navy caption pills · uncropped image frames · scheme switcher
   ============================================================ */

:root{
  /* SMRT Built caption navy, sampled from the brochure content stream
     (PDF operator: 0.133 0.22 0.424 rg). V20: the caption pill keeps the
     brochure navy — white text inside the pill is unaffected by the page
     ground — but the pill now carries a hairline so it reads as an object
     on black rather than a hole in it. Standalone navy TEXT moved to
     --navy-on-dark, defined in the main token block. */
  --navy:      #22386C;
  --navy-deep: #1A2B54;
  --navy-soft: rgba(34,56,108,.16);
}

/* ---------- Colour-fidelity guard ----------------------------------------
   Brochure photography is supplied as CMYK JPEG. It is converted to sRGB at
   build time, so nothing here may re-tint it. These declarations stop any
   parent theme, plugin or Elementor global from applying a filter, blend
   mode or overlay that would reintroduce the magenta/purple cast.          */
.mmh .shot img,
.mmh .model-media .photo,
.mmh .md-hero .m-main .photo,
.mmh .page-hero-band > .photo,
.mmh .split-media .photo{
  filter:none !important;
  -webkit-filter:none !important;
  mix-blend-mode:normal !important;
  opacity:1 !important;
  background:none;
}
.mmh .shot::before,.mmh .shot::after,
.mmh .shot-frame::before,.mmh .shot-frame::after{ content:none !important; }

/* ---------- Uncropped image frames ---------------------------------------
   Frames adopt each image's own aspect ratio (--ar, set inline from the
   real pixel dimensions), so nothing is ever cropped. object-fit:contain is
   a belt-and-braces fallback if --ar is missing.                            */
.shot{ margin:0; display:flex; flex-direction:column; }
.shot-frame{
  position:relative;
  aspect-ratio:var(--ar, 3 / 2);
  background:var(--paper-3);
  border:1px solid var(--line-soft);
  overflow:hidden;
}
.shot-frame img{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:contain;
  object-position:center;
  display:block;
}

/* ---------- Navy caption pill (matches the brochure) ---------- */
.pill-cap{
  align-self:flex-start;
  margin-top:.85rem;
  background:var(--navy);
  color:#fff;
  font-family:var(--mono);
  font-size:.66rem;
  letter-spacing:.14em;
  text-transform:uppercase;
  line-height:1;
  padding:.72em 1.15em;
  border-radius:999px;
  border:1px solid rgba(124,155,224,.42);
}

/* ---------- Gallery grid ---------- */
.shot-grid{
  display:grid;
  grid-template-columns:repeat(2,1fr);
  gap:clamp(18px,2.4vw,34px);
  margin-top:2.2rem;
}
.shot--full{ grid-column:1 / -1; }
.shot[hidden]{ display:none; }

.gal-head{
  display:flex; flex-wrap:wrap; gap:1.6rem;
  align-items:flex-end; justify-content:space-between;
}

/* ---------- Light / Dark scheme switcher ---------- */
.scheme-bar{
  display:flex; align-items:center; flex-wrap:wrap; gap:.5rem;
  padding:.45rem;
  border:1px solid var(--line);
  background:var(--card);
  border-radius:999px;
}
.scheme-label{
  font-family:var(--mono); font-size:.62rem; letter-spacing:.18em;
  text-transform:uppercase; color:var(--muted-2);
  padding-inline:.9rem .4rem;
}
.scheme-btn{
  font-family:var(--mono); font-size:.7rem; letter-spacing:.1em;
  text-transform:uppercase;
  padding:.7em 1.25em;
  border:1px solid transparent;
  border-radius:999px;
  color:var(--muted);
  transition:background .35s var(--ease), color .35s var(--ease), border-color .35s var(--ease);
}
.scheme-btn:hover{ border-color:var(--line-gold); color:var(--paper); }
.scheme-btn[aria-pressed="true"]{ background:var(--gold); color:var(--ink); border-color:var(--gold); }
.scheme-btn--light[aria-pressed="true"]{ background:var(--paper); color:var(--ink); border-color:var(--paper); }
.scheme-btn--dark[aria-pressed="true"]{ background:var(--ink-3); color:var(--paper); border-color:var(--line-ui); }
.scheme-btn:focus-visible{ outline:2px solid var(--gold); outline-offset:2px; }

.scheme-empty{
  margin-top:2rem; text-align:center;
  font-family:var(--mono); font-size:.74rem; letter-spacing:.1em;
  text-transform:uppercase; color:var(--muted-2);
}
.scheme-empty[hidden]{ display:none; }

/* ---------- Warranty badges ---------- */
.warranty-row{
  display:grid;
  grid-template-columns:repeat(3,1fr);
  gap:clamp(14px,2vw,26px);
}
.warranty-card{
  display:flex; flex-direction:column; align-items:center; gap:.6rem;
  text-align:center;
  padding:clamp(1.6rem,3vw,2.4rem) 1.2rem;
  background:var(--card);
  border:1px solid var(--line-gold);
}
.warranty-card .wv{
  font-family:var(--display); font-weight:300; line-height:1;
  font-size:clamp(2.6rem,5vw,3.8rem);
  color:var(--gold-glow);
}
.warranty-card .wl{
  font-family:var(--mono); font-size:.66rem; letter-spacing:.16em;
  text-transform:uppercase; color:var(--muted);
}

.foot-warranty{
  display:flex; flex-wrap:wrap; justify-content:center; gap:clamp(1.2rem,4vw,3.4rem);
  padding-top:2.4rem; margin-top:2.6rem;
  border-top:1px solid rgba(255,255,255,.1);
}
.foot-warranty span{
  font-family:var(--mono); font-size:.66rem; letter-spacing:.16em;
  text-transform:uppercase; color:var(--muted-dk);
}
.foot-warranty b{
  font-family:var(--display); font-size:1.5rem; font-weight:340;
  color:var(--gold-glow); letter-spacing:0; margin-right:.35em;
}
/* Keep each warranty badge on a single line (Modular Update V12). */
.foot-warranty .warranty-nowrap{ white-space:nowrap; }
.foot-warranty .warranty-nowrap b{ margin-right:0; }

/* ---------- V89 · warranty strip as three ruled columns ----------
   Client's new footer (Contact Us 1, p.9): 7 / 10 / 25, each a gold numeral and
   "Years" over the cover it names, the three separated by hairline rules.

   Grid, not flex, so the three columns are EQUAL and the two rules land at the
   thirds however long the labels are. The rules are borders on the 2nd and 3rd
   cells rather than pseudo-elements, so there is no trailing rule to suppress.

   The label goes back to `white-space:normal`: .warranty-nowrap above is still
   on each badge (the V32 white-text rule selects on it) and its `nowrap` would
   otherwise force "Builders Indemnity Insurance" to hold one line at every
   width — which is exactly the overflow this two-line form exists to avoid. */
.foot-warranty--cols{
  display:grid; grid-template-columns:repeat(3, 1fr); gap:0;
  justify-content:stretch; text-align:center;
}
/* One rule above the strip, not two. .foot-top already draws a border-bottom
   under the link columns; .foot-warranty was drawing its own border-top 42px
   below it, so the rendered footer carried a pair of hairlines with an empty
   band between them. The design (Contact Us 1, p.9) shows a single divider. */
.foot-warranty--cols{ border-top:0; padding-top:0; margin-top:clamp(2rem,3.4vw,3rem); }
.foot-warranty--cols .warranty-nowrap{
  display:block; padding-inline:clamp(.6rem, 2vw, 1.6rem); white-space:normal;
}
.foot-warranty--cols .warranty-nowrap + .warranty-nowrap{
  border-inline-start:1px solid rgba(255,255,255,.14);
}
.foot-warranty--cols .wn-l{
  display:block; margin-top:.42rem; font-size:.62rem; letter-spacing:.15em; line-height:1.5;
}
@media (max-width:640px){
  .foot-warranty--cols{ grid-template-columns:1fr; gap:1.1rem; }
  .foot-warranty--cols .warranty-nowrap + .warranty-nowrap{
    border-inline-start:0; border-block-start:1px solid rgba(255,255,255,.14); padding-top:1.1rem;
  }
}

/* Inline walkthrough video in the How It's Done gallery (Modular Update V12). */
.gal-video{ background:var(--ink-3); }
.gal-video video{ width:100%; height:100%; object-fit:cover; display:block; background:var(--ink-3); }

/* ---------- Model card media: natural ratio, no crop ---------- */
.mmh .model-media{
  aspect-ratio:var(--ar, 16 / 11);
  background:var(--paper-3);
}
.mmh .model-media .photo{ object-fit:contain; }

/* ---------- V6: uniform, symmetric grid cards ----------
   Collection grids (homepage + Modular Home Gallery + How It's Done) must line
   up in a clean matrix: every media block the same height and width, filled
   edge-to-edge. Overrides the natural-ratio rule above for grid contexts only;
   model detail pages keep their uncropped hero framing.                       */
.mmh .model-grid .model-media{
  aspect-ratio:4 / 3;
  height:auto;
}
.mmh .model-grid .model-media .photo{
  object-fit:cover;
  width:100%;
  height:100%;
}
.mmh .model-grid .model-body .name{
  min-height:2.6em;
}

/* ---------- Model hero ---------- */
.mmh .md-hero .m-main{ min-height:56vh; background:var(--paper-3); }
.mmh .md-hero .m-main .photo{ object-fit:cover; }

/* ---------- Inclusions columns ---------- */
.inclusions h4{
  font-family:var(--mono); font-size:.68rem; letter-spacing:.18em;
  text-transform:uppercase; color:var(--gold-deep);
  padding-bottom:.9rem; margin-bottom:.4rem;
  border-bottom:1px solid var(--line-gold);
}
.inclusions .tick-list li{ font-size:.94rem; }

/* ---------- Accessibility ---------- */
.screen-reader-text{
  position:absolute !important; width:1px; height:1px;
  padding:0; margin:-1px; overflow:hidden;
  clip:rect(0,0,0,0); white-space:nowrap; border:0;
}
.skip-link:focus{
  position:fixed !important; top:12px; left:12px; z-index:200;
  width:auto; height:auto; clip:auto; margin:0;
  padding:.8em 1.2em; background:var(--gold); color:var(--ink);
  font-family:var(--mono); font-size:.72rem; letter-spacing:.12em;
  text-transform:uppercase;
}

/* ---------- Responsive ---------- */
@media (max-width:900px){
  .warranty-row{ grid-template-columns:1fr; }
  .gal-head{ align-items:flex-start; }
}
@media (max-width:720px){
  .shot-grid{ grid-template-columns:1fr; }
  .shot--full,.shot--half{ grid-column:1 / -1; }
  .scheme-bar{ width:100%; justify-content:center; }
  .pill-cap{ font-size:.6rem; letter-spacing:.11em; }
}

/* ---------- Model card footer note ---------- */
.model-body .card-note{
  font-family:var(--mono); font-size:.62rem; letter-spacing:.1em;
  text-transform:uppercase; color:var(--muted-2); line-height:1.5;
  max-width:22ch;
}
.mmh .model-media .badge{
  top:auto; bottom:12px; left:14px;
  background:var(--navy); color:#fff; border:0;
  border-radius:999px; padding:.5em .9em;
  font-size:.55rem; letter-spacing:.12em;
}

/* ============================================================
   ROUND 2 — promo banner · nav phone · area chip ·
   included/excluded · lifestyle blocks
   ============================================================ */

/* ---------- Promotional banner (Customizer-controlled) ---------- */
.promo-banner{ background:var(--navy); color:#fff; position:relative; z-index:60; }
.promo-inner{
  display:flex; align-items:center; justify-content:center;
  gap:1rem; flex-wrap:wrap; padding:.7rem 0; text-align:center;
}
.promo-text{
  font-family:var(--mono); font-size:.7rem; letter-spacing:.14em; text-transform:uppercase;
}
.promo-link{
  font-family:var(--mono); font-size:.7rem; letter-spacing:.12em; text-transform:uppercase;
  color:var(--gold-glow); border-bottom:1px solid currentColor; padding-bottom:2px;
}
.promo-link:hover{ color:#fff; }

/* ---------- Header phone ---------- */
.nav-phone{
  display:inline-flex; align-items:baseline; gap:.5rem;
  font-family:var(--mono); font-size:.78rem; letter-spacing:.06em;
  color:var(--paper); white-space:nowrap; margin-right:.4rem;
}
.nav-phone:hover{ color:var(--gold-deep); }
.nav-phone-label{
  font-size:.58rem; letter-spacing:.18em; text-transform:uppercase; color:var(--muted-2);
}
@media (max-width:1100px){ .nav-phone-label{ display:none; } }
/* V18.1 §12 — the menu grew (Installation Story et al.) and the full bar no
   longer fit 1201–1360px: every page scrolled sideways 36px at 1280. Hide the
   phone number in that band (it stays in the drawer, footer and contact page).
   V50 · item 1 — the gap/font tightening this comment used to describe here
   was dead code: a LATER rule (`.nav-links{gap:...}` under "MODULAR UPDATE
   V21 — THE MENU", search that heading below) carries higher source order at
   equal specificity and was winning the cascade the whole time, so editing
   the tightening here never did anything. The real, currently-governing fix
   for the row's fit is at that V21 rule, not here — this block now only
   controls the phone number, and its threshold is synced to that rule's own
   (1520px), not the old 1360px, for the reason explained over there. */
/* V150 — THE PHONE BECOMES AN ICON HERE, IT DOES NOT DISAPPEAR.
   Max, card 101: "Please add the Phone icon so people can click to call
   easly", against a screenshot of the mobile header.

   The two display:none rules that used to sit here are the reason there was
   no click-to-call on any phone. The fit problem they were solving is real
   and is unchanged: below 1520px the number and its label genuinely do not
   fit beside the menu. So the TEXT still goes at 1520px, exactly as before,
   and only the icon stays — which costs about 40px instead of the number's
   ~150px and sits beside the burger where he drew it.

   .nav-phone-ic is hidden above 1520px so a wide screen shows the number on
   its own, as it always has. Nothing about the desktop bar changes. */
.nav-phone-ic{ display:none; }
.nav-phone-ic svg{ width:20px; height:20px; display:block; }
@media (max-width:1520px){
  .nav-phone-no{ display:none; }
  /* The "Call" label goes with the number, not with the icon. On its own
     beside a handset glyph it reads as a button with its subject missing,
     and between 1100 and 1520px that is exactly what it rendered as —
     measured: icon + "Call", no number. Icon alone in the whole band. */
  .nav-phone-label{ display:none; }
  .nav-phone-ic{ display:block; }
  /* align-items:baseline suits a text row; an icon on its own needs centring,
     and the tap target has to clear 44px in both directions to be a real one
     on a phone rather than a 20px glyph that keeps getting missed. */
  .nav-phone{
    align-items:center; justify-content:center;
    gap:0; margin-right:.15rem;
    min-width:44px; min-height:44px;
  }
}

/* ---------- Floor-area chip on model cards ---------- */
.model-media .area-chip{
  position:absolute; top:12px; right:14px; z-index:3;
  background:rgba(11,17,22,.86); color:var(--paper); backdrop-filter:blur(3px);
  font-family:var(--mono); font-size:.6rem; letter-spacing:.1em; text-transform:uppercase;
  padding:.5em .8em; border-radius:999px; border:1px solid var(--line-soft);
}

/* ---------- What's included / not included ---------- */
.incl-split{
  display:grid; grid-template-columns:1.35fr 1fr;
  gap:clamp(20px,3vw,44px); align-items:start;
}
.incl-col{
  padding:clamp(1.6rem,3vw,2.6rem);
  border:1px solid var(--line); background:var(--card);
}
.incl-col--no{ background:var(--paper-3); }
.incl-head{ display:flex; align-items:center; gap:.8rem; margin-bottom:1.6rem; }
.incl-head h3{
  font-family:var(--display); font-weight:340;
  font-size:clamp(1.25rem,2.2vw,1.6rem); margin:0;
}
.incl-badge{
  display:grid; place-items:center;
  width:2rem; height:2rem; border-radius:999px;
  font-size:1rem; line-height:1; flex:0 0 auto;
}
.incl-badge--yes{ background:var(--gold); color:var(--ink); }
.incl-badge--no{ background:var(--ink-3); color:var(--paper); border:1px solid var(--line-ui); }
.incl-col h4{
  font-family:var(--mono); font-size:.66rem; letter-spacing:.18em; text-transform:uppercase;
  color:var(--gold-deep); margin:1.8rem 0 .8rem;
  padding-bottom:.7rem; border-bottom:1px solid var(--line-gold);
}
.incl-col h4:first-of-type{ margin-top:0; }
.incl-col .tick-list li{ font-size:.92rem; }
.incl-intro{ font-size:.94rem; color:var(--muted); margin-bottom:1.4rem; }

.cross-list{ list-style:none; margin:0; padding:0; }
.cross-list li{
  position:relative; padding-left:1.7rem; margin-bottom:.75rem;
  font-size:.92rem; line-height:1.6; color:var(--muted);
}
.cross-list li::before{
  content:"\00d7"; position:absolute; left:0; top:-.05em;
  font-size:1.05rem; line-height:1.4; color:var(--gold-deep);
}
.incl-note{
  margin-top:2rem; padding-top:1.6rem; border-top:1px solid var(--line);
}
.incl-note p{ font-size:.9rem; color:var(--muted); margin-bottom:1rem; }

/* ---------- Lifestyle marketing blocks ---------- */
.life-grid{ display:grid; grid-template-columns:repeat(2,1fr); gap:clamp(20px,2.6vw,38px); }
.life-card{ display:flex; flex-direction:column; background:var(--card); border:1px solid var(--line); }
.life-media{ aspect-ratio:16/10; overflow:hidden; background:var(--paper-3); }
.life-media .photo{ width:100%; height:100%; object-fit:cover; display:block; transition:transform 1.2s var(--ease); }
.life-card:hover .life-media .photo{ transform:scale(1.04); }
.life-body{ padding:clamp(1.5rem,2.6vw,2.2rem); display:flex; flex-direction:column; gap:.9rem; }
.life-body h3{
  font-family:var(--display); font-weight:340; line-height:1.15;
  font-size:clamp(1.3rem,2.4vw,1.75rem); margin:0;
}
.life-body p{ font-size:.95rem; color:var(--muted); margin:0; }
.life-body .tlink{ margin-top:.4rem; align-self:flex-start; }

@media (max-width:900px){
  .incl-split{ grid-template-columns:1fr; }
  .life-grid{ grid-template-columns:1fr; }
}

/* ---------- Form error message ---------- */
.form-error{
  display:none; margin-top:1rem; padding:.85rem 1.1rem;
  background:rgba(179,148,94,.14); border-left:3px solid var(--gold);
  font-size:.88rem; color:var(--paper);
}

/* ============================================================
   ROUND 3 — process carousel · reviews · FAQ accordion ·
   contact page · ideal-for · future expansion · enquiry form
   ============================================================ */

/* ---------- "From Factory to Floor" horizontal carousel ---------- */
.proc-carousel{ margin-top:2rem; }
.proc-carousel .proc{
  display:grid; grid-auto-flow:column;
  grid-auto-columns:minmax(272px, 1fr);
  gap:0;
  overflow-x:auto; overflow-y:hidden;
  scroll-snap-type:x mandatory;
  scrollbar-width:none; -ms-overflow-style:none;
  border-top:1px solid var(--line-gold);
  border-bottom:1px solid var(--line-gold);
}
.proc-carousel .proc::-webkit-scrollbar{ display:none; }
.proc-carousel .proc:focus-visible{ outline:2px solid var(--gold); outline-offset:3px; }
.proc-carousel .proc-step{
  scroll-snap-align:start;
  border-right:1px solid var(--line-soft);
  border-bottom:0;
  padding:clamp(1.5rem,2.4vw,2.2rem);
}
.proc-carousel .proc-step:last-child{ border-right:0; }

.proc-controls{
  display:flex; align-items:center; gap:1.2rem;
  margin-top:1.6rem;
}
.proc-btn{
  width:2.9rem; height:2.9rem; flex:0 0 auto;
  display:grid; place-items:center;
  border:1px solid var(--line-gold); background:var(--card);
  color:var(--paper); font-size:1rem; cursor:pointer;
  border-radius:999px;
  transition:background .3s var(--ease), color .3s var(--ease), opacity .3s var(--ease);
}
.proc-btn:hover:not(:disabled){ background:var(--gold); color:var(--ink); border-color:var(--gold); }
.proc-btn:disabled{ opacity:.3; cursor:default; }
.proc-btn:focus-visible{ outline:2px solid var(--gold); outline-offset:2px; }
.proc-progress{ flex:1 1 auto; height:2px; background:var(--line); position:relative; }
.proc-progress span{
  position:absolute; inset:0 auto 0 0; width:0;
  background:var(--gold); transition:width .3s var(--ease);
}

/* ---------- Map embed ---------- */
.map-embed{
  margin-top:2.4rem; border:1px solid var(--line-gold);
  aspect-ratio:16/7; background:var(--paper-3);
}
.map-embed iframe{ width:100%; height:100%; border:0; display:block; }

/* ---------- FAQ accordion ---------- */
.faq-accordion{ max-width:820px; margin-inline:auto; border-top:1px solid var(--line); }
.faq-accordion .qa{ border-bottom:1px solid var(--line); }
.faq-accordion .qa summary{
  cursor:pointer; list-style:none; padding:1.35rem 2.6rem 1.35rem 0;
  position:relative; font-size:1.03rem; line-height:1.45; color:var(--paper);
  font-weight:500;
}
.faq-accordion .qa summary::-webkit-details-marker{ display:none; }
.faq-accordion .qa summary::after{
  content:"+"; position:absolute; right:.4rem; top:50%; transform:translateY(-50%);
  font-size:1.35rem; line-height:1; color:var(--gold-deep); transition:transform .3s var(--ease);
}
.faq-accordion .qa[open] summary::after{ content:"\2013"; }
.faq-accordion .qa summary:hover{ color:var(--gold-deep); }
.faq-accordion .qa summary:focus-visible{ outline:2px solid var(--gold); outline-offset:3px; }
.faq-accordion .qa .a{ padding:0 2.6rem 1.5rem 0; }
.faq-accordion .qa .a p{ font-size:.96rem; line-height:1.75; color:var(--muted); margin:0 0 .8rem; }
.faq-accordion .qa .a p:last-child{ margin-bottom:0; }

/* ---------- Ideal For + Future Expansion ---------- */
.ideal-split{ display:grid; grid-template-columns:1fr 1fr; gap:clamp(20px,3vw,40px); align-items:stretch; }
/* V116 · card 87 item 3 removes the Future Expansion card from the model pages
   and says to close the gap. One child in a two-column grid sits at half width
   with an empty column beside it; this collapses the grid so the remaining box
   fills the row. The two-column rule stays for any page still passing both. */
.ideal-split--solo{ grid-template-columns:1fr; }
/* A seven-item list left in one column across a full-width box is the gap
   moved, not closed: the words end a third of the way across and the rest of
   the box is empty. The list runs in two columns inside the box instead, so
   the box is filled and nothing else about it changes. Back to one column at
   the width where two would crowd. */
.ideal-split--solo .ideal-list{ grid-template-columns:1fr 1fr; column-gap:clamp(20px,3vw,40px); }
@media (max-width:640px){ .ideal-split--solo .ideal-list{ grid-template-columns:1fr; } }
.ideal-box,.expand-box{
  padding:clamp(1.7rem,3vw,2.5rem);
  border:1px solid var(--line-gold); background:var(--card);
}
.expand-box{ background:var(--ink-2); color:var(--paper); border-color:var(--line-gold); }
.expand-box h3{ color:var(--paper); }
.expand-box p{ color:rgba(244,241,234,.82); font-size:.97rem; line-height:1.7; }
.expand-box .tlink{ margin-top:1.2rem; display:inline-block; color:var(--gold-glow); }
.expand-badge{
  display:inline-block; background:var(--gold); color:var(--ink);
  font-family:var(--mono); font-size:.62rem; letter-spacing:.16em;
  text-transform:uppercase; padding:.65em 1.1em; border-radius:999px;
}
.ideal-list{ list-style:none; margin:0; padding:0; display:grid; gap:.65rem; }
.ideal-list li{
  position:relative; padding-left:1.7rem; font-size:1rem; line-height:1.6; color:var(--paper);
}
.ideal-list li::before{
  content:""; position:absolute; left:0; top:.55em;
  width:.6rem; height:.6rem; background:var(--gold-deep); border-radius:999px;
}

/* Expandable chip on model cards */
.model-media .expand-chip{
  position:absolute; left:14px; bottom:12px; z-index:3;
  background:var(--gold); color:var(--ink);
  font-family:var(--mono); font-size:.55rem; letter-spacing:.13em;
  text-transform:uppercase; padding:.5em .85em; border-radius:999px;
}

/* ---------- Enquiry form partial ---------- */
.enq-contact{ display:flex; flex-direction:column; gap:.5rem; margin-top:1.8rem; }
.enq-phone{ font-family:var(--display); font-size:1.5rem; color:var(--paper); }
.enq-phone:hover{ color:var(--gold-deep); }
.enq-contact a{ color:var(--muted); }
.enq-addr{ font-size:.9rem; line-height:1.6; color:var(--muted-2); margin-top:.4rem; }
.hp-field{ position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden; }

.foot-address{ display:block; padding:.42rem 0; color:var(--muted-dk); font-size:.94rem; line-height:1.6; }

/* ---------- Responsive ---------- */
@media (max-width:980px){
  .review-grid{ grid-template-columns:1fr 1fr; }
  .contact-cards{ grid-template-columns:1fr; }
  .link-grid{ grid-template-columns:1fr 1fr; }
}
@media (max-width:820px){
  .ideal-split{ grid-template-columns:1fr; }
}
@media (max-width:700px){
  .review-grid{ grid-template-columns:1fr; }
  .link-grid{ grid-template-columns:1fr; }
  .proc-carousel .proc{ grid-auto-columns:minmax(230px, 82%); }
  .map-embed{ aspect-ratio:4/3; }
  .faq-accordion .qa summary{ font-size:.98rem; padding-right:2.2rem; }
}

/* ============================================================
   ROUND 4 — hero copy · enquiry panel · thank-you ·
   Trustindex · FAQ polish · Where-to-next
   ============================================================ */

/* ---------- Longer hero headline ---------- */
.hero h1.hero-h1--long{
  max-width:34ch;
  font-size:clamp(2.3rem, 4.9vw, 4rem);
  line-height:1.05;
}
@media (max-width:720px){
  .hero h1.hero-h1--long{ max-width:15ch; font-size:clamp(2.1rem, 9.2vw, 3.2rem); }
}
@media (min-width:781px){
  .hero h1.hero-h1--long .line > span{ white-space:nowrap; }
}

/* ────────────────────────────────────────────────────────────────────────
   V96 · HERO — "Make the main heading slightly smaller" (card item 2)
   ────────────────────────────────────────────────────────────────────────
   Two reasons this is not just a smaller number.

   1. The line is held at `white-space:nowrap` above 780px and its wrapper is
      `.line{overflow:hidden}` for the reveal mask. So a headline that is too
      wide for its column is not wrapped and it is not scrolled — it is
      CLIPPED, silently, by the mask. It reads as a missing word rather than
      as a layout fault, which is the exact trap recorded against
      .hero-aside .hero-actions further up this file.

   2. The right-hand column just grew. The two feature cards are ~372px wide
      at 1440 on top of the stacked buttons, and that width comes out of the
      headline's share.

   4.9vw → 4.05vw and 4rem → 3.35rem is about a 16% reduction. The client's
   note is "make this a little bit smaller so that it can look balanced with
   the 2 buttons under", so the test is the RATIO of the headline to the
   button row beneath it, not an absolute size. At 1440 that lands the two
   within about three quarters of each other, which is where the mock-up
   sits. The headline also got shorter this version ("Granny Flat", not
   "Flats"), so some of the margin came free.

   If the cards are ever widened, re-measure this. The failure is invisible. */
.hero h1.hero-h1--v96{
  font-size:clamp(2.05rem, 4.05vw, 3.35rem);
}
@media (max-width:720px){
  .hero h1.hero-h1--v96{ font-size:clamp(1.95rem, 8.6vw, 2.95rem); }
}

/* Per-slide title, bottom-left. Only rendered once the client supplies the
   eight lines; see tpl-home.php. Stacked in one grid cell so the block never
   changes height as the titles change length, which would shunt the headline
   above it on every slide. */
.hero-slide-titles{
  display:grid; grid-template-areas:"t";
  margin:clamp(.8rem,1.6vh,1.2rem) 0 0;
  min-height:1.4em;
}
.hero-slide-titles .hst{
  grid-area:t;
  font-family:var(--mono); font-size:.78rem; letter-spacing:.16em;
  text-transform:uppercase; color:var(--paper);
  opacity:0; transition:opacity .45s var(--ease, ease);
  /* V120 — Bianca, 17 Sep: "remove all the text shadows on all pages". */
}
.hero-slide-titles .hst.is-active{ opacity:1; }

/* ---- the two feature cards, bottom-right ---- */
.hero-cards{
  /* V97 — Max, 15 Sep, arrow drawn between the two cards: "increase the space
     between those". Was clamp(8px,.9vw,12px), which at 1440 put 13px between
     them and read as one split panel rather than two cards. */
  display:flex; gap:clamp(16px,1.9vw,28px);
  /* Ahead of the buttons in the source, so the reading order down the right
     edge is cards → actions → slider controls. */
}
.hero-card{
  display:flex; flex-direction:column;
  /* V97.8 — Bianca, 15 Sep: "the video on homepage still needs to be larger a
     bit". Was clamp(120px,11.6vw,164px), so 164px at 1440. Up about a
     quarter. Both cards move together: they are a pair on one baseline and
     enlarging only one of them reads as a mistake rather than as emphasis.
     The clearance measured after this is what stops it going further — at
     210px the pair plus its gap is within 40px of the headline column.

     V101 — Bianca, 15 Sep: "the video on the hero section needs to be a
     LITTLE BIT smaller. It's too big now." Back down about a tenth, to 186px
     at 1440. That sits between this version's 206 and the 164 it replaced,
     which is the range the last three notes have been circling. Both cards
     still move together, for the reason above. */
  width:clamp(126px,13.2vw,186px);
  text-decoration:none; color:var(--paper);
  background:rgba(9,14,19,.55);
  border:1px solid var(--line);
  border-radius:2px;
  overflow:hidden;
  backdrop-filter:blur(4px);
  transition:border-color .28s var(--ease, ease), transform .42s var(--ease, ease), box-shadow .42s var(--ease, ease);
  /* The card grows from its BOTTOM edge. Both cards sit on the same baseline
     at the foot of the hero, and scaling from the centre lifts that baseline
     on hover, which reads as the card coming loose. From the bottom it reads
     as the card coming forward. */
  transform-origin:50% 100%;
  will-change:transform;
}
/* V97.1 — Max, 15 Sep, arrow on the left card: "the box zoom and it gets a bit
   bigger, like on https://www.dvele.com/". Bianca, same day: "I want the video
   to be LARGE when hover, just as large as dvele's video when hover."

   ⚠ THE FIRST PASS GUESSED THE SIZE AND GUESSED WRONG. It used 1.045, which is
   seven pixels on a 164px card, on the reasoning that a card pinned to the
   corner of a hero should not jump. Reasonable, and not what was asked for.
   Dvele's panel was then MEASURED in the browser rather than described from
   memory, and the real numbers are nothing like that:

     at rest   .portrait-vid  272 x 472, held at transform:scale(0.69)
     on hover                 the same element at scale(1), so 188 x 326 → 272 x 472
     growth    1.449x, about 45% bigger
     easing    0.5s cubic-bezier(.645,.045,.355,1)   (ease-in-out cubic)
     origin    0px 472px — the BOTTOM LEFT corner, which stays pinned while
               the panel expands up and to the right

   All four numbers are used below. `.hero-card--video` gets the full 1.449.

   DIRECTION IS FLIPPED, AND IT HAS TO BE. Dvele has ONE panel in that corner,
   so it can expand to the right into open sky. Ours is the LEFT of a pair, and
   expanding right would drive it straight through the 3D card beside it. So
   the pinned corner is the bottom RIGHT and it expands up and to the left,
   into the open part of the photograph. Same gesture, same magnitude, the only
   direction available.

   THE 3D CARD DOES NOT GET 1.449, deliberately. It sits hard against the right
   edge of the hero with nowhere to expand to: right takes it off the page,
   left puts it on top of the video. It keeps the smaller lift, which is also
   the correct reading of the two — the video is the feature, the 3D card is a
   link to a page. */
.hero-card:hover,
.hero-card:focus-visible{
  border-color:var(--gold-deep);
  transform:translateY(-3px) scale(1.045);
  box-shadow:0 14px 34px rgba(0,0,0,.42);
}
.hero-card:focus-visible{ outline:2px solid var(--gold-hi); outline-offset:2px; }

/* The video card, at dvele's own magnitude, easing and pinned-corner
   behaviour. z-index lifts it over its neighbour and over the slider controls
   for the duration of the hover — without it the expanded panel would pass
   UNDER the card it is growing away from, which looks like a clipping bug
   rather than a card coming forward. */
.hero-card--video{
  position:relative; z-index:1;
  transform-origin:100% 100%;
  transition:transform .5s cubic-bezier(.645,.045,.355,1), box-shadow .5s cubic-bezier(.645,.045,.355,1), border-color .28s var(--ease, ease);
}
.hero-card--video:hover{
  transform:scale(1.449);
  z-index:5;
  box-shadow:0 26px 60px rgba(0,0,0,.55);
}

/* NO SECOND ZOOM INSIDE THE VIDEO CARD. A picture that pushes against its
   frame is the right gesture on a tile that moves a few pixels; on one that
   grows by 45% it is two scales fighting, and the footage ends up cropped
   harder the bigger the panel gets, which is backwards. Dvele does not do it
   either — its video sits still inside the frame and the frame grows.
   The 3D card keeps it, because that card only moves a little. */
.hero-card--3d .hero-card-media img{
  transition:transform .62s var(--ease, ease);
}
.hero-card--3d:hover .hero-card-media img{ transform:scale(1.07); }

/* Motion is the whole point of this, so it is the whole thing that goes when
   the visitor has asked for less of it. The gold edge stays: that is the
   affordance, and it costs no movement. */
@media (prefers-reduced-motion:reduce){
  .hero-card:hover,
  .hero-card:focus-visible,
  .hero-card--video:hover{ transform:none; box-shadow:none; }
  .hero-card--3d:hover .hero-card-media img{ transform:none; }
}

/* Below 1200px .hero-aside goes full width and LEFT aligned (see the media
   query further down), so the open space is on the other side and the pinned
   corner has to follow it. Left as-is, the panel would expand off the left
   edge of the screen on a tablet. */
@media (max-width:1199px){
  .hero-card--video{ transform-origin:0% 100%; }
}

/* On a phone the cards are already 40% of the screen width. A 45% expansion
   would put the panel over the headline and past the right edge, and there is
   no hover on a touch screen to trigger it deliberately — it fires on tap.
   The video card takes the small lift here, same as its neighbour. */
@media (max-width:720px){
  .hero-card--video:hover{ transform:translateY(-3px) scale(1.045); box-shadow:0 14px 34px rgba(0,0,0,.42); }
}

/* PORTRAIT, 2:3 — not the 16/9 this started as.
   Bianca, 15 Sep: "I think Max wanted a portrait just like on dvele.com", and
   she is right on both counts. Dvele's feature panel is a tall portrait video,
   and the client's own annotated mock-up draws both hero cards portrait too
   (the left one measures about 2:3 in their image).

   The video source is 16:9, so a portrait card centre-crops it and the far
   left and right of each frame are lost. That is the cost of the shape and it
   is worth knowing: the walkthrough holds up because its subjects are centred,
   but if a future clip has anything important at the edges, this crop will eat
   it. The poster is cut from the same frame at the same ratio so the still and
   the first frame of playback agree.

   The cards also came down in width. Portrait at the old 180px would have made
   them 270px tall and let the corner out-shout the headline. */
.hero-card-media{
  display:block; position:relative;
  aspect-ratio:2 / 3; overflow:hidden; background:var(--ink-3);
}
.hero-card-media img,
.hero-card-video{
  display:block; width:100%; height:100%;
  object-fit:cover;
}
.hero-card-label{
  display:flex; align-items:center; justify-content:space-between; gap:.5rem;
  padding:.55rem .7rem;
  font-family:var(--mono); font-size:.68rem; letter-spacing:.14em;
  text-transform:uppercase; line-height:1.2;
}
.hero-card-label .arrow{ transition:transform .25s var(--ease, ease); }
.hero-card:hover .arrow{ transform:translateX(3px); }

/* V96.2 — the client's mock-up puts the buttons UNDER the headline, side by
   side, left-aligned to the same edge. `width:max-content` on the row stops
   the two stretching across the whole lead column; `flex-wrap` lets them drop
   to two lines on a narrow phone rather than shrinking the labels. */
.hero-actions--under{
  margin-top:clamp(1.3rem,2.6vh,2rem);
  display:flex; flex-direction:row; align-items:center;
  gap:clamp(.6rem,1vw,1rem);
  width:max-content; max-width:100%;
  flex-wrap:wrap;
}
.hero-actions--under .btn{ justify-content:center; }

/* The 3D card is the click target; the video card is not.

   V97 — the video card USED to have every hover affordance stripped, on the
   reasoning that nothing should suggest it can be clicked. Max's 15 Sep note
   draws its arrow at this exact card and asks for the dvele zoom on it, so the
   zoom is now shared. The distinction it still keeps is the one that actually
   signals clickability: no pointer cursor, no gold edge, no arrow, no label.
   A picture that responds to the mouse is not a promise of a link; a hand
   cursor is. */
.hero-card--video{ margin:0; cursor:default; }
.hero-card--video:hover{ border-color:var(--line); }

/* BOTTOM EDGES ALIGNED, and this needs saying because the obvious fix is the
   wrong one. Only the 3D card carries a label — it is the link, so it needs a
   visible name — which makes it taller than the video card beside it. Left
   alone, the pair sits with one bottom edge 30px above the other and reads as
   a mistake rather than as a difference.

   The fix is NOT to add a decorative label to the video card to even them up:
   that would put a caption on something the client explicitly does not want
   read as clickable. Instead the video's media is allowed to grow into the
   space the label occupies on its neighbour. `aspect-ratio:auto` is required
   — the 16/9 above would otherwise win and the flex growth would do nothing —
   and `object-fit:cover` on the video absorbs the slightly taller box. */
.hero-cards{ align-items:stretch; }
.hero-card--video .hero-card-media{
  aspect-ratio:auto;
  flex:1 1 auto;
  min-height:0;
}

.hero-card-mark{
  position:absolute; right:.5rem; bottom:.5rem;
  width:26px; height:26px; display:grid; place-items:center;
  color:var(--paper);
  background:rgba(9,14,19,.55); border:1px solid rgba(244,241,234,.28);
  border-radius:2px;
  transition:color .28s var(--ease, ease), border-color .28s var(--ease, ease);
}
.hero-card-mark svg{ width:15px; height:15px; }
.hero-card--3d:hover .hero-card-mark{ color:var(--gold-deep); border-color:var(--gold-deep); }

@media (max-width:1199px){
  /* .hero-aside goes left-aligned and full width here (see above), so the
     cards lead the block rather than hanging off the right edge. */
  .hero-cards{ order:-1; }
}
@media (max-width:480px){
  .hero-card{ width:clamp(118px,38vw,150px); }
  .hero-card-label{ font-size:.62rem; letter-spacing:.1em; }
  .hero-actions--under{ width:100%; }
  .hero-actions--under .btn{ flex:1 1 100%; }
}

/* ---------- Enquiry panel ---------- */
.enq-panel{
  display:grid; grid-template-columns:0.86fr 1.14fr;
  border:1px solid var(--line-gold); background:var(--card);
  overflow:hidden;
}
.enq-aside{
  background:var(--ink-2); color:var(--paper);
  padding:clamp(2rem,3.4vw,3rem);
  display:flex; flex-direction:column;
}
/* V46 — .enq-aside--green retired. Every enquiry form's aside is this one
   dark navy now; no page varies it any more. */
.enq-aside .eyebrow{ color:var(--gold-glow); }
.enq-title{ color:var(--paper); margin:1rem 0 .9rem; }
.enq-intro{ color:rgba(244,241,234,.78); font-size:.97rem; line-height:1.7; margin:0; }

.enq-details{ list-style:none; margin:2.2rem 0 1.9rem; padding:0; display:grid; gap:1.5rem; }
.enq-details li{ display:flex; flex-direction:column; gap:.3rem; }
.ed-label{
  font-family:var(--mono); font-size:.6rem; letter-spacing:.2em;
  text-transform:uppercase; color:var(--gold-glow);
}
.ed-value{ font-size:1rem; line-height:1.55; color:var(--paper); }
a.ed-value:hover{ color:var(--gold-hi); }
.ed-value--lg{ font-family:var(--display); font-size:1.65rem; line-height:1.2; }
.ed-note{ font-size:.8rem; color:rgba(244,241,234,.78); }

/* ------------------------------------------------------------------
   V67 · THE CLIENT'S NEW ENQUIRY FORM  (About us card / annotated PDF p.9)

   One rounded panel, both columns, in the SITE'S OWN NAVY — the same
   treatment as the homepage call-to-action (#showroom / #book). Everything
   else from the client's page-9 design stands: the field order, the purchase
   timeframe, the two locations, the warranties, no hours row, no disclaimer.
   `.enq-trust` (the old bottom warranty strip) is retired; the warranties are
   now a row inside .enq-details, which is where her design puts them.

   THIS PANEL WAS BUILT GREEN FIRST, AND THAT WAS WRONG. The design arrived
   only as a scan of a printout, and page 9 measures genuinely greener than
   pages 4, 6 and 8 of the SAME scan — pages which show sections that are navy
   on the live site — so the reading was defensible, but it was still a colour
   read off a photocopy. The client then said plainly: "keep the original
   style that's currently on the homepage for the call to action, yes, the
   blue one". Measured on the live homepage, that is:
       #showroom section  rgb(19,29,38)  = --paper-2  #131D26
       #book form card    rgb(22,33,44)  = --card     #16212C
       inputs             #16212C ground, rgba(244,241,234,.38) hairline
       labels             rgb(175,169,157) = --muted
   which is what this panel now is. DO NOT reintroduce green here on the
   strength of the PDF.

   RULE #75, IN REVERSE, AND IT MATTERS. While this panel was olive it carried
   the same gold variable lift `.bg-green` carries, because #C9A96C measures
   4.34:1 on olive. The stylesheet's own note says to take a component OFF that
   lift the moment it stops being green — so the lift is GONE from here. On
   #16212C the site's standard --gold-deep is correct and consistent with every
   other panel; leaving the lift on would have made this one panel's gold
   brighter than the rest of the site for no reason.
   ------------------------------------------------------------------ */
.enq-panel--green{
  /* The rounded container is the one purely visual thing kept from the
     client's page-9 design. Colour comes from the .enq-panel base, which is
     already --card / --ink-2, i.e. the homepage CTA's navy. The class name is
     left alone deliberately: renaming it would touch every call site for no
     behavioural gain, and the comment above records what it now means. */
  border-radius:clamp(18px,2vw,28px);
}
/* Match the homepage call-to-action's form treatment exactly (measured on
   staging, see above) rather than inheriting the generic dark-section rule,
   so the two read as one component across the site. */
.enq-panel--green .field input,
.enq-panel--green .field select,
.enq-panel--green .field textarea{
  background:var(--card); color:var(--paper);
  border-color:rgba(244,241,234,.38);
}
.enq-panel--green .field input::placeholder,
.enq-panel--green .field textarea::placeholder{ color:rgba(244,241,234,.55); }
.enq-panel--green .field input:focus,
.enq-panel--green .field select:focus,
.enq-panel--green .field textarea:focus{ border-color:var(--gold); }
/* V59 — the OPEN native select popup is drawn by the OS, so its colours have
   to be declared as a pair or the list renders unreadably. */
.enq-panel--green .field select{ color-scheme:dark; }
.enq-panel--green .field select option{ background:#20303F; color:#FFF; }

/* V67 · PRE-EXISTING DEFECT, found while responsive-sweeping this rebuild and
   fixed here because it clips the client's new form on a phone.

   MEASURED, and measured against v2.77.0 so the blame is right: the aside and
   the form column both render 363px wide REGARDLESS of viewport — 320, 360,
   375 and 390 all give 363 — so `.enq-panel{overflow:hidden}` cuts
   **84px at 320px, 44px at 360px, 14px at 390px**, and is clean from ~414px
   up. That takes the right edge off the input fields and the Send button on
   an iPhone SE / mini and on plenty of Android handsets. v2.77.0 produces the
   IDENTICAL numbers, so this is not a V67 regression — it has been shipping
   for a long time and nobody had swept the form at phone widths.

   Cause: a grid item defaults to `min-width:auto`, i.e. it refuses to shrink
   below its own min-content width. The email address is one unbreakable
   259px token, plus 2x32px of aside padding = 323px, and the column sizes to
   that instead of to the panel. `min-width:0` releases the floor;
   `overflow-wrap:anywhere` then lets the address itself break rather than
   pushing the box wide again. Both are needed — either alone leaves it. */
.enq-panel > *{ min-width:0; }
.enq-aside .ed-value{ overflow-wrap:anywhere; }

/* The client's design sets the heading in caps. Done here rather than in the
   template so the PHP still carries readable sentence-case copy, which is
   what lands in a screen reader and in any future export of the strings. */
.enq-panel--green .enq-title{ text-transform:uppercase; letter-spacing:.005em; }

/* Locations: a sub-heading per state inside the one Location row. */
.ed-place{
  font-family:var(--mono); font-size:.62rem; letter-spacing:.16em;
  text-transform:uppercase; color:var(--paper); margin-top:.55rem;
}
.ed-place:first-of-type{ margin-top:0; }
.ed-warranty{ line-height:1.7; }

.enq-formwrap{ padding:clamp(2rem,3.4vw,3rem); }

/* ---------- V67 · About: Our team ----------
   Replaces the four-step process strip the client removed (annotated PDF
   p.5). The photograph is not supplied yet, so the template renders the
   figure only when assets/img/about-team-2026.jpg actually exists — these
   rules have to look right BOTH ways, which is why the copy column is
   centred and measured on its own rather than positioned against the photo. */
.team-block{ display:flex; flex-direction:column; align-items:center; gap:clamp(2rem,4vw,3.4rem); }
.team-photo{
  margin:0; width:100%; max-width:1100px;
  aspect-ratio:16/9; overflow:hidden;
  border:1px solid var(--line-soft); background:var(--paper-3);
}
.team-photo img{ width:100%; height:100%; object-fit:cover; display:block; }
.team-copy{ max-width:70ch; text-align:center; }
.team-copy .eyebrow{ display:block; margin-bottom:1.2rem; }
@media (max-width:720px){
  .team-photo{ aspect-ratio:4/3; }
  .team-copy{ text-align:left; }
}

/* ---------- V67.2 · Our Process — the seven numbered step bands ----------
   MEASURED ON THE CLIENT'S OWN REFERENCE (smrtbuilthomes.com.au/our-process),
   after she reported "the images are not aligned with each step and there are
   not aligned contents". The reference does three things and this now matches
   all three:

     1. ONE image per step, never two, every one a consistent 3:2 frame
        (659x439 there). Handled in tpl-process.php, not here.
     2. The columns ALTERNATE — image left / copy right, then mirrored.
     3. The columns are TOP-ALIGNED: the heading starts level with the top of
        the photograph and spare height falls at the BOTTOM of the copy
        column.

   Point 3 is the "not aligned contents" half of her report and it is the one
   line below. The shared `.split` component centres its columns
   (`align-items:center`), which is right for a photo beside a short block of
   copy and wrong here: with copy of varying length beside a fixed frame, every
   step's heading sat at a different height relative to its image, so nothing
   lined up down the page. `align-items:start` is scoped to `.proc-band` only,
   so no other .split on the site moves.

   Reuses `.split` / `.split.rev` rather than introducing a second two-column
   mechanism, so the entrance rules, the body register and every breakpoint
   already written for .split apply here unchanged.

   NO NEW COLOUR IS INTRODUCED, on purpose. The card's instruction is "use the
   Current Master Modular colour palette, do not use any blue from the
   reference website" — and it explicitly WITHDRAWS the "gold and black" note
   handwritten on the PDF, which predates the site's current dark theme. So
   these bands pin no scheme: they alternate between the page ground and
   .bg-paper2, both existing tokens, and the step numbers take --gold-deep via
   .eyebrow, i.e. whatever the palette currently resolves to.

   The 3:2 frame with object-fit:cover is deliberate and must not become
   `auto`: the six supplied photographs run portrait to landscape (1023x1537
   up to 6671x4447), and letting each keep its own proportions is exactly the
   defect V62 was sent back for on the model galleries — "not cropped
   properly" turned out to mean not cropped at all. */
/* V120 — Bianca, 17 Sep: "For Our Process page, best to align the contents
   like the 2nd image center vertically right?" Yes. `start` was set at V67 so
   that every step's HEADING sat at the same height as the top of its
   photograph, which lined the page up when the copy blocks were of even
   length. They are not: the copy now runs from two short paragraphs to five,
   so `start` leaves a growing block of dead space under the shorter ones and
   the photograph beside them reads as unanchored.
   Centring restores the shared .split behaviour, which is also what the rest
   of the site does, so the Our Process bands no longer behave differently from
   every other two-column band. */
.proc-band .split{ align-items:center; }
.proc-media-stack{ display:grid; gap:clamp(14px,1.6vw,22px); align-content:start; }
.proc-shot{
  margin:0; position:relative; overflow:hidden;
  aspect-ratio:3/2; background:var(--paper-3);
  border:1px solid var(--line-soft);
}
.proc-shot > .photo{ width:100%; height:100%; object-fit:cover; display:block; }
/* .split.rev moves the FIRST child to the end; the media column is that
   child here, exactly as it is in every other .split on the site. */
.split.rev .proc-media-stack{ order:2; }

/* V67.3 — this rule is DEAD and kept only as a signpost. The site-wide
   heading entrance further down this file sets `margin-block-end:-.07em` on
   `.section :is(h2,.display-l,.display-m,.display-s)`, which outranks a
   single-class rule, so a bottom margin set here is silently replaced and the
   copy beneath ends up touching the heading (measured at MINUS 3px on the
   closing block, which is what the client reported). The step and closing
   headings therefore set their margins INLINE in tpl-process.php, the same way
   every other page on this site already does. Do not "tidy" those inline
   styles back into this rule. */
.proc-band-title{ margin-top:0; }
.proc-band .split-copy > p + p{ margin-top:1.2rem; }
/* A step with no photograph is a measured text band, not a half-empty split. */
.proc-band-solo{ max-width:72ch; }
.proc-band-solo.center{ max-width:80ch; margin-inline:auto; }
/* V67.4 — the closing block's paragraph was 112px left of its heading at
   desktop and tablet. .lead carries its own max-width:58ch, narrower than
   this 80ch box, and nothing gave it side margins — so the box sat flush
   left while its text centred inside itself. text-align:center on the
   parent centres inline content, never a block box with a max-width.
   Measured after: 0px at 1411 and 1024, unchanged at 390 (where .lead is
   already narrower than the viewport). MY defect, shipped in 2.78.0. */
.proc-band-solo.center > .lead{ margin-inline:auto; }

/* ============================================================
   V68 — CAN I BUILD  (.cib-*)
   The card's mock-up uses one repeating band: a copy column on the page
   ground and a photograph bleeding to the opposite edge. Three sections
   share it (hero, five steps, FAQ), so it is one rule set, not three.

   COLOUR: the PDF prints every band dark green. That is the printer, not
   the design — confirmed with Bianca. These bands sit on the site's own
   --ink / --paper-2 grounds like every other page. Rule #78.
   ============================================================ */
.cib-band{
  display:grid;
  /* V68.3 — 45.6%, measured off the mock-up PNG's hard vertical edge, not 50%. */
  grid-template-columns:45.6% 1fr;
  align-items:center;
  background:var(--paper-2);
  border-block:1px solid var(--line-soft);
}
.cib-band + .cib-wash, .cib-wash + .cib-band{ border-top:0; }

/* .cib-wash — the hero and the FAQ.
   V68.1: these were built as a hard 1fr 1fr split. WRONG. Re-read the mock-up
   at 150dpi: the photograph runs FULL BLEED under both bands and a dark wash
   fades across it left to right — roofs and trees are visible through the dark
   side. Only the five-steps band (page 2) is a true hard split; its panel edge
   is a clean vertical line with no image behind it. Do not "simplify" these
   two back into .cib-band. */
.cib-wash{
  position:relative; overflow:hidden;
  display:grid; align-items:center;
  border-block:1px solid var(--line-soft);
  background:var(--ink);
}
.cib-wash > .photo{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; z-index:0; }
/* V68.7 — the hero photo is NOT flush left in the mock-up. Correlating the
   mock-up against the clean source (0.992) puts it at 1.025 scale with its
   left edge 6.3% in and its right edge overflowing to 108.8%. Flush-left
   placement shifts the whole composition ~6% left, which is why the modular
   home and the dimension bars never lined up. */
.cib-hero > .photo{ left:6.3%; right:auto; width:102.5%; }
.cib-wash > .scrim{
  position:absolute; inset:0; z-index:1; pointer-events:none;
  /* V68.6 — the wash profile, DERIVED not estimated: the mock-up PNG was
     divided against the clean source photograph to recover the overlay's
     alpha per column. Result: alpha 1.00 from 5% to 33%, below 0.90 at 33.7%,
     below 0.50 at 43.0%, and 0 by 44%. A solid panel with a short, sharp
     fade — NOT the long ramp this used to have, which dimmed the middle of
     the picture where the modular home actually sits.
     The copy column is capped to match, so text never leaves the solid part. */
  background:linear-gradient(96deg,
    rgba(16,24,32,1) 0%, rgba(16,24,32,1) 32%, rgba(16,24,32,.70) 38%,
    rgba(16,24,32,.30) 44%, rgba(16,24,32,0) 52%);
}

/* ============================================================
   V88 — THE JOIN BETWEEN THE HERO AND THE FIVE-STEPS BAND
   ------------------------------------------------------------
   Client, 10 Sep: "the bottom of the hero image joins directly onto the second
   image below, which makes the two images look a little awkwardly attached/cut
   together... please also remove the thin horizontal line/border visible
   across the join."
   Both photographs are dusk shots and both bleed to the section edge, so they
   met at a hard seam with a 1px hairline across it and read as one picture.
   Three changes, using the site's own idiom rather than a new one:
     1. The hero's photograph now settles into --ink at its base, exactly the
        way .page-hero-band's own scrim does (see its 180deg layer).
     2. The hairline between the two sections is gone.
     3. A band of ink sits between them so they start and stop deliberately.
   ============================================================ */
.cib-hero > .scrim{
  background:
    linear-gradient(96deg,
      rgba(16,24,32,1) 0%, rgba(16,24,32,1) 32%, rgba(16,24,32,.70) 38%,
      rgba(16,24,32,.30) 44%, rgba(16,24,32,0) 52%),
    linear-gradient(180deg,
      rgba(16,24,32,0) 0%, rgba(16,24,32,0) 62%, rgba(16,24,32,.18) 80%,
      rgba(16,24,32,.62) 94%, var(--ink) 100%);
}
.cib-hero{ border-block-end:0; }
.cib-steps{ border-block-start:0; margin-block-start:clamp(2rem, 4.5vw, 4.5rem); }
/* Matched to the wash: the mock-up's longest copy line ends at ~33% of the
   band, inside the solid panel. 54% pushed text out over bare photograph. */
/* max-width is border-box, so the block's own gutters have to be added back or
   the TEXT column lands ~5% short of the mock and the H1 breaks to two lines.
   33% is where the mock's longest line ends — still inside the solid panel. */
.cib-wash .cib-copy{ position:relative; z-index:2;
  max-width:calc(33% + clamp(1.4rem,4vw,4rem)); }
/* display-l holds at 5.6vw through the mid range, but the copy column is only
   ~29.4vw of usable measure and "Can I Build?" runs 5.53x its own font-size —
   so anything above 5.31vw breaks the H1 to two lines. The mock has it on one
   line at every width. Capped just under that limit. */
.cib-hero .display-l{ font-size:clamp(2.2rem, 5.1vw, 4.6rem); }

/* V68.1 — the hero needs height or the aerial crops to roofs and loses the
   modular home the whole page is about. Matched to .page-hero-band's 70vh
   so it sits in the same rhythm as every other page hero. The FAQ band has
   no min-height: its accordion already makes it tall. */
/* V68.5 — the hero must show the WHOLE aerial: the 20m / 15m / 36m dimension
   labels sit at the very edges of the frame, and a cover-crop was cutting the
   top one off (only 87% of image height visible at 1440). Pinning the band to
   the source's own 1693x929 ratio means object-fit:cover has nothing to crop.
   min-height is the floor for narrow viewports where the copy needs the room. */
/* V83 — the hero is FULL SCREEN. svh, not vh, so mobile browser chrome does
   not push the bottom of the copy under the address bar. The source is 1.822:1
   and a full-height band at 1440 is ~1.64:1, so cover scales by HEIGHT: the
   whole image height stays visible (the "20 m" label at the top is safe) and
   only ~5% is trimmed from each side, which is under the wash on the left and
   outside the "36 m" label on the right. */
/* V88 — CLIENT, 10 Sep: the hero "feels quite tall compared with the hero
   sections across the rest of the website" and there is "a wide black area".
   Both came from V83 making this hero 100svh with the copy anchored to the
   bottom: 52-58% of the band was empty navy above the eyebrow (measured 628px
   of it on a 1080-tall screen).
   Fixed two ways. The band goes back to the photograph's own 1693x929 ratio,
   which is ~790px at 1440 instead of a full 900-1080, and the copy is CENTRED
   like every other hero on the site (.page-hero-band is min-height:70vh with
   align-items:center). The empty band above the copy drops to about a fifth of
   what it was, and it now reads as deliberate framing rather than a gap.
   The ratio is kept rather than dropping to a flat 70vh because this
   photograph's whole point is the 20 m / 15 m / 36 m dimension labels at its
   edges — a shorter band cover-crops them off. */
/* ⚠ NO min-height ON THIS RULE. aspect-ratio + min-height lets the browser
   derive WIDTH from height: at 1024x900 the 70vh floor (630px) drove the band
   to 630 x 1.823 = 1148px wide inside a 1024 viewport and pushed +124px of
   horizontal overflow onto the page. Third time this pairing has done it on
   this project. The ratio alone is enough — it is what keeps the dimension
   labels in frame, and it is shorter than the old 100svh at every width. */
.cib-hero{ aspect-ratio:1693 / 929; }
/* NOTE: aspect-ratio and min-height together let the browser derive WIDTH from
   height — the band computed to 1017px inside a 390px viewport. Below the
   stack breakpoint the aspect is dropped and min-height takes over alone. */
@media (max-width:900px){
  /* Stacked: no ratio (it derives WIDTH from height and blows past the
     viewport), and 52vh to match .page-hero-band's own mobile height. */
  .cib-hero{ aspect-ratio:auto; min-height:max(520px, 52vh); }
}

/* V68.2 — corrections after reading the DIGITAL mock-up PNGs. Everything above
   was built from the scanned printout, which loses colour and fine detail.
   The PNGs are the source of truth for design; the card is for copy. */

/* Ghost button reads gold on gold, not white on white-ish. */
/* V68.4 — gold border, WHITE label. (.btn.ghost is (0,2,0), so a single class
   can never win — matched here.) */
.btn.ghost.cib-ghost{ --fg:var(--paper); --bd:rgba(179,148,94,.75); }

/* Stacked, left-aligned, each button sized to its own text. */
.cib-actions{ flex-direction:column; align-items:flex-start; gap:.85rem; width:max-content; flex-wrap:nowrap; }

/* The mock-up's eyebrow is plain gold caps with no leading rule. */
.cib-eyebrow::before{ display:none; }

/* The copy block sits low in the frame, not centred: measured off the mock-up
   it spans 36%-84% of the hero height, centre ~60%. */
/* V88 — was align-items:end with a deep bottom pad, measured off the mock-up.
   The client asked for consistency with the other page heroes, which centre
   their copy. Centred here too. */
.cib-hero{ align-items:center; }

/* Step cards: bigger numeral, bigger title, and the arc notch cut into the
   lower-right of each card. */
.cib-step{ position:relative; overflow:hidden; }
.cib-step::after{
  /* Identical to .fx-key::after — same corner, size, weight and opacity. */
  content:""; position:absolute; right:-14px; bottom:-14px;
  width:54px; height:54px; border-radius:50%;
  border:2px solid rgba(196,150,74,.3);
  pointer-events:none;
}
.cib-step-cta{ border:1px solid rgba(179,148,94,.75); }

.cib-fineprint--center{ text-align:center; color:var(--paper); opacity:.78; }

/* FAQ: the mock-up boxes the whole accordion in a gold hairline and sets the
   questions in uppercase. The +/- toggle already exists in .faq-accordion. */
/* V83 — the FAQ panel is WIDER than the hero's. Measured off the mock: the
   accordion's right edge lands at ~54% of the band and the photograph is clear
   of the wash by ~60%. The hero's 33% panel was cutting into the picture and
   squeezing the question rows. */
.cib-faq > .scrim{
  background:linear-gradient(96deg,
    rgba(16,24,32,1) 0%, rgba(16,24,32,1) 50%, rgba(16,24,32,.80) 56%,
    rgba(16,24,32,.42) 62%, rgba(16,24,32,.12) 68%, rgba(16,24,32,0) 73%);
}
.cib-wash.cib-faq .cib-copy{ max-width:calc(55% + clamp(1.4rem,4vw,4rem)); }
/* V85 — the FAQ photograph is not full-bleed in the mock either. Landmarks
   read off the mock against the source: the house's left corner sits at 35%
   of the source and 68% of the band; the roofline at 22% of the source and
   28% of the band; the deck edge 55% -> 57%. Two vertical points give a scale
   of 0.879 of the band height, which at the source's 1.777:1 puts the photo at
   82.4% of the band width with its left edge at 39.2% — overflowing the right
   edge, the same construction as the hero. Full-bleed drew the house far too
   large and too far left. */
.cib-faq > .photo{ left:39.2%; right:auto; width:82.4%; }

/* V86 — the photograph must not zoom when a question is opened.
   Cause: this band is taller than it is proportioned, so object-fit:cover
   scales by HEIGHT. Opening an answer grew the band 11.7%-19.2% (measured at
   1920/1440/1280/1024/901) and the picture scaled with it.
   Fix, in two parts:
     1. The <details> share a name, so only one answer can be open at a time.
        That makes the growth bounded by the tallest single answer.
     2. That much slack is reserved at the FOOT of the copy column and
        collapses when an answer opens. Nothing visible moves — the band
        height stays put, so the photograph never rescales.
   Reserve tracks the measured growth: 200px at 901 falling to ~108px at 1920. */
@media (min-width:901px){
  .cib-faq .cib-copy{ --cib-reserve:192px; }
  .cib-faq .cib-copy::after{ content:""; display:block; height:var(--cib-reserve); }
  .cib-faq .cib-copy:has(.qa[open])::after{ height:0; }
  /* Every answer panel is padded to the same height as the reserve, so the
     collapse and the expansion cancel exactly — not just on the longest
     answer. Without this the shortest answer still shrank the band 5.5%. */
  .cib-faq .qa > .a{ min-height:var(--cib-reserve); box-sizing:border-box; }
}
/* Stepped to the measured growth at each range, not interpolated — the
   answers rewrap, so it is not linear in vw. */
@media (min-width:1000px){ .cib-faq .cib-copy{ --cib-reserve:164px; } }
@media (min-width:1201px){ .cib-faq .cib-copy{ --cib-reserve:136px; } }
@media (min-width:1600px){ .cib-faq .cib-copy{ --cib-reserve:108px; } }
/* "Common questions. Clear answers." measures 16.85x its own font-size and the
   55% column gives ~51.4vw of measure, so display-m's own scale breaks it in
   two. One line in the mock, one line here. */
.cib-faq .display-m{ font-size:clamp(1.6rem, 2.95vw, 2.7rem); }

.cib-faq .faq-accordion{
  max-width:none; margin-inline:0;
  border:1px solid rgba(179,148,94,.45);
  border-radius:clamp(8px,.8vw,12px);
  padding-inline:clamp(1rem,1.6vw,1.6rem);
}
.cib-faq .faq-accordion .qa{ border-bottom:1px solid rgba(179,148,94,.28); }
.cib-faq .faq-accordion .qa:last-child{ border-bottom:0; }
.cib-faq .faq-accordion .qa summary{
  text-transform:uppercase; letter-spacing:.055em; color:var(--paper);
  font-weight:600;
}

.cib-copy{
  padding:clamp(2.6rem,5vw,5.5rem) clamp(1.4rem,4vw,4rem);
  padding-inline-start:max(var(--edge), calc((100vw - var(--maxw)) / 2 + var(--edge)));
  min-width:0;
}
/* ⚠ overflow:hidden IS LOAD-BEARING — V138. The hero photograph carries the
   site's cinematic drift (.kb), which scales it slowly past 100%. In the old
   full-bleed hero that was contained by .cib-wash's own overflow:hidden. Moved
   into a two-column band, nothing clipped it and the scaled image pushed 9px
   of horizontal scroll onto the page at 1440 and 4px at 390. Found by listing
   every element wider than the viewport, not by looking at the page. */
.cib-media{ position:relative; align-self:stretch; overflow:hidden; min-height:clamp(280px,42vw,620px); }
.cib-media .photo{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }

/* ════════════════════════════════════════════════════════════════════════════
   V138 · CAN I BUILD? — THE 22 SEPTEMBER REDESIGN
   Max, card 66, after meeting Jordan. The client's words: the page "looks too
   much like an AI-generated/mock-up page" and should "feel more deliberately
   designed and architectural". The brief names the palette to stay on:
   "the existing charcoal/dark background, warm white text and restrained gold
   accents", and says not to take colours or typography from the concept site
   Jordan supplied. So nothing here introduces a colour or a face; every value
   below is an existing site token.
   ════════════════════════════════════════════════════════════════════════════ */

/* The hero and the four-steps intro are both .cib-band, which ships on
   --paper-2, the light ground. Both are dark here. The band itself is
   untouched so the five-steps layout it was built for still works anywhere
   else it is used. */
.cib-hero2, .cib-intro{ background:var(--ink); border-block-color:rgba(179,148,94,.18); }

/* V145 — THE FOUR-STEP SECTION IS ONE PANEL, ONE COLOUR.
   Max, 23 Sep: the heading block and the steps "should be as 1 session".

   MEASURED, not chosen. Max's reference (4_steps.webp) was read pixel by
   pixel: the copy half samples rgb(0,9,22) and the steps half samples
   rgb(0,9,22) — the SAME value — with a single warm hairline between them at
   y=370, peaking at rgb(92,83,70). So the reference is one panel divided by
   a rule, not two panels butted together.

   #010916 is what .cib-steprow already used, sampled from Max's earlier
   concept in V139. The whole band now takes it, so the steps did not move to
   meet the copy; the copy moved to meet the steps, which is the half he has
   already approved twice.

   THE HERO TAKES IT TOO, and that is a consequence of the fade below, not a
   free choice. Max also asked for the hero image to "blend into the charcoal
   background of the following section". The following section is this panel.
   If the hero stayed on var(--ink) #101820 while the panel sat at #010916,
   the fade would land on one colour and immediately meet another, and the
   left half — copy, no photo — would show a flat step with nothing to
   disguise it. Fading into a colour that is not there is not blending.

   The two values are 15/15/10 apart in RGB, so this is a tonal nudge rather
   than a recolour, and #010916 is Max's own figure from both his concept and
   this reference. The rule between the two bands goes with it: a gold
   hairline is exactly the abrupt ending he asked to remove. */
.cib-hero2, .cib-intro{ background:#010916; }
.cib-hero2{ border-block-end:0; }
.cib-intro{ border-block-start:0; }

/* V145 — AND THE TWO PHOTOGRAPHS STILL MUST NOT TOUCH.
   Removing the rule above satisfied the 23 Sep note but broke the 10 Sep one
   on this same card: "the bottom of the hero image joins directly onto the
   second image below, which makes the two images look a little awkwardly
   attached/cut together. Please adjust the transition so there is clearer
   visual separation." That rule was the only thing holding them apart at the
   photo column, and measuring the junction showed them meeting on adjacent
   pixels once it went — hero fading to rgb(2,10,23) at y=717, the aerial
   opening at rgb(65,66,71) at y=718.

   So the separation moves from a line to a space. The aerial is inset from
   the top of its row, which leaves a strip of flat panel colour between the
   two images. Both notes are then satisfied at once: the hero fades out
   properly, and the pictures read as two deliberate sections rather than one
   cut in half. A space also cannot be mistaken for a section break the way a
   gold rule was.

   On the copy column there is nothing to separate and nothing happens there;
   below 901px the band is one column and this becomes the gap between the
   hero and the aerial stacked above the copy, which is the same job. */
.cib-intro > .cib-media{ margin-block-start:clamp(18px,2.4vw,36px); }
.cib-hero2 .cib-copy, .cib-intro .cib-copy{ color:var(--paper); }
.cib-hero2 .lead, .cib-intro .lead{ color:var(--ink-soft); }

/* THE HERO IS TALLER THAN THE INTRO BAND, deliberately. It is the first thing
   on the page and the photograph is doing the work the old full-bleed aerial
   used to do. Not a viewport height: a fixed vh band leaves a dead zone on a
   short window, which is the exact fault Max raised on this page's first hero
   on 10 September ("there is a wide black area in the Hero section"). */
.cib-hero2 .cib-media{ min-height:clamp(340px, 46vw, 640px); }
.cib-intro .cib-media{ min-height:clamp(280px, 38vw, 520px); }

/* V140.1 — WHERE THE CROP SITS, not how big the box is.
   Bianca, 23 September: "its too zoom i think / look at the how the reference
   image displays". The box was never the problem. home-faster-2026.jpg is
   1864x843, a 2.21 frame, going into a 783x640 column at 1440. object-fit
   cover scales it to 1415x640, so only 55.3% of its width is ever on screen,
   and the browser's default object-position of 50% takes that 55.3% out of
   the MIDDLE — 22.3% to 77.7%. That throws away both white planters and the
   left glazing bay and lands on the black planter, which is what reads as
   zoomed in.

   The concept takes it off the left instead. Measuring where the black
   cylinder planter falls: 50.7% of the way across the source, and 84% of the
   way across the photo column in the comp. Solving for the crop that puts it
   there gives 8%, which shows 3.6% to 58.9% — both white planters in, both
   glazing bays in, the big tree on the right out of frame, exactly the
   reference.

   Hero only. The aerial in the intro band is a plan view of one block and
   wants its centre. */
.cib-hero2 .cib-media .photo{ object-position:8% center; }

/* V145 — THE HERO IMAGE FADES OUT AT THE BOTTOM, LIKE EVERY OTHER INTERNAL PAGE.
   Max, 23 Sep: "use the same bottom fade/gradient treatment that is already
   used on the other internal-page hero images, such as the About Us page...
   At the moment, the Can I Build? hero ends a little abruptly."

   The five stops below — their positions and their alphas — are taken
   VERBATIM from `.page-hero-band .scrim`, which is what About Us, Finance and
   the rest render. The ramp is not matched by eye and not re-derived.

   ONE THING DIFFERS: the colour. About Us fades to var(--ink) because
   var(--ink) is what sits under it. This band sits on #010916, so it fades to
   #010916. Copying var(--ink) here would have produced a gradient ending on a
   colour the page does not contain, which is the abrupt edge Max is asking us
   to remove, not a fix for it. Same treatment, correct terminus.

   Why an ::after and not a mask: this band is a grid, not a stacked band with
   a .scrim, and .cib-media already spends its mask-image on the left-edge
   feather from V140. A second mask layer would need mask-composite; an
   overlay needs nothing and is the same technique .scrim uses anyway. It sits
   inside .cib-media, so the left feather applies to it too and the corner
   stays clean — and where the feather removes it, the band's own background
   is already underneath.

   The photo itself is untouched. The V140 left feather and the 8% crop are
   both answers to earlier notes from Max on this same card. */
.cib-hero2 .cib-media::after{
  content:"";
  position:absolute;
  inset:0;
  z-index:1;
  pointer-events:none;
  background:linear-gradient(180deg,
    rgba(1,9,22,0) 0%,
    rgba(1,9,22,0) 52%,
    rgba(1,9,22,.16) 80%,
    rgba(1,9,22,.52) 97%,
    #010916 100%);
}

/* V140 — THE JUNCTION IS NOT A STRAIGHT CUT.
   Bianca, 23 September: "look at this navy on the middle, it should not be
   straight cut". Correct, and the mock-up says so in numbers. Scanning Max's
   concept horizontally across the hero junction, at a quarter of the way down
   the band, gives this run of pixels:

     #131D26 #131D26 #131D26 #131C25 #121B24 #101920 #0D141A #0C1117
     #090E12 #080C0F #05090C #060709  ... photograph

   The navy does not stop. It ramps down over roughly 110px of an 1840px-wide
   comp — about 6% of the page, or a shade over an eighth of the photo column —
   and the photograph comes up through it. V68.3 read that same junction as a
   "hard vertical edge" and set grid-template-columns off it; the column split
   was right, the edge treatment was not.

   Done as a mask on the media rather than a scrim over it, so the band's own
   var(--ink) is what shows through the fade. That keeps one navy on the page
   instead of a second, slightly-off one layered on top, and it survives the
   .kb drift because a mask is applied after overflow clipping.

   Desktop only. Below 901px .cib-band collapses to one column and the photo
   goes full-bleed above the copy, where a left-edge fade would eat the picture
   for no reason. */
@media (min-width:901px){
  .cib-hero2 .cib-media,
  .cib-intro .cib-media{
    -webkit-mask-image:linear-gradient(90deg,
      rgba(0,0,0,0) 0%,
      rgba(0,0,0,.28) 4.5%,
      rgba(0,0,0,.72) 9%,
      rgba(0,0,0,.94) 13%,
      #000 17%);
            mask-image:linear-gradient(90deg,
      rgba(0,0,0,0) 0%,
      rgba(0,0,0,.28) 4.5%,
      rgba(0,0,0,.72) 9%,
      rgba(0,0,0,.94) 13%,
      #000 17%);
  }
}

/* "See What's Possible / on Your Property" — the gold second line is the
   mock-up's own treatment and the only gold heading on the page. */
.cib-h1{ margin:1.1rem 0 1.1rem; font-size:clamp(2.1rem, 4.6vw, 4.1rem); line-height:1.04; }
.cib-h1-gold{ color:var(--gold); }
.cib-hero2-lead{ max-width:42ch; }
.cib-intro-h{ margin:1.1rem 0 1rem; line-height:1.12; }
.cib-intro-lead{ max-width:46ch; }
.cib-intro .cib-fineprint{ margin-top:1.1rem; }

/* ── THE FOUR STEPS, ONE ROW ──────────────────────────────────────────────
   "preferably in a cleaner horizontal/premium presentation". Four equal
   columns with hairline dividers between them, the same device the SMRT
   Townhouse popup's feature row uses, so the page borrows from the site
   rather than inventing a second visual language for the same job. */
/* V139 — THE BAND GOES DARKER, NOT LIGHTER.
   Bianca, 23 September: "the shading now of the navy background on the
   middle feels different from the old/new mock up". She is right, and it
   was the wrong direction, not the wrong amount. Sampling Max's concept
   (New_can_I_build__page_design_concept.jpg) down its left gutter gives
   #101920 for the hero and intro bands and #010916 for this one: the step
   row DROPS to a near-black navy so the four numbers sit in a well. V138
   used --ink-2 (#16212C), which lifts it instead, so the band read as a
   step up in the middle of the page rather than a step down.
   #010916 is the mock-up's own value, kept literal rather than tokenised,
   because nothing else on the site uses it and inventing an --ink-3 would
   put a palette-wide token in play for one band. */
/* V145 — now a child of .cib-intro, spanning both of its columns, so the
   copy/aerial row and the steps row are one element.

   No background of its own: it inherits the panel's #010916, which is what
   removes the colour step. No bottom rule either — the panel's own
   border-block already closes it, and two rules in 3rem of each other were
   half of why this read as two blocks.

   The top rule is the divider measured off the reference. Alpha .38 rather
   than the site's usual .18 hairline: the reference's line is clearly
   visible at rgb(92,83,70), which solves to roughly .5 gold over this
   background, spread across two rows of a downscaled image. .38 lands in
   the middle — a rule you can see, not a seam between sections. */
/* ⚠ V153 — THE RULE AND THE PADDING ARE SPLIT OFF THE GRID SELECTOR, because
   the home page now renders this same block and was silently getting neither.

   Max, card 101: "please Match those 4 steps on the home page." The markup
   moved into template-parts/cib-steps4.php so both pages share it, but the
   separator and the breathing room lived on `.cib-intro > .cib-steprow` — a
   DIRECT-CHILD selector against this page's grid. On the home page the block
   sits inside `.reveal.d1` inside `.wrap`, so it matched nothing.

   Measured at 1440, home page against this one:
     home     border-top 0px none, padding-block 0px
     canibuild border-top 1px solid rgba(179,148,94,.38), padding 51.2px

   The visible result was the four steps jammed up against the fine print with
   no rule between them — the "two things that should read as one section"
   problem V145 was raised to fix, reappearing on the other page as its
   opposite. What he praised and asked to match includes this spacing.

   `grid-column` STAYS on the direct-child selector, deliberately: it is only
   meaningful for a grid child, and applying it on the home page, where the
   parent is not a grid, would be a declaration that does nothing at best. */
.cib-steprow{
  border-top:1px solid rgba(179,148,94,.38);
  padding-block:clamp(2rem,3.6vw,3.2rem);
}
.cib-intro > .cib-steprow{
  grid-column:1 / -1;
}
.cib-steprow-eyebrow{ display:flex; justify-content:center; text-align:center; color:var(--gold-deep); margin-bottom:clamp(1.3rem,2.4vw,2rem); }
.cib-steps4{
  list-style:none; margin:0; padding:0;
  display:grid; grid-template-columns:repeat(4, minmax(0,1fr));
}
.cib-step4{
  display:flex; flex-direction:column; gap:.4rem;
  padding-inline:clamp(.9rem,1.8vw,1.8rem);
  border-inline-start:1px solid rgba(244,238,228,.13);
}
.cib-step4:first-child{ border-inline-start:0; padding-inline-start:0; }
/* V140.2 — THE ROW SAT 29px OFF CENTRE.
   Bianca, 23 September: "observe spacing here". Measured at 1920, with the
   band's content box running 149 to 1771:

     column 1   text 149 -> 526    flush to the left edge
     column 2   text 584 -> 931
     column 3   text 990 -> 1337
     column 4   text 1395 -> 1742  stopping 29px SHORT of the right edge

   :first-child dropped its leading padding so column 1 would sit flush, and
   nothing did the same for the trailing edge, so every cell's padding-inline
   left a 28.8px gutter hanging off the right of the row. The band's own
   padding is even (51 top, 52 bottom) and the columns are equal
   (405.4px each); it was only this. The eyebrow and the fine print are both
   centred, so nothing else on the band gave the drift away. */
.cib-step4:last-child{ padding-inline-end:0; }
.cib-step4-top{ display:flex; align-items:center; gap:.7rem; }
.cib-step4-ic{ color:var(--gold); width:30px; height:30px; display:block; flex:0 0 auto; }
.cib-step4-ic svg{ width:100%; height:100%; display:block; }
.cib-step4-n{
  font-family:var(--display); font-size:clamp(1.25rem,1.9vw,1.6rem);
  line-height:1; color:var(--paper); font-weight:400;
}
.cib-step4-t{
  font-family:var(--mono); font-size:.62rem; letter-spacing:.14em; text-transform:uppercase;
  color:var(--gold-deep);
}
.cib-step4-d{
  margin-top:.15rem;
  font-size:clamp(.86rem,1vw,.95rem); line-height:1.5; color:rgba(244,238,228,.72);
}
.cib-steprow .cib-fineprint--center{ margin-top:clamp(1.2rem,2.2vw,1.8rem); }

/* Two by two below 900, one column below 560. Four columns on a phone would
   put each description in a 70px gutter. */
@media (max-width:900px){
  .cib-steps4{ grid-template-columns:repeat(2, minmax(0,1fr)); row-gap:1.6rem; }
  .cib-step4:nth-child(3){ border-inline-start:0; padding-inline-start:0; }
  /* V140.2 — the mirror of nth-child(3). At two columns the row ends are 2
     and 4, not 4 alone, so 2 needs its trailing padding dropped as well or
     the first row stops 29px short of the second. */
  .cib-step4:nth-child(2){ padding-inline-end:0; }
}
@media (max-width:560px){
  .cib-steps4{ grid-template-columns:1fr; row-gap:1.4rem; }
  .cib-step4{ border-inline-start:0; padding-inline:0; }
}

/* ── THE CLOSING CTA ──────────────────────────────────────────────────────
   "make the closing section feel purposeful rather than simply adding another
   large empty/dark section." So: a photograph, with the copy over its darker
   half. The scrim is left-weighted rather than flat, so the copy gets its
   contrast and the rest of the house stays visible. */
.cib-close{ position:relative; overflow:hidden; isolation:isolate; }
.cib-close > .photo{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; object-position:70% 50%; z-index:0;
}
.cib-close-scrim{
  position:absolute; inset:0; z-index:1;
  background:linear-gradient(100deg,
    rgba(16,24,32,.94) 0%,
    rgba(16,24,32,.88) 32%,
    rgba(16,24,32,.58) 58%,
    rgba(16,24,32,.30) 100%);
}
.cib-close-inner{
  position:relative; z-index:2;
  padding-block:clamp(3.4rem,7vw,6.4rem);
  color:var(--paper);
}
.cib-close-h{ margin:1.05rem 0 1rem; line-height:1.1; max-width:18ch; }
.cib-close-lead{ max-width:44ch; color:rgba(244,238,228,.86); }
.cib-close-actions{ margin-top:clamp(1.5rem,2.6vw,2.2rem); flex-wrap:wrap; gap:.9rem; }
@media (max-width:560px){
  /* The gradient runs top to bottom on a phone, because a left-weighted one
     across a 390px column leaves the last words of each line on the bright
     part of the photograph. */
  .cib-close-scrim{ background:linear-gradient(180deg, rgba(16,24,32,.90) 0%, rgba(16,24,32,.80) 55%, rgba(16,24,32,.58) 100%); }
  .cib-close > .photo{ object-position:60% 50%; }
  .cib-close-actions .btn{ width:100%; justify-content:center; }
}

/* Five steps — heading is WHITE with a GOLD sub-line, beside a line-art icon. */
.cib-steps-head{ display:grid; grid-template-columns:auto 1fr; gap:clamp(.9rem,1.6vw,1.5rem); align-items:start; }
.cib-steps-ic{ width:clamp(54px,5vw,78px); height:auto; color:var(--gold-deep); flex:0 0 auto; }
.cib-steps-t{ text-transform:uppercase; letter-spacing:.045em; color:var(--paper);
  font-size:clamp(1.05rem,1.7vw,1.42rem); font-weight:600; line-height:1.15; }
.cib-steps-d{ color:var(--gold-deep); margin-top:.5rem; max-width:40ch;
  font-size:clamp(.96rem,1.15vw,1.06rem); line-height:1.45; }

.cib-steplist{ list-style:none; margin:1.7rem 0 0; padding:0; display:grid; gap:.55rem; }
.cib-step{
  display:grid; grid-template-columns:auto 1fr; gap:clamp(.9rem,1.5vw,1.4rem); align-items:center;
  /* V68.4 — the site ALREADY has this card. .fx-key, five across on every
     model page: 12px radius, --line-gold hairline, --ink-2 ground, and a 54px
     2px arc tucked into the bottom-right corner. Matched exactly rather than
     inventing a second treatment, so Can I Build reads as the same site. */
  background:var(--ink-2);
  border:1px solid var(--line-gold);
  border-radius:12px;
  padding:.95rem clamp(1rem,1.6vw,1.5rem);
  /* V68.3 — the arc sweeps in from the right. Reserve room for it or the
     description runs underneath and reads as clipped text. Measured before:
     textCollidesWithArc = true. */
  padding-inline-end:clamp(3.1rem,4vw,4rem);
}
.cib-step-n{ font-family:var(--mono); font-size:clamp(1.85rem,3.1vw,2.7rem); font-weight:500; color:var(--gold-deep); line-height:1; }
.cib-step-b{ display:grid; gap:.12rem; min-width:0; }
.cib-step-t{ text-transform:uppercase; letter-spacing:.045em; font-weight:600;
  font-size:clamp(1.02rem,1.42vw,1.28rem); color:var(--paper); line-height:1.15; }
/* V68.1 — gold, not grey. Checked against the mock-up at 150dpi. */
.cib-step-d{ font-size:clamp(.88rem,1.02vw,.99rem); color:var(--gold-deep); line-height:1.4; }
.cib-step-cta{ display:flex; justify-content:center; width:100%; margin-top:1.6rem; }

.cib-fineprint{ margin-top:1rem; font-size:.84rem; color:var(--ink-soft); opacity:.85; }
.cib-fineprint--ruled{ margin-top:1.8rem; padding-top:1.2rem; border-top:1px solid var(--line-soft); }

/* Rule — monogram — rule, above the property checker. */
/* V68.5 — the mock-up sets this lead in exactly two lines; .lead's 58ch
   measure was breaking it to three. */
#property-checker .sec-head{ max-width:none; }
#property-checker .sec-head .lead{ max-width:84ch; }

.cib-mono{ display:grid; grid-template-columns:1fr auto 1fr; align-items:center;
  gap:clamp(1rem,2vw,2rem); max-width:640px; margin:clamp(1.6rem,3vw,2.6rem) auto 0; }
.cib-mono::before, .cib-mono::after{ content:""; height:1px; background:rgba(179,148,94,.55); }
.cib-mono img{ width:clamp(22px,2.2vw,30px); height:auto; display:block; opacity:.9; }

@media (max-width:900px){
  .cib-band{ grid-template-columns:1fr; }
  .cib-media{ order:-1; min-height:clamp(220px,52vw,360px); }
  .cib-copy{ padding-inline:var(--edge); }
  .cib-wash .cib-copy{ max-width:none; }
  /* V85 — stacked: both photos go back to full-bleed. The hero's and the FAQ's
     right-side placements only make sense beside a copy column. */
  .cib-hero > .photo, .cib-faq > .photo{ left:0; right:0; width:100%; }
  .cib-wash > .scrim{ background:linear-gradient(180deg, rgba(16,24,32,.90) 0%, rgba(16,24,32,.87) 55%, rgba(16,24,32,.83) 100%); }
  .cib-steps-head{ grid-template-columns:auto 1fr; }
}
.proc-band-solo > p + p{ margin-top:1.2rem; }

/* ---------- Thank-you state ---------- */
.thankyou{
  display:none; flex-direction:column; align-items:flex-start; gap:.7rem;
  padding:clamp(1.8rem,3vw,2.6rem);
  background:var(--paper-3); border-left:4px solid var(--gold);
}
.thankyou.show{ display:flex; animation:tyIn .5s var(--ease-out) both; }
@keyframes tyIn{ from{ opacity:0; transform:translateY(10px);} to{ opacity:1; transform:none; } }
.ty-tick{
  display:grid; place-items:center;
  width:2.6rem; height:2.6rem; border-radius:999px;
  background:var(--gold); color:var(--ink); font-size:1.25rem; line-height:1;
}
.thankyou h3{
  font-family:var(--display); font-weight:340; font-size:1.7rem;
  color:var(--paper); margin:.4rem 0 0;
}
.thankyou p{ font-size:1rem; line-height:1.7; color:var(--muted); margin:0; }
.thankyou .ty-alt{ font-size:.92rem; }
.thankyou .ty-alt a{ color:var(--gold-deep); font-weight:600; }

/* ---------- Trustindex wrapper ---------- */
.trustindex-wrap{ margin-top:.5rem; }
.trustindex-wrap .ti-widget{ margin:0 !important; }

/* ---------- FAQ page polish ---------- */
.faq-cat{ margin-bottom:clamp(2.4rem,4vw,3.6rem); }
.faq-cat > h2{
  font-family:var(--mono); font-size:.7rem; letter-spacing:.2em;
  text-transform:uppercase; color:var(--gold-deep);
  margin:0 0 .4rem; padding-bottom:0;
}
.faq-accordion.faq-cat{ border-top:0; }
.faq-cat.faq-accordion .qa:first-of-type{ border-top:1px solid var(--line); }

/* ---------- Where to next ---------- */
.next-grid{
  display:grid; grid-template-columns:1fr 1fr;
  gap:clamp(24px,4vw,64px);
  max-width:960px; margin-inline:auto;
}
.next-col{ display:flex; flex-direction:column; }
.next-label{
  font-family:var(--mono); font-size:.62rem; letter-spacing:.2em;
  text-transform:uppercase; color:var(--gold-deep);
  padding-bottom:1rem; margin-bottom:.2rem;
  border-bottom:1px solid var(--line-gold);
}
.next-link{
  display:flex; flex-direction:column; gap:.25rem;
  padding:1.25rem 2rem 1.25rem 0;
  border-bottom:1px solid var(--line);
  position:relative;
  transition:padding-left .3s var(--ease);
}
.next-link::after{
  content:"\2192"; position:absolute; right:.3rem; top:1.35rem;
  color:var(--gold-deep); opacity:0; transform:translateX(-6px);
  transition:opacity .3s var(--ease), transform .3s var(--ease);
}
.next-link:hover{ padding-left:.5rem; }
.next-link:hover::after{ opacity:1; transform:none; }
.nl-title{ font-size:1.05rem; color:var(--paper); }
.next-link:hover .nl-title{ color:var(--gold-deep); }
.nl-desc{ font-size:.86rem; line-height:1.55; color:var(--muted-2); }

@media (max-width:900px){
  .enq-panel{ grid-template-columns:1fr; }
  .next-grid{ grid-template-columns:1fr; gap:2.4rem; }
}

/* ============================================================
   V3 ADDITIONS — logo · contact icons · SMRT partner · next cards
   ============================================================ */

/* ---------- Brand logo (header) ---------- */
.brand-logo{ height:clamp(52px,5.6vw,68px); width:auto; display:block; }
@media (max-width:620px){ .brand-logo{ height:48px; } }

/* ---------- Brand logo (footer, on dark) ---------- */
/* The lockup uses dark text, so render it solid white for the dark footer. */
.foot-logo{ width:100%; max-width:240px; height:auto; display:block; margin-bottom:.6rem; }
.foot-logo--tint{ filter:brightness(0) invert(1); }
@media (max-width:620px){ .foot-logo{ max-width:210px; } }

/* ---------- Footer contact list with icons ---------- */
.foot-contact{ list-style:none; margin:0; padding:0; display:grid; gap:1rem; }
.foot-contact li{ display:flex; align-items:flex-start; gap:.7rem; }
.foot-contact .fc-ic{ flex:0 0 auto; width:18px; height:18px; margin-top:.15rem; color:var(--gold-glow); }
.foot-contact .fc-ic svg{ width:18px; height:18px; display:block; }
.foot-contact .fc-txt{ color:var(--muted-dk); font-size:.94rem; line-height:1.55; transition:color .3s var(--ease); }
.foot-contact a.fc-txt:hover{ color:var(--paper); }
/* Phone & email use <a>, which inherits .foot-col a{display:block;padding:.42rem 0}.
   Reset it so the link's text baseline aligns with the icon like the showroom rows. */
.foot-contact a.fc-txt{ display:inline-block; padding:0; }
.foot-contact .fc-sub{ color:rgba(184,172,147,.7); font-size:.82rem; }

/* ---------- Enquiry detail rows with icons ---------- */
.enq-details li{ flex-direction:row; align-items:flex-start; gap:.9rem; }
.ed-ic{ flex:0 0 auto; width:1.6rem; height:1.6rem; margin-top:.15rem; color:var(--gold-glow); }
.ed-ic svg{ width:100%; height:100%; display:block; }
.ed-body{ display:flex; flex-direction:column; gap:.3rem; min-width:0; }

/* ---------- Showroom detail icons ---------- */
.spec-ic{ width:1rem; height:1rem; margin-right:.5rem; vertical-align:-2px; display:inline-block; }
.showroom-details .spec-code{ display:inline-flex; align-items:center; }

/* ---------- SMRT partnership block (home · The Company) ---------- */
.partner-block{
  display:grid; grid-template-columns:1fr minmax(80px,120px); align-items:center;
  gap:clamp(1.4rem,3vw,2.6rem);
  margin-top:2.4rem; padding-top:2.2rem;
  border-top:1px solid var(--line-gold);
}
.partner-text{ display:flex; flex-direction:column; align-items:flex-start; gap:1.1rem; }
.partner-copy{ font-size:.95rem; line-height:1.7; color:var(--muted); margin:0; max-width:64ch; }
.partner-more{ align-self:flex-start; }
.partner-logo{ flex:0 0 auto; display:block; transition:opacity .2s ease; }
.partner-logo:hover{ opacity:.72; }
.partner-logo img{ height:clamp(22px,2.2vw,30px); width:auto; display:block; }
@media (max-width:620px){
  .partner-block{ grid-template-columns:1fr; gap:1.4rem; }
  .partner-logo img{ height:26px; }
}

/* ---------- Where to next — modern cards ---------- */
.next-cards{
  display:grid; grid-template-columns:repeat(3,1fr);
  gap:clamp(16px,1.8vw,22px);
}
.next-card{
  display:flex; flex-direction:column; align-items:flex-start; gap:.55rem;
  padding:clamp(1.5rem,2.2vw,2rem);
  background:var(--card); border:1px solid var(--line-gold);
  transition:transform .35s var(--ease), box-shadow .35s var(--ease), border-color .35s var(--ease);
}
.next-card:hover{
  transform:translateY(-4px);
  border-color:var(--gold);
  box-shadow:0 18px 40px -22px rgba(0,0,0,.8);
}
.nc-ic{
  display:grid; place-items:center;
  width:2.6rem; height:2.6rem; margin-bottom:.4rem;
  border:1px solid var(--line-gold); border-radius:999px;
  color:var(--gold-deep);
  transition:background .35s var(--ease), color .35s var(--ease), border-color .35s var(--ease);
}
.nc-ic svg{ width:1.35rem; height:1.35rem; }
.next-card:hover .nc-ic{ background:var(--wash-gold); color:var(--gold-hi); border-color:var(--gold); }
.nc-tag{
  font-family:var(--mono); font-size:.6rem; letter-spacing:.2em;
  text-transform:uppercase; color:var(--gold-deep);
}
.nc-title{ font-family:var(--display); font-size:1.28rem; line-height:1.15; color:var(--paper); }
.nc-desc{ font-size:.9rem; line-height:1.55; color:var(--muted-2); }
.nc-go{
  margin-top:.5rem; font-family:var(--mono); font-size:.66rem; letter-spacing:.12em;
  text-transform:uppercase; color:var(--gold-deep);
  display:inline-flex; align-items:center; gap:.5em;
}
.nc-go .arrow{ transition:transform .3s var(--ease); }
.next-card:hover .nc-go .arrow{ transform:translateX(4px); }
@media (max-width:900px){ .next-cards{ grid-template-columns:1fr 1fr; } }
@media (max-width:620px){ .next-cards{ grid-template-columns:1fr; } }

/* ---------- Finance specialist card (Finance · Dom Forgione) ---------- */
.fin-spec-card{
  display:grid; grid-template-columns:minmax(300px,380px) 1fr; gap:clamp(1.6rem,3vw,3rem);
  align-items:start; background:var(--card); border-radius:18px;
  padding:clamp(1.2rem,2.4vw,1.8rem);
  border:1px solid var(--line-gold,rgba(138,108,58,.2));
  box-shadow:0 22px 60px rgba(0,0,0,.5);
}
/* V67 · Finance — the Wiz Home Loans flyer was removed from this card
   (client: "Remove the flyer only"), so the card reflows to one column rather
   than leaving the copy beside an empty 380px well. `.fsc-photo` below is kept
   because the two-column form is still the component's default and nothing
   about it changed; only this page opts out. */
.fin-spec-card--solo{ grid-template-columns:1fr; }
.fin-spec-card--solo .fsc-body{ padding-inline:clamp(.6rem,2vw,1.6rem); }
.fin-spec-card--solo .fsc-intro{ max-width:68ch; }

.fsc-photo{
  margin:0; border-radius:12px; overflow:hidden; align-self:start;
  border:1px solid var(--line); background:var(--ink-3);
  box-shadow:0 6px 18px rgba(0,0,0,.4);
}
.fsc-photo img{ display:block; width:100%; height:auto; }
.fsc-body{ padding:clamp(.4rem,1.2vw,1rem) clamp(.4rem,1.4vw,1rem); display:flex; flex-direction:column; }
.fsc-head{ padding-bottom:1.1rem; border-bottom:1px solid var(--line,rgba(22,19,13,.12)); }
.fsc-name{ font-family:var(--display); font-size:clamp(1.5rem,2.6vw,2rem); line-height:1.1; margin:0; color:var(--paper); }
.fsc-role{
  margin:.5rem 0 0; font-family:var(--mono); font-size:.8rem;
  letter-spacing:2px; text-transform:uppercase; color:var(--gold-deep);
}
.fsc-intro{ margin:1.2rem 0 0; color:var(--muted); line-height:1.65; max-width:52ch; }
.fsc-contact{ list-style:none; margin:1.4rem 0 0; padding:0; display:grid; gap:.7rem; }
.fsc-contact li{ display:flex; align-items:center; gap:.7rem; }
.fsc-ic{
  flex:0 0 auto; width:34px; height:34px; border-radius:9px; display:grid; place-items:center;
  background:var(--wash-gold); color:var(--gold-glow);
}
.fsc-ic svg{ width:17px; height:17px; }
.fsc-contact a{ color:var(--paper); text-decoration:none; font-weight:600; font-size:1.02rem; }
.fsc-contact a:hover{ color:var(--gold-deep); }
.fsc-creds{
  list-style:none; margin:1.6rem 0 0; padding:1.2rem 0 0; display:flex; flex-wrap:wrap;
  gap:.8rem 1.8rem; border-top:1px solid var(--line,rgba(22,19,13,.12));
}
.fsc-creds li{ font-size:.86rem; color:var(--muted); }
.fsc-creds li b{ color:var(--paper); font-family:var(--display); font-weight:600; }
.fsc-cta{ align-self:flex-start; margin-top:1.8rem; }
@media (max-width:720px){
  .fin-spec-card{ grid-template-columns:1fr; }
  .fsc-photo{ max-width:460px; margin-inline:auto; }
}

/* ---------- Model features & specifications (redesigned) ---------- */
.feat{
  display:grid; grid-template-columns:1fr 1fr; gap:1rem;
}
.feat-card{
  border:1px solid var(--line); border-radius:14px;
  background:linear-gradient(180deg,var(--ink-2),var(--paper-3));
  overflow:hidden; transition:box-shadow .25s ease, border-color .25s ease;
}
.feat-card[open]{ box-shadow:0 14px 40px rgba(0,0,0,.5); border-color:var(--line-gold); }
.feat-card > summary{
  list-style:none; cursor:pointer; display:flex; align-items:center; gap:1rem;
  padding:1.15rem 1.25rem;
}
.feat-card > summary::-webkit-details-marker{ display:none; }
.feat-ic{
  flex:0 0 auto; width:44px; height:44px; border-radius:11px;
  display:grid; place-items:center; color:var(--gold-glow);
  background:var(--wash-gold); transition:background .25s ease, color .25s ease;
}
.feat-ic svg{ width:22px; height:22px; }
.feat-card[open] .feat-ic{ background:var(--gold); color:var(--ink); }
.feat-head{ display:flex; flex-direction:column; gap:.15rem; min-width:0; }
.feat-title{ font-family:var(--display); font-size:1.12rem; color:var(--paper); line-height:1.2; }
.feat-sub{ font-size:.78rem; letter-spacing:.3px; color:var(--muted); }
.feat-chev{
  flex:0 0 auto; margin-left:auto; width:12px; height:12px; position:relative;
}
.feat-chev::before{
  content:""; position:absolute; inset:0; margin:auto; width:9px; height:9px;
  border-right:2px solid var(--gold); border-bottom:2px solid var(--gold);
  transform:translateY(-2px) rotate(45deg); transition:transform .25s ease;
}
.feat-card[open] .feat-chev::before{ transform:translateY(2px) rotate(-135deg); }
.feat-list{
  margin:0; padding:0 1.25rem 1.3rem calc(1.25rem + 44px + 1rem);
  list-style:none; display:grid; gap:.5rem;
}
.feat-list li{
  position:relative; padding-left:1.5rem; color:var(--muted);
  font-size:.94rem; line-height:1.55;
}
.feat-list li::before{
  content:""; position:absolute; left:0; top:.5em; width:11px; height:7px;
  border-left:2px solid var(--gold); border-bottom:2px solid var(--gold);
  transform:rotate(-45deg);
}
@media (max-width:760px){
  .feat{ grid-template-columns:1fr; }
  .feat-list{ padding-left:1.25rem; }
}

/* ---------- Model filter (Designs & How It's Done) ---------- */
.mf-controls{
  display:flex; flex-wrap:wrap; align-items:flex-end; gap:1.4rem 2rem;
  padding:clamp(1.1rem,2vw,1.6rem) clamp(1.2rem,2.4vw,1.9rem);
  margin-bottom:2.4rem; border-radius:16px;
  background:linear-gradient(180deg,var(--ink-2), var(--paper-3));
  border:1px solid var(--line-gold);
  box-shadow:0 10px 30px rgba(0,0,0,.36);
}
.mf-group{ display:flex; flex-direction:column; gap:.55rem; }
.mf-group-label{
  font-size:.68rem; letter-spacing:2.4px; text-transform:uppercase;
  color:var(--muted); font-family:var(--mono);
}
.mf-pills{
  display:inline-flex; gap:.35rem; padding:.3rem;
  background:var(--ink-3); border-radius:999px;
}
.mf-pill{
  appearance:none; cursor:pointer; border:0; background:transparent;
  font:inherit; font-size:.9rem; font-weight:600; color:var(--paper);
  padding:.5rem 1rem; border-radius:999px; line-height:1;
  transition:background .2s ease, color .2s ease, box-shadow .2s ease;
}
.mf-pill:hover{ background:var(--wash-gold); }
.mf-pill.is-active{
  background:var(--gold); color:var(--ink);
  box-shadow:0 4px 12px rgba(0,0,0,.4);
}
.mf-pill:focus-visible{ outline:2px solid var(--gold); outline-offset:2px; }
.mf-meta{ display:flex; align-items:center; gap:1rem; margin-left:auto; align-self:center; }
.mf-count{ margin:0; font-size:.9rem; color:var(--muted); }
.mf-reset{
  padding:.5rem 1rem; font:inherit; font-size:.85rem; font-weight:600; cursor:pointer;
  background:transparent; color:var(--gold-deep);
  border:1px solid var(--line-gold); border-radius:999px;
}
.mf-reset:hover{ background:var(--gold); color:var(--ink); }

/* ============================================================
   V87 — Gallery card: the "2+" bedrooms pill and the Custom
   Configurations link.
   ------------------------------------------------------------
   The "2+" pill is an <a>, not a <button>, because the card says it must OPEN
   the Custom Configurations page and must NOT filter the grid. .mf-pill is
   written for a button, so an anchor picks up its colour and padding but not
   its box: an inline element ignores the vertical padding and arrives
   underlined. These four declarations are the whole difference.
   ============================================================ */
.mf-pill--nav{
  display:inline-flex; align-items:center; text-decoration:none;
  color:var(--gold-deep); white-space:nowrap;
}
.mf-pill--nav:hover{ background:var(--gold); color:var(--ink); }
.mf-pill--nav:focus-visible{ outline:2px solid var(--gold); outline-offset:2px; }

/* Sits beside the design count, as the card asks. Two lines: the question in
   the muted body colour, the action in gold. */
.mf-custom{
  display:inline-flex; flex-direction:column; gap:.1rem;
  text-decoration:none; line-height:1.35;
}
.mf-custom-q{
  font-family:var(--mono); font-size:.62rem; letter-spacing:.14em;
  text-transform:uppercase; color:var(--muted);
}
.mf-custom-a{
  font-size:.85rem; font-weight:600; color:var(--gold-deep);
  display:inline-flex; align-items:center; gap:.4em;
}
.mf-custom:hover .mf-custom-a{ color:var(--gold); }
.mf-custom .arrow{ transition:transform .3s var(--ease); }
.mf-custom:hover .arrow{ transform:translateX(3px); }
.mf-custom:focus-visible{ outline:2px solid var(--gold); outline-offset:3px; border-radius:4px; }

/* The same two-line treatment in the mobile drawer that the desktop dropdown
   already had. .d-sublink is a flex row with a hidden number, so the text has
   to be its own column or the spec lands beside the name instead of under it. */
.drawer .d-sublink-text{ display:inline-flex; flex-direction:column; gap:.1rem; min-width:0; }
.drawer .d-sublink-name{ line-height:1.25; }
.drawer .d-sublink-spec{
  font-family:var(--mono); font-size:.6rem; letter-spacing:.05em;
  color:var(--muted-2); line-height:1.4;
}

/* ---------- V116 · card 94 — model thumbnails in the mobile drawer ----------
   "Position the thumbnail to the left of the model name. Use a consistent
   thumbnail size and aspect ratio across all models. Crop images consistently
   so the menu remains clean and aligned. Keep the model names easy to read.
   Maintain the existing Master Modular styling, spacing and colours. Keep the
   thumbnails relatively small so the mobile menu does not become excessively
   tall."

   One fixed box, 56 × 42 (4:3), object-fit:cover so every hero lands on the
   same crop whatever its own ratio is — that is what makes the column of names
   align. It takes the place of the hidden .num spacer, so the row keeps two
   columns and the names stay where they were.

   The indent drops to match, or the thumbnail would push every name right and
   undo the V?? fix above that stopped the long names wrapping. Row height is
   set by the 42px thumbnail, which is shorter than the two-line name plus spec
   it sits beside, so the menu does not get taller. */
.drawer .d-sublink--thumbed{ padding-left:1.1rem; gap:.75rem; align-items:center; }
.drawer .d-sublink-thumb{
  flex:0 0 auto; width:56px; height:42px; overflow:hidden;
  border:1px solid var(--line-gold); background:var(--ink-2);
}
/* Slightly below centre. The heroes are wide exterior shots with a lot of sky;
   a dead-centre crop on the palest of them (the white Sanctuary against a
   bright sky) came back as an apparently empty box. Dropping the crop window
   catches the building and its landscaping on every model without changing
   the box, the ratio or the alignment. */
.drawer .d-sublink-thumb img{ width:100%; height:100%; object-fit:cover; object-position:center 58%; display:block; }


.mf-grid{ position:relative; }
.mf-card{ transition:opacity .35s ease; }
.mf-card.mf-dim{ opacity:.5; }
.mf-card.mf-hit{ opacity:1; }
.mf-divider{
  grid-column:1 / -1; margin:.4rem 0 .2rem;
  display:flex; align-items:center; gap:1rem; color:var(--muted);
}
.mf-divider::before,.mf-divider::after{
  content:""; flex:1; height:1px; background:var(--line-gold);
}
.mf-divider span{
  font-family:var(--mono); font-size:.76rem; letter-spacing:2px;
  text-transform:uppercase; white-space:nowrap; color:var(--gold-deep);
}
@media (max-width:720px){
  .mf-controls{ gap:1.1rem 1.4rem; }
  .mf-meta{ margin-left:0; width:100%; justify-content:space-between; }
  .mf-pills{ flex-wrap:wrap; border-radius:14px; }
}


/* ---------- V6: FAQ full-width + homepage "Most asked" Top-10 ---------- */
.faq-full{ max-width:none; }
.faq-full .faq-cat{ max-width:none; }

.faq-top{ margin:0 auto 3rem; max-width:920px; }

/* V96 — "Two columns of five FAQs" (Max, 11 Sep). Column flow, not the row
   flow this had before: with row flow the ranks read 01 02 / 03 04 ACROSS,
   which makes a ranked list look shuffled. Five explicit rows and column flow
   put 01-05 down the left and 06-10 down the right, so the ranking reads the
   way a numbered list is read. The rows are fixed at 5 rather than left to
   auto-flow because the list is always exactly ten items; if that ever
   changes, change this number with it or the second column will run long. */
.faq-top-list{
  display:grid;
  grid-template-columns:repeat(2,1fr);
  grid-template-rows:repeat(5,auto);
  grid-auto-flow:column;
  gap:.7rem 1.4rem;
  align-items:stretch;
}
.faq-top-item{
  display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:.9rem;
  padding:.85rem 1.1rem; background:var(--card); border:1px solid var(--line-soft);
  text-decoration:none; color:var(--paper); transition:border-color .2s ease, transform .2s ease;
}
.faq-top-item:hover{ border-color:var(--line-gold); transform:translateY(-2px); }
.faq-top-item .ft-n{ font-family:var(--mono); font-size:.72rem; letter-spacing:.08em; color:var(--gold); }
.faq-top-item .ft-q{ font-size:.98rem; line-height:1.35; }
.faq-top-item .ft-c{ font-family:var(--mono); font-size:.7rem; color:var(--muted); }

/* One line, and the size is what gives way — not the line.
   Max: "keep the heading on one line rather than breaking it across two."
   `white-space:nowrap` alone would hold the line and then push the page into
   a horizontal scroll on a phone, trading a wrap for a much worse defect.
   Paired with a viewport-relative size that can shrink below the display-m
   floor, the heading stays on one line at every width and simply gets
   smaller where it has to. */
.h-1line{
  white-space:nowrap;
  font-size:min(clamp(1.9rem, 3.6vw, 3.05rem), 6.9vw);   /* = .display-m, capped so the line keeps the page gutter on a phone */
}

@media (max-width:720px){
  .faq-top-list{ grid-template-columns:1fr; grid-template-rows:none; grid-auto-flow:row; }
}


/* ---------- V6.1: The Company section (redesigned) ---------- */
.company-grid{
  display:grid; grid-template-columns:1.05fr .95fr;
  gap:clamp(28px,4vw,64px); align-items:center;
}
.company-lead .display-l{ margin-top:1.1rem; max-width:15ch; }
.company-lead .lead{ margin-top:1.4rem; max-width:46ch; }
.company-cta{ margin-top:1.8rem; }
.company-visual{
  position:relative; border:1px solid var(--line-soft); overflow:hidden;
  aspect-ratio:4/3; background:var(--paper-3);
  box-shadow:0 24px 60px -32px rgba(0,0,0,.85);
}
.company-visual .photo{ width:100%; height:100%; object-fit:cover; display:block; }

.partner-card{
  margin-top:clamp(34px,4vw,58px);
  display:grid; grid-template-columns:auto 1fr auto; align-items:center;
  gap:clamp(20px,3vw,40px);
  padding:clamp(20px,2.4vw,32px) clamp(22px,2.8vw,38px);
  background:var(--ink-2); border:1px solid var(--line-gold);
}
.partner-card-logo{ display:block; flex:none; }
.partner-card-logo img{ width:132px; max-width:34vw; height:auto; display:block; }
.partner-card-copy{ color:var(--paper); font-size:1rem; line-height:1.65; margin:0; max-width:62ch; }
.partner-card-cta{ flex:none; white-space:nowrap; }
/* Fix: ghost button on the dark partner card was dark-on-dark (invisible). Force light text + gold hover fill. */
.partner-card .btn.ghost{ --fg:var(--paper); --bd:rgba(255,255,255,.28); }
.partner-card .btn.ghost::after{ background:var(--gold); }
.partner-card .btn.ghost:hover{ color:var(--ink); border-color:var(--gold); }
@media (max-width:900px){
  .company-grid{ grid-template-columns:1fr; }
  .company-visual{ order:-1; aspect-ratio:16/10; }
  .partner-card{ grid-template-columns:1fr; text-align:left; gap:1.1rem; justify-items:start; }
  .partner-card-logo img{ width:120px; }
}

/* ============================================================
   12–14 WEEK POP-UP (home)
   ============================================================ */
.mmh-modal{ position:fixed; inset:0; z-index:1200; display:grid; place-items:center; padding:clamp(16px,4vw,40px); }
.mmh-modal[hidden]{ display:none; }
.mmh-modal-backdrop{ position:absolute; inset:0; background:rgba(4,7,10,.74); backdrop-filter:blur(3px); opacity:0; transition:opacity .35s var(--ease-out); }
.mmh-modal-card{
  position:relative; z-index:1; width:min(560px,100%);
  background:var(--ink-2); border:1px solid var(--line-gold); border-radius:8px;
  padding:clamp(28px,4vw,46px); text-align:center; overflow:hidden;
  box-shadow:0 40px 90px -40px rgba(22,19,13,.55);
  opacity:0; transform:translateY(18px) scale(.98); transition:opacity .4s var(--ease-out), transform .4s var(--ease-out);
}
.mmh-modal.is-open .mmh-modal-backdrop{ opacity:1; }
.mmh-modal.is-open .mmh-modal-card{ opacity:1; transform:none; }
.mmh-modal-accent{ position:absolute; z-index:3; top:0; left:0; right:0; height:4px; background:linear-gradient(90deg,var(--gold-glow),var(--gold),var(--gold-deep)); }
.mmh-modal-media{ margin:calc(-1 * clamp(28px,4vw,46px)) calc(-1 * clamp(28px,4vw,46px)) 0;
  aspect-ratio:16/7; overflow:hidden; background:var(--ink); }
.mmh-modal-media img{ width:100%; height:100%; object-fit:cover; display:block; }
.mmh-modal .eyebrow{ justify-content:center; margin-top:.4rem; }
.mmh-modal-card h2{ margin:1rem 0 0; }
.mmh-modal-copy{ margin:1.1rem auto 0; max-width:46ch; color:var(--ink-soft); }
.mmh-modal-x{
  position:absolute; z-index:3; top:12px; right:12px; width:38px; height:38px; display:grid; place-items:center;
  background:transparent; border:1px solid var(--line-ui); border-radius:50%; color:var(--paper);
  cursor:pointer; transition:background .25s var(--ease), color .25s var(--ease), border-color .25s var(--ease);
}
.mmh-modal-x svg{ width:17px; height:17px; }
.mmh-modal-x:hover{ background:var(--gold); color:var(--ink); border-color:var(--gold); }
.mmh-steps{ list-style:none; margin:1.8rem 0 0; padding:0; display:flex; justify-content:center; gap:0;
  font-family:var(--mono); font-size:.72rem; letter-spacing:.12em; text-transform:uppercase; color:var(--gold-deep); }
.mmh-steps li{ position:relative; display:flex; flex-direction:column; align-items:center; gap:.6rem; flex:1; max-width:150px; }
.mmh-steps li .dot{ width:11px; height:11px; border-radius:50%; background:var(--gold); box-shadow:0 0 0 4px rgba(179,148,94,.16); }
.mmh-steps li::before{ content:""; position:absolute; top:5px; left:-50%; width:100%; height:1px; background:var(--gold); opacity:.4; }
.mmh-steps li:first-child::before{ display:none; }
.mmh-modal-actions{ display:flex; flex-wrap:wrap; gap:.8rem; justify-content:center; margin-top:2rem; }
@media (max-width:520px){
  .mmh-modal-actions .btn{ width:100%; justify-content:center; }
  .mmh-steps{ font-size:.64rem; }
}
body.mmh-modal-lock{ overflow:hidden; }
@media (prefers-reduced-motion:reduce){
  .mmh-modal-card{ transition:opacity .2s linear; transform:none; }
  .mmh-modal-backdrop{ transition:opacity .2s linear; }
}

/* ============================================================
   SCROLL-TO-TOP + READING PROGRESS
   ============================================================ */
.mmh-totop{
  position:fixed; right:clamp(16px,2.5vw,30px); bottom:clamp(16px,2.5vw,30px); z-index:1100;
  width:52px; height:52px; padding:0; border:0; border-radius:50%; cursor:pointer;
  background:var(--ink-2); color:var(--paper); border:1px solid var(--line-gold);
  box-shadow:0 14px 30px -12px rgba(0,0,0,.7);
  display:grid; place-items:center;
  opacity:0; transform:translateY(14px) scale(.9); pointer-events:none;
  transition:opacity .35s var(--ease-out), transform .35s var(--ease-out), background .25s var(--ease), color .25s var(--ease);
}
.mmh-totop.show{ opacity:1; transform:none; pointer-events:auto; }
.mmh-totop:hover{ background:var(--gold); color:var(--ink); }
.mmh-totop-ring{ position:absolute; inset:0; width:100%; height:100%; transform:rotate(-90deg); }
.mmh-totop-ring .rt{ fill:none; stroke:rgba(179,148,94,.22); stroke-width:2.4; }
.mmh-totop-ring .rp{ fill:none; stroke:var(--gold); stroke-width:2.4; stroke-linecap:round;
  stroke-dasharray:125.66; stroke-dashoffset:125.66; transition:stroke-dashoffset .1s linear; }
.mmh-totop-ic{ width:20px; height:20px; position:relative; z-index:1; }
@media (prefers-reduced-motion:reduce){
  .mmh-totop{ transition:opacity .2s linear; transform:none; }
}


/* ============================================================
   HOST/ELEMENTOR RESET — neutralise plugin default (pink) colours
   Our bare <button>s must not inherit Elementor's global kit accent.
   ============================================================ */
.mmh-modal-x, .mmh-totop { -webkit-appearance:none; appearance:none; box-shadow:none; }
.mmh-modal .mmh-modal-x { background:transparent !important; color:var(--paper) !important; border:1px solid var(--line-ui) !important; }
.mmh-modal .mmh-modal-x:hover { background:var(--gold) !important; color:var(--ink) !important; border-color:var(--gold) !important; }
.mmh-totop { background:var(--ink-2) !important; color:var(--paper) !important; }
.mmh-totop:hover { background:var(--gold) !important; }

/* Scroll-to-top button now carries the MM logo mark */
.mmh-totop-ic { width:24px; height:24px; object-fit:contain; position:relative; z-index:1; display:block; }

/* ============================================================
   POP-UP — subtle animated topographic line background (adds life)
   ============================================================ */
.mmh-modal-card { isolation:isolate; }
.mmh-modal-card > *:not(.mmh-modal-accent):not(.mmh-modal-x){ position:relative; z-index:1; }
.mmh-modal-card::after {
  content:""; position:absolute; inset:0; z-index:0; pointer-events:none;
  background-image:url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22320%22%20height%3D%22220%22%20viewBox%3D%220%200%20320%20220%22%3E%3Cg%20opacity%3D%220.55%22%3E%3Cpath%20d%3D%22M0%2018.0%20L20%2020.7%20L40%2022.9%20L60%2024.5%20L80%2025.0%20L100%2024.5%20L120%2022.9%20L140%2020.7%20L160%2018.0%20L180%2015.3%20L200%2013.1%20L220%2011.5%20L240%2011.0%20L260%2011.5%20L280%2013.1%20L300%2015.3%20L320%2018.0%22%20fill%3D%22none%22%20stroke%3D%22%2523B3945E%22%20stroke-width%3D%221%22%2F%3E%3Cpath%20d%3D%22M0%2051.6%20L20%2052.9%20L40%2052.8%20L60%2051.4%20L80%2048.9%20L100%2045.6%20L120%2042.1%20L140%2038.9%20L160%2036.4%20L180%2035.1%20L200%2035.2%20L220%2036.6%20L240%2039.1%20L260%2042.4%20L280%2045.9%20L300%2049.1%20L320%2051.6%22%20fill%3D%22none%22%20stroke%3D%22%2523B3945E%22%20stroke-width%3D%221%22%2F%3E%3Cpath%20d%3D%22M0%2080.0%20L20%2077.5%20L40%2073.8%20L60%2069.6%20L80%2065.4%20L100%2061.9%20L120%2059.7%20L140%2059.0%20L160%2060.0%20L180%2062.5%20L200%2066.2%20L220%2070.4%20L240%2074.6%20L260%2078.1%20L280%2080.3%20L300%2081.0%20L320%2080.0%22%20fill%3D%22none%22%20stroke%3D%22%2523B3945E%22%20stroke-width%3D%221%22%2F%3E%3Cpath%20d%3D%22M0%2097.0%20L20%2094.3%20L40%2091.8%20L60%2090.0%20L80%2089.1%20L100%2089.2%20L120%2090.4%20L140%2092.4%20L160%2095.0%20L180%2097.7%20L200%20100.2%20L220%20102.0%20L240%20102.9%20L260%20102.8%20L280%20101.6%20L300%2099.6%20L320%2097.0%22%20fill%3D%22none%22%20stroke%3D%22%2523B3945E%22%20stroke-width%3D%221%22%2F%3E%3Cpath%20d%3D%22M0%20115.2%20L20%20113.5%20L40%20113.0%20L60%20114.0%20L80%20116.1%20L100%20119.2%20L120%20122.7%20L140%20126.0%20L160%20128.8%20L180%20130.5%20L200%20131.0%20L220%20130.0%20L240%20127.9%20L260%20124.8%20L280%20121.3%20L300%20118.0%20L320%20115.2%22%20fill%3D%22none%22%20stroke%3D%22%2523B3945E%22%20stroke-width%3D%221%22%2F%3E%3Cpath%20d%3D%22M0%20137.5%20L20%20139.4%20L40%20142.7%20L60%20146.8%20L80%20151.1%20L100%20154.9%20L120%20157.7%20L140%20158.9%20L160%20158.5%20L180%20156.6%20L200%20153.3%20L220%20149.2%20L240%20144.9%20L260%20141.1%20L280%20138.3%20L300%20137.1%20L320%20137.5%22%20fill%3D%22none%22%20stroke%3D%22%2523B3945E%22%20stroke-width%3D%221%22%2F%3E%3Cpath%20d%3D%22M0%20172.0%20L20%20174.8%20L40%20177.4%20L60%20179.5%20L80%20180.7%20L100%20181.0%20L120%20180.1%20L140%20178.4%20L160%20176.0%20L180%20173.2%20L200%20170.6%20L220%20168.5%20L240%20167.3%20L260%20167.0%20L280%20167.9%20L300%20169.6%20L320%20172.0%22%20fill%3D%22none%22%20stroke%3D%22%2523B3945E%22%20stroke-width%3D%221%22%2F%3E%3Cpath%20d%3D%22M0%20205.9%20L20%20208.1%20L40%20209.0%20L60%20208.5%20L80%20206.8%20L100%20204.0%20L120%20200.6%20L140%20197.1%20L160%20194.1%20L180%20191.9%20L200%20191.0%20L220%20191.5%20L240%20193.2%20L260%20196.0%20L280%20199.4%20L300%20202.9%20L320%20205.9%22%20fill%3D%22none%22%20stroke%3D%22%2523B3945E%22%20stroke-width%3D%221%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E");
  background-size:340px auto; background-repeat:repeat;
  opacity:.22;
  -webkit-mask-image:linear-gradient(165deg, transparent 0%, #000 42%, #000 100%);
          mask-image:linear-gradient(165deg, transparent 0%, #000 42%, #000 100%);
  animation:mmhDrift 26s linear infinite;
}
@keyframes mmhDrift { from { background-position:0 0; } to { background-position:340px -120px; } }
@media (prefers-reduced-motion:reduce){ .mmh-modal-card::after { animation:none; } }

/* ============================================================
   HEADER — Contact Us dropdown (Perth / Melbourne showrooms)
   ============================================================ */
.nav-links{ overflow:visible; }
.nav-has-sub{ position:relative; display:inline-flex; align-items:center; }
.nav-sub-toggle{ display:inline-flex; align-items:center; gap:.4em; cursor:pointer; }
.nav-caret{ width:.46em; height:.46em; border-right:1.6px solid currentColor; border-bottom:1.6px solid currentColor;
  transform:rotate(45deg) translateY(-1px); transition:transform .25s var(--ease); opacity:.75; }
.nav-has-sub:hover .nav-caret, .nav-has-sub:focus-within .nav-caret{ transform:rotate(225deg) translateY(-1px); }
.nav-sub{
  position:absolute; top:calc(100% + 12px); left:50%;
  transform:translateX(-50%) translateY(6px);
  min-width:250px; background:var(--ink-2); border:1px solid var(--line-gold); border-radius:14px;
  box-shadow:0 30px 60px -28px rgba(0,0,0,.86); padding:1rem; z-index:80;
  display:flex; flex-direction:column; gap:.4rem;
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity .22s var(--ease-out), transform .22s var(--ease-out);
}
/* invisible hover bridge so the menu doesn't close in the gap */
.nav-sub::before{ content:""; position:absolute; bottom:100%; left:0; right:0; height:14px; }
.nav-has-sub.open .nav-sub,
.nav-has-sub:hover .nav-sub, .nav-has-sub:focus-within .nav-sub{
  opacity:1; visibility:visible; pointer-events:auto; transform:translateX(-50%) translateY(0);
}
/* V21 — see the note on .nav-mega below. `visibility` is not in the
   transition list above, so the close had no animation at all: the panel
   was hidden on the same frame the pointer left it while opacity was still
   trying to fade. Both dropdowns get the delayed-visibility treatment. */
.nav-sub a{
  display:block; padding:.5rem .6rem; border-radius:10px; white-space:nowrap;
  font-size:.88rem; letter-spacing:0; color:var(--paper); text-transform:none;
  transition:background .18s var(--ease), color .18s var(--ease);
}
.nav-sub a:hover, .nav-sub a:focus-visible{ background:var(--wash-gold); color:var(--gold-glow); }
/* mobile drawer sub-links */
.drawer .d-sublink{ padding-left:2.6rem; font-size:.98rem; opacity:.88; }
.drawer .d-sublink .num{ visibility:hidden; }

/* ============================================================
   V11 — Gallery mega-menu (3-column preview dropdown)
   ============================================================ */
.site-head .nav{ position:relative; }
/* V18.1 fix: the mega-menu now anchors to its own menu item — exactly like the
   Contact dropdown (.nav-sub) — so both dropdowns open with the SAME gap under
   their parent link. It was anchored to the whole nav bar before, which put it
   ~22px lower than the Contact dropdown. */
.nav-has-mega{ position:relative; }
.nav-mega{
  position:absolute; top:calc(100% + 12px); left:50%;
  width:min(680px,calc(100vw - 2rem));
  background:var(--ink-2); border:1px solid var(--line-gold);
  border-radius:14px; box-shadow:0 30px 60px -28px rgba(0,0,0,.86);
  padding:1rem; z-index:80; transform:translateX(-50%) translateY(6px);
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity .22s var(--ease-out), transform .22s var(--ease-out);
}
.nav-mega::before{ content:""; position:absolute; bottom:100%; left:0; right:0; height:14px; }
.nav-has-mega.open .nav-mega,
.nav-has-mega:hover .nav-mega, .nav-has-mega:focus-within .nav-mega{
  opacity:1; visibility:visible; pointer-events:auto; transform:translateX(-50%) translateY(0);
}
/* Never let the centred panel escape the viewport on narrower desktops. */
@media (max-width:1500px){
  .nav-mega{ left:0; transform:translateX(0) translateY(6px); }
  .nav-has-mega.open .nav-mega,
  .nav-has-mega:hover .nav-mega, .nav-has-mega:focus-within .nav-mega{ transform:translateX(0) translateY(0); }
}
.nav-mega-grid{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.4rem; }
.nav-mega-card{ display:flex; gap:.7rem; align-items:center; min-width:0; padding:.5rem; border-radius:10px;
  transition:background .18s var(--ease); }
.nav-mega-card:hover{ background:var(--wash-gold); }
.nav-mega-thumb{ width:72px; height:50px; flex:0 0 auto; border-radius:7px; overflow:hidden;
  background:var(--ink-3); border:1px solid var(--line); }
.nav-mega-thumb img{ width:100%; height:100%; object-fit:cover; display:block; }
.nav-mega-text{ display:flex; flex-direction:column; min-width:0; }
.nav-mega-name{ font-size:.88rem; font-weight:600; color:var(--paper); line-height:1.15; }
.nav-mega-specs{ font-family:var(--mono); font-size:.58rem; letter-spacing:.03em; color:var(--muted-2);
  margin-top:.22rem; white-space:normal; line-height:1.4; }
.nav-mega-foot{ display:inline-flex; align-items:center; gap:.5em; margin-top:.6rem;
  padding:.7rem .5rem .2rem; border-top:1px solid var(--line); width:100%;
  font-size:.82rem; font-weight:600; color:var(--gold-deep); }
.nav-mega-foot .arrow{ transition:transform .3s var(--ease); }
.nav-mega-foot:hover .arrow{ transform:translateX(4px); }
@media (max-width:1180px){ .nav-mega{ width:min(600px,calc(100vw - 2rem)); } }

/* ============================================================
   V11 — 404 page
   ============================================================ */
.err404{ padding-block:clamp(3rem,7vw,6rem); }
.err404-code{ font-family:var(--display); font-size:clamp(5rem,16vw,11rem); line-height:.9;
  color:var(--gold); opacity:.22; font-weight:340; margin:0 0 .4rem; letter-spacing:-.02em; }
.err404-links{ margin-top:2.6rem; }
.err404-links-label{ display:block; font-family:var(--mono); font-size:.62rem; letter-spacing:.16em;
  text-transform:uppercase; color:var(--muted-2); margin-bottom:.9rem; }
.err404-chips{ display:flex; flex-wrap:wrap; gap:.6rem; justify-content:center; }
.err404-chips a{ padding:.5rem .9rem; border:1px solid var(--line); border-radius:999px;
  font-size:.85rem; color:var(--paper); transition:background .18s var(--ease),color .18s var(--ease),border-color .18s var(--ease); }
.err404-chips a:hover{ background:var(--gold); color:var(--ink); border-color:var(--gold); }

/* V11 — configurator now has 4 module steps; keep them tidy on one row (desktop) */
@media (min-width:721px){
  .evz-panel .steps{ gap:.45rem; }
  .evz-btn{ padding:.85rem .45rem; }
  .evz-btn .t{ font-size:1.02rem; }
  .evz-btn small{ font-size:.55rem; white-space:nowrap; }
}

/* V11.1 — collapse the primary nav to the burger drawer earlier so the bar
   never crams at tablet-landscape / 1024px widths. */
@media (max-width:1200px){
  .nav-links,.nav-cta .btn{ display:none; }
  .burger{ display:flex; }
  /* V49 — regression from V46.2's header change. `.nav` went from
     `justify-content:space-between` to `flex-start` with the right-hand
     push coming from `margin-left:auto` on `.nav-links` (line ~518) —
     which only has an effect while `.nav-links` is actually in the flex
     flow. The instant `.nav-links` goes `display:none`, right here, that
     auto-margin stops existing along with it, and the two remaining
     children — `.brand` and `.nav-cta` (which is all the burger lives
     in, by this width) — both sit packed at flex-start: the burger ends
     up next to the logo instead of on the right. `space-between` didn't
     have this problem because it repositions whatever's actually
     visible, not a specific hidden element. Moving the auto-margin onto
     `.nav-cta` here — the thing that still IS visible once `.nav-links`
     isn't — restores the same right-docked burger the old layout had,
     without touching the V46.2 desktop behaviour at all. */
  .nav-cta{ margin-left:auto; }
}

/* ============================================================
   Custom Online Builder — Canibuild Lead Converter mounts
   (Juvira Update V13). Widgets ship their own internal styles;
   these rules only frame the mount points and reserve space so
   the page does not jump as the embeds load.
   ============================================================ */
.cb-search{ margin-top:1.8rem; max-width:none; width:100%; }
.cb-search #canibuild-leadconverter-search-root{ width:100%; }
/* The Canibuild iframe is injected with height:100%, so the mount needs a
   definite height (not just min-height) for that percentage to resolve. */
.cb-widget{ margin-top:2.4rem; height:780px; }
.cb-widget #lc-widget{ width:100%; height:100%; min-height:780px; }
.cb-widget #lc-widget iframe{ width:100% !important; height:100% !important; border:0; }
/* V30 — the old `@media (max-width:640px){ .cb-widget{height:720px} ... }`
   is REMOVED, not overridden. A 720px panel on a 667px-tall phone is the
   defect the client reported ("far too tall and takes over most of the
   screen"), and leaving it here to be beaten by a later rule is how a fix
   quietly stops working the next time someone reorders the file. The mobile
   sizing now lives in one place: see "4 · THE ONLINE BUILDER ON MOBILE". */

/* ============================================================
   MODULE CONFIGURATOR  (Future-Proof by Design)
   ============================================================ */
/* nested to-scale footprints — one light box per module, click to highlight */
.fp-wrap{ position:absolute; inset:0; }
.fp-cell{ position:absolute; left:9%; bottom:13%; aspect-ratio:3/2; border:1px solid var(--line-gold);
  background:transparent; cursor:pointer;
  transition:border-color .3s var(--ease), background .3s var(--ease), box-shadow .3s var(--ease); }
.fp-cell:hover{ border-color:var(--gold); background:rgba(179,148,94,.06); }
.fp-cell.active{ border:1.6px solid var(--gold); background:rgba(179,148,94,.16);
  box-shadow:inset 0 0 0 4px rgba(251,247,239,.45); z-index:45 !important; }
.fp-cell--max{ border-style:dashed; }
.fp-tag{ position:absolute; top:5px; left:7px; font-family:var(--mono); font-size:.52rem;
  letter-spacing:.05em; color:var(--gold-deep); white-space:nowrap; opacity:0; transition:opacity .3s var(--ease);
  font-variant-numeric:tabular-nums; }
.fp-cell.active .fp-tag{ opacity:1; }
.fp-cell--max > .fp-tag{ left:auto; right:8px; opacity:.7; }

/* selectable module list */
.msz-pick-grid{ display:grid; grid-template-columns:repeat(2,1fr); gap:.55rem; margin:1.7rem 0 1.9rem; }
.msz-pick{ text-align:left; padding:.7rem .85rem; border:1px solid var(--line); background:var(--card);
  cursor:pointer; display:flex; align-items:center; justify-content:space-between; gap:.6rem;
  transition:border-color .25s var(--ease), background .25s var(--ease); }
.msz-pick .pl{ display:flex; flex-direction:column; gap:.15rem; min-width:0; }
.msz-pick .pn{ font-family:var(--display); font-size:.98rem; font-weight:360; line-height:1.08; color:var(--paper); }
.msz-pick .pc{ font-family:var(--mono); font-size:.54rem; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); }
.msz-pick .ps{ font-family:var(--mono); font-size:.66rem; color:var(--gold-deep); white-space:nowrap; }
.msz-pick[aria-pressed="true"]{ border-color:var(--gold); background:var(--wash-gold); }
.msz-pick[aria-pressed="true"] .ps{ color:var(--gold-deep); }
.msz-pick:hover{ border-color:var(--line-gold); }
@media (max-width:560px){ .msz-pick-grid{ grid-template-columns:1fr; } }

/* ============================================================
   How It's Done — video tile + YouTube lightbox
   ============================================================ */
.gal-video-btn{
  display:block; position:absolute; inset:0; width:100%; height:100%;
  padding:0; margin:0; border:0; background:none; cursor:pointer;
}
.gal-video-btn .photo{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block; }
.gal-video-btn::after{ content:""; position:absolute; inset:0; z-index:1;
  background:linear-gradient(180deg, rgba(11,17,22,0) 46%, rgba(11,17,22,.26) 100%);
  transition:opacity .3s var(--ease); }
.gal-video-btn:hover::after{ opacity:.5; }
.gal-play{
  position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  width:74px; height:74px; border-radius:50%; z-index:2;
  display:grid; place-items:center;
  background:rgba(244,241,234,.94); color:var(--ink);
  box-shadow:0 8px 30px rgba(0,0,0,.55);
  transition:transform .3s var(--ease), background .3s var(--ease);
}
.gal-video-btn:hover .gal-play{ transform:translate(-50%,-50%) scale(1.08); background:var(--gold); }
.gal-play svg{ position:static; inset:auto; width:30px; height:30px; margin-left:4px; }
.gal-video-btn:focus-visible{ outline:2px solid var(--gold-hi); outline-offset:-4px; }

.mmh-ytlb{ position:fixed; inset:0; z-index:1300; display:grid; place-items:center; padding:clamp(16px,4vw,48px); }
.mmh-ytlb[hidden]{ display:none; }
.mmh-ytlb-backdrop{ position:absolute; inset:0; background:rgba(11,9,6,.82); backdrop-filter:blur(4px); opacity:0; transition:opacity .3s var(--ease-out); }
.mmh-ytlb.is-open .mmh-ytlb-backdrop{ opacity:1; }
.mmh-ytlb-dialog{
  position:relative; z-index:2; width:min(1100px,100%);
  opacity:0; transform:translateY(12px) scale(.985);
  transition:opacity .35s var(--ease-out), transform .35s var(--ease-out);
}
.mmh-ytlb.is-open .mmh-ytlb-dialog{ opacity:1; transform:none; }
.mmh-ytlb-frame{ position:relative; width:100%; aspect-ratio:16/9; background:#000; box-shadow:0 24px 80px rgba(0,0,0,.6); }
.mmh-ytlb-frame iframe{ position:absolute; inset:0; width:100%; height:100%; border:0; }
.mmh-ytlb-x{
  position:absolute; top:-46px; right:0; z-index:3;
  width:40px; height:40px; border:0; border-radius:50%; cursor:pointer;
  background:rgba(244,241,234,.16); color:#fff; font-size:1.7rem; line-height:1;
  display:grid; place-items:center; transition:background .25s var(--ease);
}
.mmh-ytlb-x:hover{ background:rgba(244,241,234,.3); }
@media (max-width:560px){ .mmh-ytlb-x{ top:-42px; } .gal-play{ width:60px; height:60px; } .gal-play svg{ width:24px; height:24px; } }
@media (prefers-reduced-motion:reduce){
  .mmh-ytlb-backdrop,.mmh-ytlb-dialog{ transition:none; }
}

/* ============================================================
   The Build Journey — animated vertical timeline
   ============================================================ */
.proc-journey{ position:relative; margin-top:1rem; }
.proc-spine{ position:absolute; top:26px; bottom:26px; left:23px; width:2px;
  background:rgba(217,192,138,.16); z-index:0; }
.proc-spine-fill{ position:absolute; left:0; top:0; width:100%; height:0;
  background:linear-gradient(180deg, var(--gold-glow), var(--gold));
  transition:height .8s var(--ease-out); }
.proc-nodes{ list-style:none; margin:0; padding:0; position:relative; z-index:1;
  display:grid; gap:clamp(.4rem,1.4vw,1rem); }
.proc-node{ display:grid; grid-template-columns:48px 1fr; gap:clamp(1rem,2.4vw,1.7rem);
  align-items:start; padding-block:clamp(.5rem,1.3vw,.9rem); cursor:pointer;
  outline:none; }
.proc-badge{ position:relative; z-index:1; width:48px; height:48px; border-radius:50%;
  display:grid; place-items:center; background:var(--ink-3);
  border:2px solid rgba(217,192,138,.26); color:var(--gold-glow);
  transition:background .5s var(--ease), border-color .5s var(--ease),
    color .5s var(--ease), transform .5s var(--ease), box-shadow .5s var(--ease); }
.proc-badge svg{ width:23px; height:23px; }
.proc-card{ padding-top:.2rem; opacity:.5; transform:translateY(3px);
  transition:opacity .5s var(--ease), transform .5s var(--ease); }
.proc-kicker{ display:block; font-family:var(--mono); font-size:.62rem; letter-spacing:.18em;
  text-transform:uppercase; color:var(--gold-glow); opacity:.8; }
.proc-card h4{ color:var(--paper); font-size:clamp(1.08rem,1.7vw,1.36rem); margin:.3rem 0 .4rem; }
.proc-card p{ color:var(--muted-dk); font-size:.95rem; line-height:1.6; max-width:54ch; margin:0; }

/* States driven by JS */
.proc-node.is-done .proc-badge{ border-color:var(--gold); color:var(--gold-glow); }
.proc-node.is-done .proc-card{ opacity:1; transform:none; }
.proc-node.is-active .proc-badge{ background:var(--gold); border-color:var(--gold);
  color:var(--ink); transform:scale(1.12); box-shadow:0 0 0 6px rgba(179,148,94,.16); }
.proc-node.is-active .proc-card{ opacity:1; transform:none; }
.proc-node:focus-visible .proc-badge{ box-shadow:0 0 0 4px rgba(217,192,138,.35); }

/* Before the animation starts / no-JS: everything readable */
.no-js .proc-card, .proc-journey.is-static .proc-card{ opacity:1; transform:none; }
.no-js .proc-badge, .proc-journey.is-static .proc-badge{ border-color:var(--gold); }

@media (max-width:560px){
  .proc-node{ grid-template-columns:42px 1fr; gap:1rem; }
  .proc-badge{ width:42px; height:42px; } .proc-badge svg{ width:20px; height:20px; }
  .proc-spine{ left:20px; }
}
@media (prefers-reduced-motion:reduce){
  .proc-spine-fill,.proc-badge,.proc-card{ transition:none; }
}

/* ============================================================
   V13 — Module range & configuration block (all model pages)
   ============================================================ */
.config-grid{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1.4rem; align-items:start; }
.config-fig{ margin:0; background:var(--card); border:1px solid var(--line-soft); border-radius:14px; overflow:hidden; }
.config-fig img{ width:100%; height:auto; display:block; }
.config-fig figcaption{ font-family:var(--mono); font-size:.66rem; letter-spacing:.14em; text-transform:uppercase;
  color:var(--muted-2); padding:.75rem 1rem 1rem; }
.config-legend{ list-style:none; margin:1.9rem 0 0; padding:0; display:grid;
  grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:0 1.6rem; }
.config-legend li{ display:flex; justify-content:space-between; gap:1rem; padding:.6rem 0;
  border-bottom:1px solid var(--line-soft); font-size:.9rem; }
.config-legend li b{ font-weight:600; }
.config-legend li span{ font-family:var(--mono); font-size:.76rem; color:var(--gold-deep); white-space:nowrap; }
@media (max-width:760px){ .config-grid{ grid-template-columns:1fr; } }

/* ============================================================
   V13 — Townhouse gallery + detail
   ============================================================ */
/* Gallery cards reuse .model-grid / .model-card. Extras: */
.th-weeks-chip{ position:absolute; top:12px; left:12px; z-index:2;
  font-family:var(--mono); font-size:.6rem; font-weight:700; letter-spacing:.14em;
  background:var(--gold); color:var(--ink); padding:.34em .7em; border-radius:999px; }
.th-soon-chip{ position:absolute; bottom:12px; left:12px; z-index:2;
  font-family:var(--mono); font-size:.6rem; letter-spacing:.14em; text-transform:uppercase;
  background:rgba(11,17,22,.82); color:#fff; padding:.34em .7em; border-radius:999px; }
.th-card--soon{ cursor:default; opacity:.94; }
.th-card--soon .model-media{ background:var(--paper-3); display:flex; align-items:center; justify-content:center; }
.th-soon-art{ width:64px; height:64px; color:var(--gold-deep); opacity:.5; }
.th-soon-art svg{ width:100%; height:100%; }

/* Hero weeks badge */
.th-weeks-badge{ display:inline-flex; align-items:center; gap:.55em; margin:1.2rem 0 0;
  font-family:var(--mono); font-size:.72rem; letter-spacing:.1em; text-transform:uppercase; color:var(--paper); }
.th-weeks-badge span{ font-size:.82rem; font-weight:700; letter-spacing:.12em;
  background:var(--gold); color:var(--ink); padding:.4em .75em; border-radius:999px; }

/* Total area + timeline highlight band */
.th-highlight{ display:grid; grid-template-columns:1.4fr 1fr; gap:1.2rem;
  background:var(--ink-2); color:var(--paper); border-radius:18px; overflow:hidden;
  border:1px solid var(--line-gold); }
.th-hl-area,.th-hl-weeks{ padding:2.2rem clamp(1.4rem,3vw,2.6rem); }
.th-hl-weeks{ background:var(--wash-gold); display:flex; flex-direction:column; justify-content:center;
  border-left:1px solid var(--line-gold); }
.th-hl-label,.th-hl-weeks-sub,.th-hl-sub{ font-family:var(--mono); font-size:.66rem; letter-spacing:.16em;
  text-transform:uppercase; opacity:.8; display:block; }
.th-hl-value{ font-family:var(--display); font-size:clamp(2.6rem,6vw,4.2rem); font-weight:340; line-height:1; margin:.4rem 0 .5rem; display:block; }
.th-hl-weeks-num{ font-family:var(--display); font-size:clamp(2rem,4.5vw,3rem); font-weight:340; line-height:1; margin:0 0 .4rem; display:block; text-transform:uppercase; }
@media (max-width:720px){ .th-highlight{ grid-template-columns:1fr; } }

/* Floor-area list */
.th-areas{ list-style:none; margin:1.6rem 0 0; padding:0; }
.th-areas li{ display:flex; justify-content:space-between; gap:1rem; padding:.7rem 0;
  border-bottom:1px solid var(--line-soft); font-size:.98rem; }
.th-areas li span{ color:var(--muted-dk,#6b6350); }
.th-areas li b{ font-weight:600; }
.th-areas-total{ border-bottom:0; margin-top:.3rem; padding-top:1rem; border-top:2px solid var(--gold); }
.th-areas-total span,.th-areas-total b{ font-size:1.1rem; color:var(--paper); font-weight:600; }

/* Specifications list */
.th-specs{ list-style:none; margin:0; padding:0; display:grid;
  grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:.2rem 2rem; }
.th-specs li{ position:relative; padding:.7rem 0 .7rem 1.6rem; border-bottom:1px solid var(--line-soft); font-size:.96rem; }
.th-specs li::before{ content:""; position:absolute; left:0; top:1.15rem; width:8px; height:8px;
  border-right:2px solid var(--gold-deep); border-bottom:2px solid var(--gold-deep); transform:rotate(45deg); }
.th-specs--dark li{ border-bottom-color:rgba(255,255,255,.14); color:var(--paper); }
.th-specs--dark li::before{ border-color:var(--gold-glow); }

/* ============================================================
   V13.1 — Blog (Articles & Resources)
   ============================================================ */
.post-grid{ display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1.6rem; }
@media (max-width:960px){ .post-grid{ grid-template-columns:repeat(2,minmax(0,1fr)); } }
@media (max-width:600px){ .post-grid{ grid-template-columns:1fr; } }

.post-card{ background:var(--card); border:1px solid var(--line-soft); border-radius:16px; overflow:hidden;
  transition:transform .4s var(--ease), box-shadow .4s var(--ease), border-color .4s var(--ease); }
.post-card:hover{ transform:translateY(-4px); box-shadow:0 18px 40px -24px rgba(0,0,0,.8); border-color:var(--line-gold); }
.post-card-link{ display:flex; flex-direction:column; height:100%; color:inherit; }
.post-media{ position:relative; aspect-ratio:3/2; background:var(--paper-3); overflow:hidden; }
.post-media .photo{ width:100%; height:100%; object-fit:cover; display:block;
  transition:transform 1.1s var(--ease); }
.post-card:hover .post-media .photo{ transform:scale(1.05); }
.post-media-fallback{ position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  color:var(--gold-deep); opacity:.45; }
.post-media-fallback svg{ width:56px; height:56px; }
.post-cat{ position:absolute; top:12px; left:12px; font-family:var(--mono); font-size:.6rem; font-weight:700;
  letter-spacing:.14em; text-transform:uppercase; background:rgba(11,17,22,.84); color:#fff;
  padding:.36em .7em; border-radius:999px; }
.post-body{ display:flex; flex-direction:column; gap:.55rem; padding:1.3rem 1.4rem 1.5rem; flex:1; }
.post-date{ font-family:var(--mono); font-size:.64rem; letter-spacing:.16em; text-transform:uppercase; color:var(--gold-deep); }
.post-title{ font-family:var(--display); font-weight:400; font-size:1.32rem; line-height:1.2; margin:0; }
.post-excerpt{ font-size:.92rem; color:var(--muted-dk,#6b6350); margin:0; }
.post-body .tlink{ margin-top:auto; padding-top:.4rem; }

.post-pagination{ margin-top:2.6rem; display:flex; justify-content:center; }
.post-pagination .nav-links{ display:flex; flex-wrap:wrap; gap:.5rem; align-items:center; }
.post-pagination .page-numbers{ display:inline-flex; align-items:center; justify-content:center; min-width:42px; height:42px;
  padding:0 .7rem; border:1px solid var(--line-soft); border-radius:999px; font-family:var(--mono); font-size:.8rem;
  color:var(--paper); transition:background .3s var(--ease), color .3s var(--ease), border-color .3s var(--ease); }
.post-pagination .page-numbers:hover{ border-color:var(--gold); }
.post-pagination .page-numbers.current{ background:var(--gold); color:var(--ink); border-color:var(--gold); }

/* Single post */
.page-hero-band--post{ min-height:44vh; }
.post-meta{ font-family:var(--mono); font-size:.72rem; letter-spacing:.12em; text-transform:uppercase; color:var(--muted-2); }
.post-content{ max-width:760px; margin-inline:auto; font-size:1.06rem; line-height:1.75; color:var(--paper); }
.post-content > *{ margin:0 0 1.3rem; }
.post-content h2{ font-family:var(--display); font-weight:400; font-size:1.9rem; line-height:1.2; margin:2.4rem 0 1rem; }
.post-content h3{ font-family:var(--display); font-weight:400; font-size:1.45rem; margin:2rem 0 .8rem; }
.post-content a{ color:var(--gold-deep); text-decoration:underline; text-underline-offset:3px; }
.post-content img,.post-content figure{ max-width:100%; height:auto; border-radius:12px; }
.post-content ul,.post-content ol{ padding-left:1.3rem; }
.post-content li{ margin:.4rem 0; }
.post-content blockquote{ border-left:3px solid var(--gold); padding:.4rem 0 .4rem 1.4rem; margin:1.8rem 0;
  font-family:var(--display); font-size:1.3rem; font-style:italic; color:var(--muted-dk,#5c5443); }
.post-foot{ max-width:760px; margin:2.6rem auto 0; padding-top:1.6rem; border-top:1px solid var(--line-soft); }

/* ============================================================
   V17 (#8) — CUSTOM ONLINE BUILDER (homepage, full-width 2-col)
   Left: heading / sub / description / address search widget.
   Right: site-plan image, bleeding to the section edge.
   ============================================================ */
.cob{ padding-block:0; background:var(--paper-2, var(--card)); }
.cob-grid{ display:grid; grid-template-columns:1fr 1fr; align-items:stretch; min-height:clamp(440px,52vw,600px); }
.cob-copy{ display:flex; flex-direction:column; justify-content:center;
  padding:clamp(40px,6vw,104px); padding-right:clamp(28px,4vw,72px); }
.cob-media{ position:relative; overflow:hidden; background:var(--paper-3,#e9e5dc); }
.cob-media img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; object-position:center; }
.cob-search{ margin-top:1.8rem; max-width:560px; }
/* Give the third-party search widget room and a consistent frame. */
.cob-search #canibuild-leadconverter-search-root{ display:block; width:100%; min-height:64px; }
@media (max-width:860px){
  .cob-grid{ grid-template-columns:1fr; }
  .cob-media{ min-height:300px; order:-1; }
}

/* ============================================================
   V17 — TOWNHOUSE: design-theme toggle, build video, builder band
   ============================================================ */
.th-theme-tabs{ display:inline-flex; gap:.4rem; margin-bottom:1.5rem; border:1px solid var(--line); border-radius:5px; padding:.3rem; }
.th-theme-tab{ font:inherit; font-size:.82rem; font-weight:600; padding:.55rem 1.15rem; border:none; background:transparent;
  color:var(--muted-2); cursor:pointer; border-radius:3px; transition:background .3s var(--ease), color .3s var(--ease); }
.th-theme-tab.is-active{ background:var(--gold); color:var(--ink); }
.th-theme-pane[hidden]{ display:none; }

.th-build-video{ position:relative; max-width:980px; margin-inline:auto; border:1px solid rgba(255,255,255,.12); background:#000; }
.th-build-video video{ width:100%; height:auto; display:block; }
.th-build-video .gal-video-btn{ position:relative; inset:auto; width:100%; aspect-ratio:16/9; }
.th-build-video .cap{ text-align:center; color:var(--paper); font-family:var(--mono); font-size:.72rem; letter-spacing:.06em; padding:.85rem; }

.builder-xp .bx-grid{ display:grid; grid-template-columns:1.15fr .85fr; gap:clamp(28px,4vw,64px); align-items:center; }
.builder-xp h2{ color:var(--paper); }
.bx-25{ color:var(--gold); font-style:italic; white-space:nowrap; }
.bx-lead .lead{ color:var(--paper); opacity:.82; }
.bx-points{ display:grid; gap:.85rem; }
.bx-points li{ display:flex; flex-direction:column; gap:.18rem; padding:.9rem 1.1rem;
  border:1px solid rgba(255,255,255,.14); background:rgba(255,255,255,.03); }
.bx-points li b{ font-family:var(--display); font-weight:400; font-size:1.16rem; color:var(--paper); }
.bx-points li span{ font-family:var(--mono); font-size:.62rem; letter-spacing:.12em; text-transform:uppercase; color:var(--gold-glow); }
@media (max-width:860px){ .builder-xp .bx-grid{ grid-template-columns:1fr; } }

/* ============================================================
   V18 — ANIMATED CONFIGURATION SHEET (per-module tracing)
   Replaces the old plain module legend. Shared on every model page.
   ============================================================ */
.mtc{ margin-top:2.6rem; }
.mtc-tabs{ display:flex; flex-wrap:wrap; gap:.5rem; margin-bottom:1.2rem; }
.mtc-tab{ font:inherit; font-size:.8rem; font-weight:600; padding:.5rem .95rem; border:1px solid var(--line-soft);
  background:var(--card); color:var(--muted); border-radius:6px; cursor:pointer;
  transition:background .25s var(--ease), color .25s var(--ease), border-color .25s var(--ease); }
.mtc-tab:hover{ border-color:var(--line-gold); }
.mtc-tab.is-active{ background:var(--gold); color:var(--ink); border-color:var(--gold); }
.mtc-stage{ background:var(--card); border:1px solid var(--line-soft); border-radius:14px; padding:22px; }
.mtc-stagehead{ display:flex; align-items:baseline; justify-content:space-between; gap:1rem; margin-bottom:.4rem; flex-wrap:wrap; }
.mtc-name{ font-family:var(--display); font-size:1.2rem; font-weight:400; }
.mtc-gfa{ font-family:var(--mono); font-size:.8rem; color:var(--ink); background:var(--gold); padding:.28rem .65rem;
  border-radius:5px; opacity:0; transform:translateY(6px); transition:opacity .5s .9s, transform .5s .9s; white-space:nowrap; }
.mtc-stage.play .mtc-gfa{ opacity:1; transform:none; }
.mtc svg{ width:100%; height:auto; display:block; }
.mtc-draw{ stroke:var(--paper); fill:none; stroke-width:6; stroke-linejoin:round; stroke-linecap:round;
  stroke-dasharray:var(--len); stroke-dashoffset:var(--len); }
.mtc-stage.play .mtc-draw{ animation:mtcTrace 1.1s ease forwards; }
.mtc-part{ stroke:var(--paper); stroke-width:3; stroke-dasharray:var(--len); stroke-dashoffset:var(--len); opacity:.7; }
.mtc-stage.play .mtc-part{ animation:mtcTrace .7s ease forwards; animation-delay:.9s; }
@keyframes mtcTrace{ to{ stroke-dashoffset:0; } }
.mtc-rlabel{ opacity:0; }
.mtc-stage.play .mtc-rlabel{ animation:mtcFade .5s ease forwards; }
@keyframes mtcFade{ to{ opacity:1; } }
.mtc-replay{ margin-top:1rem; font:inherit; font-size:.78rem; font-weight:600; color:var(--gold-deep);
  background:none; border:none; cursor:pointer; padding:0; }
.mtc-replay:hover{ color:var(--gold); }
.mtc-furn{ stroke:var(--paper); stroke-width:1.6; fill:none; opacity:.5; stroke-linecap:round; stroke-linejoin:round;
  stroke-dasharray:var(--len); stroke-dashoffset:var(--len); }
.mtc-stage.play .mtc-furn{ animation:mtcTrace .55s ease forwards; }

@media (prefers-reduced-motion:reduce){
  .mtc-draw,.mtc-part,.mtc-furn{ stroke-dashoffset:0 !important; animation:none !important; }
  .mtc-rlabel,.mtc-gfa{ opacity:1 !important; transform:none !important; animation:none !important; }
}

/* ============================================================
   V18 — FEATURES & SPECIFICATIONS (redesigned card grid)
   ============================================================ */
.fx-keys{ display:grid; grid-template-columns:repeat(5,1fr); gap:14px; margin-bottom:22px; }
/* Five of these sit in a row on every model page. In green they were five
   sixths of that page's green on their own. */
.fx-key{ background:var(--ink-2); color:var(--paper); border-radius:12px; padding:15px 16px; position:relative; overflow:hidden;
  border:1px solid var(--line-gold); }
.fx-key .v{ display:block; font-family:var(--display); font-size:1.5rem; font-weight:400; line-height:1; }
.fx-key .v small{ font-size:.6rem; margin-left:2px; }
.fx-key .l{ display:block; font-family:var(--mono); font-size:.58rem; letter-spacing:.12em; text-transform:uppercase; color:var(--gold-glow); margin-top:8px; }
.fx-key::after{ content:""; position:absolute; right:-14px; bottom:-14px; width:54px; height:54px; border-radius:50%; border:2px solid rgba(196,150,74,.3); }

.fx-grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:16px; }
.fx-card{ background:var(--card); border:1px solid var(--line-soft); border-radius:14px; padding:22px;
  transition:box-shadow .3s var(--ease), transform .3s var(--ease), border-color .3s var(--ease); }
.fx-card:hover{ box-shadow:0 14px 34px rgba(0,0,0,.55); transform:translateY(-3px); border-color:var(--line-gold); }
.fx-top{ display:flex; align-items:center; gap:12px; margin-bottom:14px; padding-bottom:14px; border-bottom:1px solid var(--line-soft); }
.fx-ic{ width:42px; height:42px; flex:none; border-radius:10px; background:var(--wash-gold); color:var(--gold-glow);
  display:flex; align-items:center; justify-content:center; }
.fx-ic svg{ width:22px; height:22px; }
.fx-head{ display:flex; flex-direction:column; gap:3px; }
.fx-ttl{ font-weight:600; font-size:1.06rem; line-height:1.1; }
.fx-sub{ font-family:var(--mono); font-size:.58rem; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); }
.fx-list{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:9px; }
.fx-list li{ display:flex; gap:9px; align-items:flex-start; font-size:.92rem; color:var(--ink-soft); line-height:1.45; }
.fx-list li::before{ content:""; flex:none; width:15px; height:15px; margin-top:3px; background:var(--gold);
  clip-path:polygon(41% 66%,78% 26%,88% 36%,41% 86%,14% 56%,24% 46%); }
.fx-card--dark{ background:var(--ink-3); border-color:var(--line-gold); }
.fx-card--dark .fx-top{ border-color:rgba(255,255,255,.15); }
.fx-card--dark .fx-ttl{ color:var(--paper); }
.fx-card--dark .fx-sub{ color:rgba(217,192,138,.9); }
.fx-ic--on{ background:rgba(196,150,74,.25); color:var(--gold-glow,#e6c98f); }
.fx-note{ margin:0; font-size:.92rem; color:rgba(244,241,234,.9); line-height:1.55; }
@media (max-width:900px){ .fx-keys{ grid-template-columns:repeat(2,1fr);} .fx-grid{ grid-template-columns:1fr 1fr;} }
@media (max-width:600px){ .fx-grid{ grid-template-columns:1fr;} }


/* V18 — magnifier loupe on the birds-eye view & configuration sheet */
.config-fig[data-mag]{ cursor:crosshair; }
/* V120 — bigger loupe, per "the magnifying function is not working well".
   280 -> 320. The lens is also the only feedback that the zoom is working at
   all, so a small one reads as a smudge rather than as an instrument. */
.mag-lens{ position:fixed; pointer-events:none; width:320px; height:320px; border-radius:50%;
  border:3px solid #fff; box-shadow:0 10px 30px rgba(0,0,0,.4), 0 0 0 1px rgba(0,0,0,.1);
  background-repeat:no-repeat; z-index:9999; will-change:left,top,background-position; }
@media (max-width:760px){ .mag-lens{ width:200px; height:200px; } }

/* ============================================================
   MODULAR UPDATE V18.1 §4 — HOMEPAGE
   ============================================================ */

/* --- §4e-overlap: The Company overlaps the section before it (V54) ---
   Client — "For The Company section on homepage, when it shows, can it be
   like overlap on the section before it? Check the Our Process section on
   Dvele and the section after it." Two things were tried here, in order,
   and it is worth recording both because the first one is the more literal
   match to Dvele and was built first, then measured and dropped:

   1. Dvele's own mechanism, measured directly off their site: "Our Process"
      sits in a `position:sticky` panel that pins in place while the next
      section — a plain, later-in-DOM block — scrolls up and visually climbs
      over it. Built the same way here: Featured Models made sticky, an extra
      spacer sibling giving it room to hold. It technically worked (proved in
      isolation and confirmed live: the pin held, `top` stayed at 0 through
      the scroll range). But Dvele's sticky panel is deliberately sized to
      one viewport (`is-sticky-100-vh`) — when it pins, the WHOLE thing is
      already on screen, nothing left to scroll past. Featured Models is a
      six-card grid taller than one viewport; pinning it would freeze it in
      place partway through — the top of the grid stuck at the top of the
      screen with the lower cards and the "See our range" button never
      brought into view by scrolling, because the pin releases before the
      user has scrolled far enough to reach them. A literal copy of Dvele's
      technique on a section shaped differently from Dvele's breaks the thing
      it's copying. Making the fit work would mean either freezing viewport
      height against this specific grid's height — the same kind of
      unmeasured, content-shape-dependent threshold rule #45 already cost
      three rounds to get right on this exact page for a *different*
      section — or cutting the grid down to fit one screen, which nobody
      asked for.
   2. What's live instead: The Company section is pulled up over the bottom
      of Featured Models with a negative margin, raised above it with
      z-index, and given a rounded top edge and a lifted shadow so it reads
      as a panel arriving on top rather than a section that merely comes
      next. No sticky, no extra scroll distance, nothing that depends on one
      section's height fitting inside another's. It cannot get into a
      half-stuck state because there is no state — it is a fixed layout
      relationship, correct at every scroll position and every viewport
      height. This is why V32-V46 is worth naming here at all: that scene
      failed by depending on exact heights and exact browsers being right at
      runtime, and got fully removed rather than perfected. This has nothing
      to get wrong at runtime to begin with.
   Desktop/tablet only (901px+): full-bleed sections butted edge-to-edge on
   a narrow phone screen have less headroom for an overlap to read as
   deliberate rather than cramped, and this treatment adds nothing that a
   phone visitor loses by not having it.

   V55 — client: "The Company section is not working properly and now
   there's no space between the two sections." The margin/radius/shadow
   were live and measured correct (-56px, 32px radius, shadow present —
   confirmed by script, not assumed). The defect: a DARK shadow reads as
   nothing against two sections that are ALREADY dark. Fixed with a thin
   light rim at the boundary plus a stronger shadow. Shipped, screenshotted,
   confirmed visible in the screenshot.

   V56 — client, again: "the Company section is still not working." The
   V55 rim was real and did read in a screenshot taken specifically to
   check it — but a 1px 16%-white line is exactly the kind of change a
   visitor scrolling normally, not pixel-peeping a before/after crop, will
   never consciously register. Screenshot-proof that an edge is
   TECHNICALLY visible is not the same bar as a client seeing it as "this
   is now fixed" at a glance, and after two rounds on the same complaint
   this needed to stop being subtle and start being unmistakable.
   Three changes together:
   1 · **The rim is now the brand gold** (`--gold`, `#B3945E`), 3px instead
       of 1px, at 85% opacity — a colour, not a shade of white-on-dark, so
       it does not depend on being lighter than whatever photo happens to
       be directly behind it. This is the actual fix; the other two below
       make the shape it sits on bigger.
   2 · **The overlap itself is deeper: -56px → -120px.** The old -56px was
       capped at `--sp`'s measured floor (56px) specifically so it could
       never reach past Featured Models' own bottom PADDING into its real
       content — correct, but it also meant the visible "lift" was at most
       56px, which reads as barely there against sections this tall.
       Rather than raise that risk by increasing the negative margin past
       what the shared padding token safely allows, Featured Models is
       given DEDICATED extra bottom padding below, independent of `--sp`
       and unrelated to any other section's rhythm, sized specifically so
       -120px has a confirmed 16-20px of clearance above the actual grid
       content at every viewport height in this range — not tuned to the
       edge of safe again.
   3 · **The radius is bigger** (32px → 44px) so the "card" reads as a
       deliberate shape at a glance, not a barely-rounded rectangle. */
/* V57 — the client named the reference outright: Dvele's "Our Process"
   section and the one after it. Read dvele.com's own stylesheet instead of
   guessing. Their two classes, verbatim:

     .is-sticky-100-vh { min-height:100vh; position:sticky; top:0 }
     .is-minus-100     { margin-top:-100vh; z-index:3 }

   plus a `._125-vh-pad { height:125vh }` empty spacer sibling inside the
   sticky section. So: the section BEFORE holds still while the section AFTER
   climbs a full screen up and over it. V54-V56 all shipped a static negative
   margin instead — a lip on a card, at 56px then 120px. That is a different
   effect, and three rounds of making the lip bigger were never going to
   become this one. This is the mechanism, not a bigger version of the
   substitute.

   WHY V54 WITHDREW THE LITERAL VERSION, AND WHAT CHANGED
   Dvele's held section is a heading and one paragraph: it fits inside one
   screen, so `top:0` can pin it with nothing lost. Featured Models is a
   six-card grid, taller than one screen. Pin a too-tall element with `top:0`
   and the browser holds its TOP against the top of the screen — everything
   below the fold stays cropped for the whole hold, and here the rising panel
   then covered the lower cards and the CTA before they were ever visible.
   Real defect, correctly withdrawn (rule #55).

   The fix is one property, and it costs nothing: `bottom:0` instead of
   `top:0`. A sticky element TALLER than the viewport with `bottom:0` does not
   pin on arrival — it scrolls normally until its BOTTOM edge reaches the
   bottom of the screen, and holds from there. So every card and the CTA
   scroll past at full size first, and what is held is the END of the section,
   which is precisely the edge the Company panel is meant to climb over.
   Same effect as the reference; nothing cropped; no JS; no scene.

   THE GEOMETRY, with h = viewport height, D = dwell, M = the negative margin.
   The pin engages at scrollY = B - h, where B is the section's bottom edge.
   Company's top sits at B + D - M, so measured from the top of the screen
   after scrolling a further Δ it is at (D - M + h - Δ). Therefore:
     · Company starts rising into view at  Δ = D - M
     · Company covers the screen fully at  Δ = D - M + h
     · the pin lets go at                  Δ = D
   Coverage must finish at or before release, i.e. M >= h, or the held section
   visibly snaps out from under a panel that has not finished covering it.
   D = 120vh, M = 110vh gives: a 10vh beat on the held section, a full screen
   of travel while the panel climbs, coverage complete at 110vh, release at
   120vh — 10vh of margin for error. Dvele runs D=125vh / M=100vh, which
   completes coverage exactly ON release; this is the same shape with the
   slack put back in. Both numbers are in `vh`, deliberately: the ratio
   between them is the whole mechanism, so they cannot be allowed to drift
   apart into different units.

   WHY THIS NEEDS ONE MEASURED NUMBER FROM JS, AND ONLY ONE
   `bottom:0` is the obvious way to say "hold this element by its bottom
   edge", and it does not work: a sticky box TALLER than the scrollport
   cannot satisfy a `bottom` inset, so the browser never holds it at all.
   Confirmed in a bare repro outside this theme (qa/repro/sticky.html) before
   building anything on it — it scrolled straight through, no hold, exactly
   as it did inside the theme. The construction that DOES work is a NEGATIVE
   `top`: `top: (viewportHeight - elementHeight)` puts the hold line above
   the top of the screen by precisely the amount the element overhangs it, so
   the element scrolls normally until its bottom edge reaches the bottom of
   the screen and holds from there — verified in the same repro
   (qa/repro/sticky2.html): scrolls, holds for the full dwell, releases only
   after the panel has finished covering it.

   That number is the section's own rendered height, which CSS cannot read —
   it changes with viewport width, with the card grid's reflow, and again
   when the card images finish loading. So `mmh.js` measures it, writes it to
   `--mmh-pin-top`, and adds `html.mmh-pin`. Everything below is gated on
   that class, which means the failure mode is not a broken pin: with no JS,
   an old browser, reduced motion, or a viewport under 901px, none of this
   applies and the section keeps the plain static V56 lip it has today. A
   pin that cannot be measured is never attempted (rule #41 — gate on the
   thing itself, not on a proxy for it).

   The gold rim and the 44px radius from V55/V56 stay exactly as they are —
   on a panel that now genuinely climbs, they read as its leading edge, which
   is the job they were always meant to do. */
.models-dwell{ height:0; }

@media (min-width:901px){
  /* Baseline = what shipped in V56. This is what anyone without the measured
     pin still gets, unchanged, so nothing regresses if the pin never arms. */
  #models{ padding-bottom:calc(var(--sp) + 84px); }
  .company--bg{
    position:relative; z-index:2;
    margin-top:-120px;
    border-radius:44px 44px 0 0;
    box-shadow:
      inset 0 3px 0 rgba(179,148,94,.85),
      /* V60 — client: "the button should be visible when it shows." Measured
         the CTA's contrast right through the climb: 15:1 while the panel is
         still away, then 3.85:1 in the last ~100px before it covers — because
         this shadow was 48px of offset plus 100px of blur at 65% black, a
         dark halo reaching ~148px ABOVE the panel's edge and landing straight
         on the button. It was cranked that hard in V56 to make a STATIC lip
         visible; the panel genuinely climbs now, so the movement sells the
         edge and the gold rim above marks it. Reach cut to ~58px at a lower
         opacity, which keeps the depth without dimming what it is about to
         pass over. */
      0 -18px 40px rgba(0,0,0,.38);
  }

  /* Armed only once mmh.js has measured the section and set --mmh-pin-top. */
  /* NOTE: the padding is deliberately NOT changed here. The hold line is
     computed from this section's measured height, so anything that changes
     that height as a side effect of arming makes the measurement stale and
     holds the section short of the screen edge by exactly the difference —
     which is what the first cut of this did (84px short, caught by asserting
     the held edge against the viewport, not by eye). Arming must change
     position only. */
  html.mmh-pin #models{
    position:sticky;
    top:var(--mmh-pin-top);   /* negative; see the note above */
    z-index:1;
  }
  /* V59 — back to the reference's own numbers, D=125vh / M=100vh, and this
     matters more than it looks. The geometry note above gives coverage at
     Δ = D - M + h and release at Δ = D. V57 chose D=120 / M=110 to leave 10vh
     of "slack" so coverage finished BEFORE the pin let go. That slack is
     precisely the defect: for those 10vh the Company panel has finished
     travelling and moved on, while Featured Models is still held — so the
     held section reappears underneath whatever section follows Company, and
     its cards and CTA show through. Setting M = h (100vh) makes coverage and
     release land on the SAME scroll position, which is why Dvele runs
     125/100: there is no window in which the pin outlives its cover.
     Do not reintroduce the slack. */
  /* V60 — client: "The overlap effect here feels weird. I like the previous
     one." The difference she is feeling is the DEAD BEAT at the start of the
     hold. The panel begins climbing at Δ = D - M, so V57's 120/110 started it
     after 10vh, while V59's 125/100 made her wait 25vh with the section held
     and nothing moving. D is now 110vh against M=100vh: the climb starts
     after 10vh again — the timing she liked — while M stays at one viewport,
     which is what keeps coverage finishing exactly at release (rule #62). All
     three constraints hold at once: start = D - M = 10vh, coverage = D - M + h
     = 110vh, release = D = 110vh. */
  html.mmh-pin .models-dwell{ height:110vh; }
  html.mmh-pin .company--bg{
    margin-top:-100vh;
    /* V59 — the panel that climbs must still be covering when the pin lets
       go, or the held section reappears from underneath it. The pin releases
       one dwell after it engages; the Company panel starts a viewport early
       (the -100vh above) and therefore has to be at least a viewport tall to
       still be there at the end. Measured, it was not: 764px at 1440x900,
       692px at 1024x900, 790px at 1920x1080 — 0.73 to 0.85 of a screen — so
       Featured Models' bottom edge and its "See our range" button were
       showing again below the Company photo. That has been live since V57.
       This is also why the reference's own covering section is full-height. */
    min-height:100vh;
  }

  /* V59 — a defect V57 shipped and V58 did not catch, found while proving the
     Built-with-experience band matches between pages: at the bottom of the
     hold, Featured Models was painting ON TOP of the sections BELOW the
     Company panel, and its "See our range" button was still hit-testable
     there — measured with elementFromPoint, so a real click would have landed
     on it rather than on the section a visitor was actually looking at.

     Cause: `#models` is `position:sticky` with `z-index:1`, which puts it in
     the positioned-elements paint layer. `.company--bg` sits above it at
     `z-index:2` and covered it correctly — but every section AFTER the
     Company panel is ordinary in-flow content with no z-index, and in-flow
     content paints BELOW positioned elements no matter how late it comes in
     the document. The -110vh pull is what brings those later sections up into
     the still-held pin's territory, so the two finally overlap.

     Fix: put every section that follows the pin into the same paint layer as
     the Company panel. Equal z-index resolves by document order, so Company
     still covers the pin, and anything after Company still covers Company —
     which is the order a reader expects. Scoped to `html.mmh-pin`, so it
     applies only where the pin is actually armed. */
  html.mmh-pin .models-pinwrap ~ .section{ position:relative; z-index:2; }

  /* V57.1 — rule #37, fifth time: AN ELEMENT INSIDE A PINNED PANEL CANNOT RUN
     A view()-DRIVEN ENTRANCE. The "See our range" button sits at the very
     bottom of this section, so it only enters the screen in the last ~200px
     before the pin engages — and then the section stops moving, so its
     view() timeline all but stops advancing with it. Measured: opacity 0.000
     the entire time it is on screen before the hold, 0.17 at 100px in, 0.52
     at 300px, and it does not reach 1 until ~600px into the hold — by which
     point the Company panel has climbed over it. Net effect: the button was
     never once legible on screen. The client saw it immediately ("the button
     is no longer that visible"), which is the same report as V40's "Where is
     the button on the left?" — same rule, same element type, new pin.

     Scope is deliberately narrow and measured, not blanket. A scan of every
     animated descendant of #models across the whole hold window found
     exactly ONE element under 0.9 opacity while on screen: this button. The
     cards sit higher up, get their full travel, and complete their entrance
     long before the pin engages — verified, so they keep it.

     `animation:none` hands the entrance back to the `.reveal` wrapper around
     this button, which is the class-driven, TIME-based system and therefore
     cannot freeze against a stalled scroll timeline. The button still fades
     in; it just no longer depends on travel it does not have. */
  html.mmh-pin #models .wrap .btn{
    animation:none;
    opacity:1;
    transform:none;
  }

  /* V61, the second cause of the transparency the client reported. The opaque
     ground added on `.company--bg` stops content showing through the panel,
     but the panel was ALSO arriving half-painted: `.company-bg-photo` runs the
     site-wide `mmhSettle` fade over `cover 0% cover 42%`, and measured through
     the climb that put the photograph at 0.00 / 0.005 / 0.52 / 0.83 opacity
     across precisely the frames in which the panel travels up over Featured
     Models. The photo was fading IN while the section moved, which reads as
     the section itself being semi-transparent.

     Same reasoning as the CTA freeze directly above (V57.1): an entrance keyed
     to a section's own scroll position is meaningless once that position is
     being driven by the pin instead of by the scroll. Frozen ONLY while the
     pin is armed — every other photograph on the site keeps the settle, and
     below 901px or without JS this one keeps it too, where the section is in
     normal flow and the fade is the correct gesture. */
  html.mmh-pin .company--bg .company-bg-photo{
    animation:none;
    opacity:1;
    transform:none;
  }
}

/* A held section sliding under a rising panel is exactly the kind of
   large-scale scroll-coupled movement reduced motion asks us not to do. The
   JS declines to arm the pin under `prefers-reduced-motion`, so this block is
   belt-and-braces for a preference that flips after load: everything unwinds
   to the ordinary static stack, same gold edge, nothing moving against the
   scroll. */
@media (prefers-reduced-motion:reduce){
  html.mmh-pin #models{ position:relative; top:auto; }
  html.mmh-pin .models-dwell{ height:0; }
  html.mmh-pin .company--bg{ margin-top:-56px; }
}

/* --- §4e: The Company as a full-width background band --- */
/* V28 — `overflow:clip`, NOT `overflow:hidden`.
   `hidden` makes this element a SCROLL CONTAINER, which makes it the nearest
   scrollport for everything inside it — and a `view()` timeline resolves
   against the nearest scrollport. The background photo fills this box exactly
   and never moves within it, so its timeline was frozen at a constant
   progress no matter where the page was scrolled. Measured: 0.6, unchanging,
   with the section still 2,855px below the viewport.
   `overflow:clip` clips identically and does NOT create a scroll container,
   so the document remains the scrollport and view() behaves. This is the same
   fix already recorded for `body{overflow-x}` breaking every sticky element. */
/* V46 · client — "didn't want the new style on The Company and smrt built
   section on homepage, so best to revert the original section layout."
   Everything the scene added between V32 and V43 is gone: the pinned track
   (`.company-reel`/`.company-reel-track`/`.company-reel-pin`), the animated
   aperture frame (`.company-bg-frame`), and the two-phase PIQUE reveal
   (`.company-smrt`, replacing the partner-card bar). This is back to the
   flat, static, non-pinned band it was before V32 — a plain full-bleed photo
   behind the copy and the partner card, with the site's ordinary scroll
   reveal (`.reveal`) and no scene of its own. See CURRENT-STATE.md for the
   full account and what stays removed vs what was already there before any
   of this shipped (the `mmhSettle`/`mmhTilt`/`mmhHeadWipe` entrance system
   below is unrelated — it's the site-wide V25-28 treatment every section
   gets, not part of the scene, and is untouched). */
/* V61 — client: "Can we remove the transparency when scrolling." Measured
   `background-color` on this element through the whole climb: `rgba(0,0,0,0)`
   at every scroll position. The panel had NO ground of its own — it was a
   transparent box relying entirely on its `.company-bg-photo` child to paint,
   which is fine for a section sitting in normal flow with page ground behind
   it, and wrong for a section that climbs OVER another one. Wherever the
   photo was not fully opaque, Featured Models showed straight through, which
   is exactly what the client screenshotted. An opaque ground here is the
   structural fix and holds regardless of animation state, viewport or JS.
   `isolation:isolate` above keeps the negative-z-index photo and scrim
   painting ABOVE this background, so nothing else changes. */
.company--bg{ position:relative; overflow:hidden; color:var(--paper); isolation:isolate; background:var(--ink); }
@supports (overflow:clip){ .company--bg{ overflow:clip; } }
.company--bg .company-bg-photo{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; object-position:center; z-index:-2;
}
.company--bg .company-bg-scrim{
  position:absolute; inset:0; z-index:-1; pointer-events:none;
  /* This band carries a full paragraph plus a partner card, so it needs more
     cover than the hero — pulled back so the building is visible through the
     right two thirds, which is where there is no text. */
  background:
    linear-gradient(90deg, rgba(16,24,32,.72) 0%, rgba(16,24,32,.44) 46%, rgba(16,24,32,.12) 78%, rgba(16,24,32,.04) 100%),
    linear-gradient(180deg, rgba(16,24,32,.08) 0%, rgba(16,24,32,.24) 100%);
}
.company--bg .company-lead{ max-width:60ch; }
.company--bg .company-lead .display-l,
.company--bg .company-lead .display-m{ margin-top:1.1rem; max-width:16ch; color:var(--paper); }
.company--bg .company-lead .lead{ margin-top:1.4rem; max-width:52ch; color:rgba(244,241,234,.9); }
.company--bg .eyebrow{ color:var(--gold-glow); }
.company--bg .eyebrow::before{ background:var(--gold-glow); }
/* Partner card sits on the photo — lift it off the background. */
.company--bg .partner-card{
  background:rgba(16,24,32,.82);
  -webkit-backdrop-filter:blur(6px); backdrop-filter:blur(6px);
}
/* SMRT BUILT logo (v2.15.3): the client's blue file, byte-for-byte, displayed
   exactly as the previous logo was — same 132px width in the partner card,
   same 38px height in the footer, no chip, no backing, no recolour. */
.partner-card-logo img{ width:132px; max-width:34vw; height:auto; }
@media (max-width:900px){
  .partner-card-logo img{ width:160px; }
  .company--bg .company-bg-scrim{
    background:linear-gradient(180deg, rgba(16,24,32,.50) 0%, rgba(16,24,32,.74) 100%);
  }
}

/* --- §4f: Custom Online Builder reduced to copy + CTA --- */
.cob--cta{ padding-block:var(--sp); background:var(--paper-2); }
.cob--cta .cob-cta-inner{
  max-width:64ch; margin-inline:auto; text-align:center;
  display:flex; flex-direction:column; align-items:center;
}
.cob--cta .cob-cta-inner .measure{ margin-inline:auto; }
.cob--cta .cob-cta-actions{ margin-top:1.8rem; display:flex; flex-wrap:wrap; gap:.9rem; justify-content:center; }

/* --- §4g: "Move in within 12–14 weeks" note beside a section heading --- */
.collection-head{
  display:grid; grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr);
  gap:clamp(24px,4vw,64px); align-items:start;
}
.delivery-note{
  border:1px solid var(--line-gold); background:var(--card);
  padding:clamp(20px,2.4vw,30px); text-align:center;
}
.delivery-note-head{
  font-family:var(--display); font-weight:400;
  font-size:clamp(1.3rem,2.1vw,1.75rem); line-height:1.25; color:var(--paper);
  padding-bottom:.7rem; border-bottom:1px solid var(--line-gold);
}
.delivery-note-copy{
  margin-top:1rem; font-size:.95rem; line-height:1.7; color:var(--muted);
}
.bg-ink .delivery-note,.bg-green .delivery-note{ background:rgba(255,255,255,.05); }
.bg-ink .delivery-note-head,.bg-green .delivery-note-head{ color:var(--paper); }
.bg-ink .delivery-note-copy,.bg-green .delivery-note-copy{ color:var(--muted-dk); }
@media (max-width:900px){
  .collection-head{ grid-template-columns:1fr; }
}

/* --- Mobile horizontal-scroll fix (found while verifying V18.1 §4) ---
   `.btn` is white-space:nowrap, so the long homepage CTA ("See our range of
   Modular granny flats designs Gallery") laid out 526px wide and dragged the
   whole document into horizontal scroll on every phone viewport — the single
   worst offender on the homepage. Below 700px, long buttons wrap and centre
   instead. Short buttons are unaffected. This is one finding from §12, which
   still needs its full audit. */
@media (max-width:700px){
  .btn{ white-space:normal; text-align:center; max-width:100%; }
}

/* Two more §12 horizontal-scroll offenders, found the same way:
   1. The model-filter divider label is white-space:nowrap and is wider than a
      phone at 410px, so /modular-homes/ scrolled sideways.
   2. Footer contact rows are flex without min-width:0, so a long address or
      email refused to shrink and pushed the footer past the viewport. */
@media (max-width:700px){
  .mf-divider{ flex-wrap:wrap; }
  .mf-divider span{ white-space:normal; }
}
.foot-contact li{ min-width:0; }
.foot-contact .fc-txt{ min-width:0; overflow-wrap:anywhere; }

/* --- V18.1 §6a/b: Behind the Build two-column videos --- */
.vid-2col{
  display:grid; grid-template-columns:1fr 1fr; gap:clamp(16px,2.2vw,28px);
  max-width:1100px; margin-inline:auto;
}
.vid-cell{ margin:0; }
.vid-frame{
  position:relative; display:block; width:100%; aspect-ratio:16/9;
  background:#000; border:1px solid rgba(255,255,255,.14); overflow:hidden;
  padding:0;
}
.vid-frame iframe{ position:absolute; inset:0; width:100%; height:100%; display:block; border:0; }
/* .gal-video-btn is written to FILL a positioned parent (absolute, inset:0,
   height:100%). Used standalone as the frame itself, that height:100% +
   aspect-ratio feedback loop inflated the cell ~33px past a phone viewport.
   Reset it to a normal in-flow frame here. */
.vid-cell .gal-video-btn.vid-frame{ position:relative; inset:auto; height:auto; }
.vid-frame .photo{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.vid-cell .cap{
  margin-top:.7rem; text-align:center; font-family:var(--mono); font-size:.72rem;
  letter-spacing:.18em; text-transform:uppercase; color:var(--muted-dk);
}
@media (max-width:760px){
  .vid-2col{ grid-template-columns:1fr; }
}

/* ============================================================
   V18.1 — Gallery mega-menu fixes
   ============================================================ */
/* The mega-menu cards are <a> elements inside .nav-links, so they inherited
   white-space:nowrap from the top-level nav links — long model names could
   not wrap and ran over the neighbouring column. They also inherited the
   gold underline ::after. Reset both, at higher specificity than
   `.nav-links a`. */
.nav-links .nav-mega-card,
.nav-links .nav-mega-foot{ white-space:normal; }
.nav-links .nav-mega-card::after,
.nav-links .nav-mega-foot::after{ display:none; }
.nav-mega-name{ overflow-wrap:break-word; }
.nav-mega-grid{ column-gap:.9rem; }

/* ============================================================
   V18.1 §11 — Build Journey scrollytelling (Our Process)
   ============================================================ */
/* Tall scroll runway; the sticky viewport inside it pins while progress
   drives the active step + photo. */
.proc-scroll{ height:auto; }
.proc-sticky{ display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  gap:clamp(24px,3.5vw,56px); align-items:start; }
.proc-photos{ position:relative; overflow:hidden; border-radius:14px;
  border:1px solid rgba(217,192,138,.22); background:#0e0c08;
  aspect-ratio:4/3; }
.proc-photo{ position:absolute; inset:0; margin:0; opacity:0;
  transition:opacity .6s var(--ease); }
.proc-photo.is-active{ opacity:1; }
.proc-photo img{ width:100%; height:100%; object-fit:cover; display:block; }
.proc-photo-cap{ position:absolute; left:0; right:0; bottom:0;
  padding:.9rem 1.1rem .8rem; font-family:var(--mono); font-size:.68rem;
  letter-spacing:.2em; text-transform:uppercase; color:var(--gold-glow);
  background:linear-gradient(180deg, rgba(14,12,8,0) 0%, rgba(14,12,8,.82) 60%); }

@media (min-width:901px){
  /* The runway = the sticky block's own height + 150vh of scroll distance.
     The distance is a generated CHILD (::after), not padding — a sticky
     element is confined to its parent's content box, so padding would give
     it nowhere to travel. */
  .proc-scroll::after{ content:""; display:block; height:150vh; }
  /* position:sticky only pins when the block FITS the viewport, so the
     pinned journey is compacted: smaller badges, tighter cards. */
  .proc-sticky{ position:sticky; top:72px; }
  .proc-sticky .proc-node{ padding-block:.22rem; grid-template-columns:38px 1fr; gap:.9rem; }
  .proc-sticky .proc-badge{ width:38px; height:38px; border-width:2px; }
  .proc-sticky .proc-badge svg{ width:18px; height:18px; }
  .proc-sticky .proc-spine{ left:18px; top:20px; bottom:20px; }
  .proc-sticky .proc-kicker{ font-size:.56rem; }
  .proc-sticky .proc-card h4{ font-size:1rem; margin:.12rem 0 .18rem; }
  .proc-sticky .proc-card p{ font-size:.83rem; line-height:1.45; }
  .proc-sticky .proc-nodes{ gap:.25rem; }
  .proc-photos{ align-self:stretch; aspect-ratio:auto; min-height:380px; }
}
/* Shorter laptop screens: drop the step descriptions so the pinned block fits. */
@media (min-width:901px) and (max-height:840px){
  .proc-sticky .proc-card p{ display:none; }
}
@media (max-width:900px){
  /* No scroll-jacking on mobile: static list, single photo above the steps
     that still follows the active step. */
  .proc-sticky{ grid-template-columns:1fr; }
  .proc-photos{ order:-1; margin-bottom:1.4rem; }
}

/* --- V18.1: centred featured video band (Our Process, Installation Story) --- */
.fv-frame{
  position:relative; width:min(960px,100%); margin-inline:auto;
  aspect-ratio:16/9; background:#000; border:1px solid var(--line-gold);
  border-radius:14px; overflow:hidden;
  box-shadow:0 30px 60px -34px rgba(0,0,0,.9);
}
.fv-frame iframe{ position:absolute; inset:0; width:100%; height:100%; display:block; border:0; }

/* ============================================================
   V18.1 §12 — Mobile audit fixes (320–430px, iOS Safari + Android Chrome)
   Findings from a full 30-page × 7-viewport instrumented sweep.
   ============================================================ */

/* iOS Safari: 100vh/94vh includes the collapsing URL bar and makes the hero
   jump while scrolling. Small-viewport units are stable. */
@supports (height: 100svh){
  .hero{ min-height:clamp(560px, 92svh, 960px); }
}

/* Honeypot input: 13px font made audits (and iOS) treat it as a zoomable
   field. It is off-screen; 16px costs nothing and silences the behaviour. */
.hp-field input{ font-size:16px; }

/* Legibility floor: the mono micro-labels sat at 9.3–9.7px, below Android's
   ~10px floor — Chrome force-inflates them inconsistently, which reads as
   "broken text" on Galaxy devices. Lift everything to ≥10.5px on phones. */
@media (max-width:560px){
  .gal-item .cap,
  .ed-label,
  .nc-tag,
  .model-media .code,
  .model-media .area-chip,
  .model-media .expand-chip,
  .model-body .card-note,
  .nav-mega-specs,
  .proc-kicker,
  .th-weeks-chip,
  .th-soon-chip,
  .model-media .badge,
  .strip .l,
  .md-hero .l,
  .fx-key .l,
  .fx-sub,
  .expand-badge,
  .pill-cap,
  .spec-code{ font-size:.68rem; }
  .md-hero .strip .s .l,
  .model-media .badge,
  .th-hero .strip .s .l,
  .hsl-thumbs + .hsl-arrow span{ font-size:.66rem !important; }
  /* any remaining sub-10px mono label */
  .eyebrow, .compass, .stagelabel{ font-size:max(.68rem, 10.5px); }
  .bx-points li span{ font-size:.68rem; }
  /* The model/townhouse hero stat strip (up to 4 fixed-gap stats) cannot fit
     320–360px in one row once labels are legible — let it wrap. */
  .md-hero .strip{ flex-wrap:wrap; gap:1.1rem 1.8rem; }
}

/* Touch targets: Apple/Google minimum is ~44/48px; these sat at 30–34px. */
@media (max-width:900px){
  .gal-tab,
  .mf-pill,
  .chip{ min-height:42px; display:inline-flex; align-items:center; }
  .hsl-thumb{ width:44px; height:32px; }
  .mtc-tab{ min-height:42px; }
}

/* Finance page: the stat row (three fixed-gap stats) was 4px too wide at
   320px. Let it wrap and shrink instead of pushing the page sideways. */
.split-copy .stat-row{ flex-wrap:wrap; gap:clamp(1.2rem,4vw,2.4rem); }
.split-copy .stat{ min-width:0; }

/* ============================================================
   V18.1 — Mobile footer redesign (≤620px)
   The stacked footer read as a loose left-aligned pile: brand block, two
   cramped link columns, then Showroom & Contact squeezed into a half column.
   Centre the brand, give the link columns breathing room and gold heading
   rules, and let the contact block go full width.
   ============================================================ */
@media (max-width:620px){
  .site-foot{ padding-top:clamp(48px,8vh,72px); text-align:left; }
  .foot-top{ gap:2rem 1.6rem; padding-bottom:2.4rem; }

  /* Brand block: centred and self-contained */
  .foot-brand{ text-align:center; padding-bottom:2rem; border-bottom:1px solid rgba(255,255,255,.1); }
  .foot-logo{ margin-inline:auto; max-width:190px; }
  .foot-brand p{ margin-inline:auto; font-size:.9rem; line-height:1.7; }
  .foot-social{ justify-content:center; margin-top:1.2rem; }
  .foot-social a{ width:44px; height:44px; }

  /* Link columns: clear headings with a short gold rule, tighter rhythm */
  .foot-col h5{ margin-bottom:.7rem; padding-bottom:.55rem; position:relative; }
  .foot-col h5::after{ content:""; position:absolute; left:0; bottom:0; width:26px; height:1px; background:var(--gold); opacity:.7; }
  .foot-col a{ padding:.5rem 0; font-size:.92rem; }

  /* Showroom & Contact: full width so addresses stop cramping */
  .foot-col:last-child{ grid-column:1 / -1; padding-top:1.6rem; border-top:1px solid rgba(255,255,255,.1); }
  .foot-contact{ gap:1.1rem; }

  /* Warranty badges: centred column, no crowding (foot-top already draws the
     divider above — drop the doubled hairline) */
  .foot-warranty{ flex-direction:column; align-items:center; gap:.9rem; padding-top:.4rem; margin-top:1.6rem; border-top:0; text-align:center; }
  .foot-warranty b{ font-size:1.3rem; }

  /* SMRT partner block + bottom line: centred, calmer spacing */
  .foot-bottom{ justify-content:center; text-align:center; flex-direction:column; align-items:center; gap:.5rem; padding-top:1.8rem; font-size:.8rem; }
}

/* ============================================================
   V19 — Module Configurator (base + auxiliary assembly)
   ============================================================ */
.cfg{ margin-top:1rem; }
.cfg-groups{ display:grid; gap:1.1rem; margin-bottom:1.4rem; }
.cfg-group-label{ display:block; font-family:var(--mono); font-size:.68rem; letter-spacing:.2em;
  text-transform:uppercase; color:var(--gold-deep); margin-bottom:.55rem; }
.cfg-btns{ display:flex; flex-wrap:wrap; gap:.5rem; }
.cfg-btn{ display:inline-flex; flex-direction:column; align-items:flex-start; gap:.1rem;
  font:inherit; padding:.5rem .95rem; border:1px solid var(--line-soft); background:var(--card);
  border-radius:9px; cursor:pointer; min-height:42px; justify-content:center;
  transition:border-color .25s var(--ease), background .25s var(--ease), color .25s var(--ease); }
.cfg-btn:hover{ border-color:var(--line-gold); }
.cfg-btn.is-active{ background:var(--gold); border-color:var(--gold); }
.cfg-btn .cb-l{ font-size:.85rem; font-weight:600; color:var(--paper); line-height:1.2; }
.cfg-btn .cb-s{ font-family:var(--mono); font-size:.66rem; color:var(--muted-2); }
.cfg-btn.is-active .cb-l{ color:var(--ink); }
.cfg-btn.is-active .cb-s{ color:rgba(16,24,32,.72); }
.cfg-host svg{ width:100%; height:auto; display:block; }
.cfg-custom{ margin:0; }
.cfg-custom img{ width:100%; height:auto; display:block; border-radius:8px; }
.cfg-custom figcaption{ margin-top:.7rem; font-family:var(--mono); font-size:.68rem;
  letter-spacing:.14em; text-transform:uppercase; color:var(--muted); text-align:center; }
.cfg-readout{ display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:.8rem; margin-top:1.2rem; }
.cfg-readout .r{ background:var(--card); border:1px solid var(--line-soft); border-radius:10px;
  padding:.8rem 1rem; display:flex; flex-direction:column; gap:.15rem; min-width:0; }
.cfg-readout .k{ font-family:var(--mono); font-size:.62rem; letter-spacing:.16em; text-transform:uppercase; color:var(--muted-2); }
.cfg-readout .val{ font-family:var(--display); font-weight:400; font-size:clamp(1.2rem,2vw,1.7rem); color:var(--paper); }
.cfg-readout .val em{ font-style:normal; font-size:.65em; color:var(--muted); }
.cfg-note{ margin-top:1.1rem; font-size:.9rem; color:var(--muted); max-width:70ch; }
/* dark sections (if ever placed on bg-ink) inherit the mtc stage styling */
@media (max-width:700px){
  .cfg-readout{ grid-template-columns:repeat(2,minmax(0,1fr)); }
}

/* ==== v2.20.1 — Module Configurator: dropdown selection on mobile & tablet ====
   ≤1024px the four button clusters collapse to native selects — one row per
   group — so the entry points stay compact and the drawing gets the screen. */
.cfg-select{
	display:none;
	width:100%;
	max-width:540px;
	padding:13px 46px 13px 16px;
	font:600 15px/1.25 var(--body);
	color:#F4F1EA;
	background:#16212C url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%239A7636' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat right 16px center;
	background-size:12px 8px;
	border:1px solid rgba(244,241,234,.34);
	border-radius:10px;
	-webkit-appearance:none;
	appearance:none;
	cursor:pointer;
}
.cfg-select:focus-visible{ outline:2px solid #B3945E; outline-offset:2px; }
/* v2.21.1 — client request: the dropdowns are now the ONLY selection UI at
   every width (desktop included). The button clusters stay in the markup as
   the JS state source but never display. */
.cfg .cfg-btns{ display:none; }
.cfg-select{ display:block; }
.cfg-groups{ gap:14px; }
.cfg-group-label{ margin-bottom:6px; }
/* Desktop: sit the four dropdowns in two columns so the configurator keeps
   its horizontal rhythm instead of stacking four full-width rows. */
@media (min-width:701px){
	.cfg-groups{ grid-template-columns:1fr 1fr; column-gap:1.4rem; }
	.cfg-select{ max-width:none; }
}
/* v2.21.3 — wide desktop: all four selectors on ONE line. */
@media (min-width:1080px){
	.cfg-groups{ grid-template-columns:repeat(4, minmax(0,1fr)); column-gap:1.1rem; }
	.cfg-select{ padding-right:38px; background-position:right 12px center; }
}

/* ==== v2.21.0 — portrait (YouTube Shorts) featured video frame ==== */
.fv-frame.fv-portrait{
	width:min(400px, 86vw);
	aspect-ratio:9/16;
}

/* ============================================================
   v2.22.0 — BLOG POST TYPOGRAPHY
   Everything the WordPress editor can output, styled to match the
   rest of the site so a post looks designed the moment it is
   published. Write in the editor, publish, done.
   Scoped to .post-content so nothing here can affect page layouts.
   ============================================================ */

.post-content{
  max-width:min(760px, 100%);
  margin-inline:auto;
  font-size:1.075rem;      /* one body size for EVERY element below */
  line-height:1.78;
  color:var(--paper);
}
/* V52 — client: "Privacy and terms page ... should be the same content
   width as others." .post-content's 760px reading column is right for a
   blog article, but Privacy/Terms aren't articles — the site's other plain
   content pages (FAQ's .faq-full) run the full site .wrap width (1760px).
   V51 borrowed .post-content wholesale for its heading/list/link styling
   and inherited the 760px column along with it. Two classes beats one
   regardless of source order, so this overrides just the width for the two
   pages that carry both .legal and .post-content, leaving every other
   .post-content use (blog articles) at 760px untouched. */
.legal.post-content{ max-width:var(--maxw); margin-inline:auto; }
/* One size for every text element: paragraphs, lists and captions all
   inherit, so a post can never end up with mismatched body sizes. */
.post-content p,
.post-content li,
.post-content dd,
.post-content dt,
.post-content td,
.post-content th{ font-size:inherit; line-height:inherit; }
.post-content > *{ margin:0 0 1.35rem; }
.post-content > *:last-child{ margin-bottom:0; }

/* --- Headings ------------------------------------------------ */
.post-content h2,
.post-content h3,
.post-content h4{ font-family:var(--display); font-weight:400; color:var(--paper); }
.post-content h2{ font-size:clamp(1.65rem,3.2vw,2.05rem); line-height:1.2; margin:2.6rem 0 1rem; }
.post-content h3{ font-size:clamp(1.3rem,2.4vw,1.5rem); line-height:1.28; margin:2.1rem 0 .75rem; }
.post-content h4{ font-size:1.16rem; line-height:1.35; margin:1.8rem 0 .6rem; font-weight:500; }
.post-content h2 + h3{ margin-top:1.3rem; }
/* Hairline under H2s, echoing the section rules used site-wide. */
.post-content h2::after{
  content:""; display:block; width:56px; height:2px; margin-top:.75rem;
  background:var(--gold); opacity:.75;
}

/* --- Opening paragraph gets a little more presence ----------- */
/* ⚠ V177 — GUARDED, because :first-of-type does not mean "the opening
   sentence". On Max's post the first <p> in the content is the wrapper the
   editor puts around the lead IMAGE, so the larger lead treatment was either
   landing on an image or, before the empty paragraphs were stripped in
   functions.php, on an empty one — measured at 16px, i.e. not applying at all
   while looking like it did.

   With :has(img) excluded it applies when a post opens with text, which is the
   normal case and what the treatment is for, and is simply skipped when a post
   opens with a picture. Skipping is the correct outcome: there is no sentence
   there to enlarge. */
.post-content > p:first-of-type:not(:has(img)){ font-size:1.2rem; line-height:1.66; color:var(--muted-dk,#4a4335); }

/* --- Links --------------------------------------------------- */
.post-content a{
  color:var(--gold-deep); text-decoration:underline;
  text-decoration-thickness:1px; text-underline-offset:3px;
  transition:color .18s ease;
}
.post-content a:hover{ color:var(--gold-hi); }

/* --- Lists (a global reset removes markers; restore them) ----- */
.post-content ul,
.post-content ol{ padding-left:1.35rem; margin-bottom:1.35rem; }
.post-content ul{ list-style:disc outside; }
.post-content ol{ list-style:decimal outside; }
.post-content ul li,
.post-content ol li{ list-style:inherit; margin:.45rem 0; padding-left:.25rem; }
.post-content li::marker{ color:var(--gold-deep); }
.post-content li > ul,
.post-content li > ol{ margin:.45rem 0 .2rem; }
.post-content ul ul{ list-style:circle outside; }

/* --- Images & captions --------------------------------------- */
.post-content figure{ margin:2rem 0; }
.post-content img{ max-width:100%; height:auto; border-radius:12px; display:block; }
.post-content figcaption,
.post-content .wp-element-caption{
  font-family:var(--body); font-size:.86rem; line-height:1.55;
  color:var(--muted-2); margin-top:.6rem; text-align:left;
}
/* Wide / full-width blocks break out of the reading column. */
.post-content .alignwide{ width:min(1040px,92vw); max-width:none; margin-left:50%; transform:translateX(-50%); }
.post-content .alignfull{ width:100vw; max-width:none; margin-left:50%; transform:translateX(-50%); border-radius:0; }
.post-content .alignfull img{ border-radius:0; }
.post-content .aligncenter{ margin-inline:auto; text-align:center; }
.post-content .aligncenter figcaption{ text-align:center; }
@media (min-width:900px){
  .post-content .alignleft{ float:left; margin:.4rem 1.8rem 1.2rem 0; max-width:46%; }
  .post-content .alignright{ float:right; margin:.4rem 0 1.2rem 1.8rem; max-width:46%; }
}
.post-content .wp-block-image{ margin:2rem 0; }
.post-content .wp-block-gallery{ display:grid; gap:1rem; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); }
.post-content .wp-block-gallery figure{ margin:0; }

/* --- Quotes -------------------------------------------------- */
.post-content blockquote{
  border-left:3px solid var(--gold); padding:.2rem 0 .2rem 1.5rem;
  margin:2rem 0; font-family:var(--display); font-size:1.32rem; line-height:1.5;
  font-style:italic; color:var(--muted-dk,#4a4335);
}
.post-content blockquote p{ font-size:inherit; line-height:inherit; margin-bottom:.6rem; }
.post-content blockquote cite,
.post-content blockquote .wp-block-quote__citation{
  display:block; font-family:var(--mono); font-style:normal; font-size:.72rem;
  letter-spacing:.12em; text-transform:uppercase; color:var(--muted-2); margin-top:.5rem;
}
.post-content .wp-block-pullquote{
  border-left:0; border-block:1px solid var(--line); padding:2rem 0; text-align:center;
}
.post-content .wp-block-pullquote blockquote{ border:0; padding:0; margin:0; }

/* --- Tables -------------------------------------------------- */
.post-content .wp-block-table,
.post-content figure.wp-block-table{ margin:2rem 0; overflow-x:auto; }
.post-content table{
  width:100%; border-collapse:collapse; background:var(--card);
  border:1px solid var(--line); font-size:.98rem;
}
.post-content thead th{
  background:var(--ink-3); color:var(--paper);
  font-family:var(--body); font-weight:600; font-size:.82rem;
  letter-spacing:.06em; text-transform:uppercase; text-align:left;
}
.post-content th,
.post-content td{ padding:.72rem .95rem; border-bottom:1px solid var(--line-soft); vertical-align:top; }
.post-content tbody tr:last-child td{ border-bottom:0; }
.post-content tbody tr:nth-child(even){ background:rgba(179,148,94,.06); }

/* --- Code, marks, small print --------------------------------- */
.post-content code{
  font-family:var(--mono); font-size:.9em; background:rgba(244,241,234,.09);
  padding:.12em .42em; border-radius:4px;
}
.post-content pre{
  font-family:var(--mono); font-size:.88rem; line-height:1.6; background:var(--ink-3);
  color:var(--paper); padding:1.2rem 1.3rem; border-radius:10px; overflow-x:auto;
}
.post-content pre code{ background:none; padding:0; color:inherit; }
.post-content mark{ background:rgba(179,148,94,.28); padding:.05em .2em; }
.post-content hr,
.post-content .wp-block-separator{
  border:0; height:1px; background:var(--line); margin:2.6rem auto;
}
.post-content .wp-block-separator.is-style-dots{ background:none; text-align:center; }

/* --- Buttons & embeds ----------------------------------------- */
.post-content .wp-block-button__link{
  display:inline-block; background:var(--gold); color:var(--ink);
  font-family:var(--body); font-weight:600; font-size:.95rem;
  padding:.85rem 1.5rem; border-radius:8px; text-decoration:none;
}
.post-content .wp-block-button__link:hover{ background:var(--gold-hi); color:var(--ink); }
.post-content .wp-block-embed__wrapper,
.post-content .wp-block-embed iframe,
.post-content iframe{ max-width:100%; }
.post-content .wp-block-embed.wp-has-aspect-ratio .wp-block-embed__wrapper{
  position:relative; padding-top:56.25%; border-radius:12px; overflow:hidden;
}
.post-content .wp-block-embed.wp-has-aspect-ratio iframe{
  position:absolute; inset:0; width:100%; height:100%; border:0;
}

/* --- Drop cap (editor option) --------------------------------- */
.post-content .has-drop-cap:not(:focus)::first-letter{
  font-family:var(--display); font-weight:400; font-size:3.4em; line-height:.82;
  float:left; margin:.06em .12em 0 0; color:var(--gold-deep);
}
.post-content .has-drop-cap{ font-size:1.075rem; color:var(--paper); }

/* --- Mobile --------------------------------------------------- */
@media (max-width:600px){
  .post-content{ font-size:1.02rem; line-height:1.72; }
  .post-content > p:first-of-type:not(:has(img)){ font-size:1.1rem; }
  .post-content blockquote{ font-size:1.15rem; padding-left:1.1rem; }
  .post-content .alignleft,
  .post-content .alignright{ float:none; max-width:100%; margin:1.4rem 0; }
  .post-content table{ font-size:.92rem; }
  .post-content th,.post-content td{ padding:.6rem .7rem; }
}

/* --- Post hero: keep long headlines readable over any photo --- */
/* ⚠ V180 — THE --post SCRIM OVERRIDE IS RETIRED. Bianca, 25 Sep: "the page
   title design on the bottom is still not the same on the others."

   Measured against About Us. The base `.page-hero-band .scrim` is two layers:
   a soft radial wash under the heading, and a vertical gradient that finishes
   on `var(--ink) 100%` — solid page background at the very bottom edge. That
   last stop is what makes every other hero melt into the section beneath it
   with no visible seam.

   This override replaced both layers with one linear gradient that stops at
   rgba(16,24,32,.56). It never reaches the page colour, so the photograph ends
   on a hard line against the dark section below, and there is no wash behind
   the heading either. That line is the "bottom" she is pointing at.

   There is nothing for the override to do that the base does not do better on
   this photograph, so it goes rather than being retuned. The colour rules
   underneath are kept — they set the heading, eyebrow and meta against a photo
   and are unrelated to the seam. */
.page-hero-band--post .page-hero h1{ color:#fff; }
.page-hero-band--post .page-hero .eyebrow{ color:var(--gold); }
.page-hero-band--post .post-meta{ color:rgba(255,255,255,.82); }

/* ===========================================================================
   v2.23.0 — hero heights, model-page scroll cue, homepage hero legibility
   =========================================================================== */

/* ---- 1. Category hero bands match the model-page hero height ----
   .page-hero-band used to be sized only by its text padding (~518px at a
   900px viewport). The background photo is object-fit:cover, so a wide
   image such as townhouse-pbg-hero.jpg lost ~110px off the top — the
   townhouse rooflines. The band now carries the same min-height as
   .md-hero (70vh), so there is far less to crop, and the title block is
   centred in it. Blog post heroes keep their own 44vh (rule further up). */
.page-hero-band{ min-height:70vh; display:flex; align-items:center; }
/* flex-grow first, so .wrap's max-width + auto margins still centre the text.
   (An auto-margin grid/flex item with no grow shrinks to fit-content and the
   title block drifts to the middle of the page.) */
.page-hero-band > .page-hero{ flex:1 1 auto; }

/* Bias the crop toward the top of the photo. Applied per page, because most
   heroes are correct centred; add the class only where the subject is high
   in the frame (currently /townhouses/). */
.page-hero-band--top > .photo{ object-position:center 26%; }

/* V67 · Finance — the client asked for the hero line at the BOTTOM LEFT of the
   photograph (annotated PDF p.2, stated twice). The band is already a flex row
   centring its .page-hero child, so this changes align-items and nothing else:
   the wrap, the scrim, the z-order and the scroll cue are untouched.
   Bottom-aligned hero text lands where the band's own scrim is at its DARKEST
   — its linear layer runs to rgba(16,24,32,.52) at 97% — so this is the safer
   end of the band for legibility, not the riskier one. Measured, not assumed:
   worst-case contrast on this hero is asserted in the V67 render harness. */
.page-hero-band--bottom{ align-items:flex-end; }
.page-hero-band--bottom > .page-hero{ padding-bottom:clamp(48px,7vh,84px); }

@media (max-width:900px){
  .page-hero-band{ min-height:52vh; }
}

/* ---- 2. Scroll cue on the model + townhouse hero ----
   Same pill as the homepage .hero-scroll. On .md-hero the right-hand column
   is the ink info panel, so the pill inverts to paper-on-ink to stay visible.
   Hidden where the hero stacks — on a phone the scroll is self-evident. */
.md-hero{ position:relative; }
.md-hero .md-scroll{ background:var(--ink-3); color:var(--paper); border:1px solid var(--line-gold);
  box-shadow:0 14px 34px rgba(0,0,0,.6); }
.md-hero .md-scroll:hover{ background:var(--gold); color:var(--ink); }
@media (max-width:900px){
  .md-hero .md-scroll{ display:none; }
}

/* ---- 3. Homepage hero eyebrow + caption legibility ----
   Both sat on --gold-deep / --muted-2 with no shadow, and disappeared over
   the pale slides (slide-1, slide-5). Darkened, and given the same cream
   halo the h1 already uses. The caption goes to Space Mono 700. */
/* Legibility over photography, WITHOUT darkening the photograph.

   The scrim is deliberately light — the client's note on 25 Aug was that
   the images must stay clean, and the reference's do. That leaves the
   small mono labels exposed: measured across all nine live slides the
   eyebrow ran 1.2–4.9:1 against the brightest ninety percent of the
   ground beneath it, because it sits high in the frame where the scrim
   has already faded out.

   A shadow is the tool that fixes this without touching a single pixel
   of the picture. It is deliberately DARK and TIGHT — not the cream halo
   the old site used, which glowed. On a dark ground a soft drop shadow
   reads as depth and is invisible as an effect; on the two brightest
   slides it is the difference between readable and not.

   To remove it: delete this block. Nothing else depends on it. */
/* The eyebrow sits high in the frame, above where the scrim has any
   density, so on the two palest slides it is the one element the light
   treatment cannot carry on contrast alone. Weight and size do the rest
   of the work — both are free, neither touches the photograph. */
/* V33 — a hero-only gold, one step up the same ramp.
   Lightening the scrim to the client's brief left this one element at 4.36:1,
   under AA for text this size, on the palest slide. The scrim cannot go back
   up: the instruction was to show more of the photograph. So the shortfall is
   paid by the TYPE, which costs the photograph nothing.
   #D9C08A sits between --gold-glow (#C9A96C) and --gold-hi (#E2CFA4) — still
   the brand gold, measured at 5.4:1 on the worst slide.
   ⚠ Scoped to `.hero` only. This is a photograph-legibility fix; the same
   eyebrow on the flat dark grounds elsewhere already clears AA on the brand
   gold and must not drift lighter alongside it. */
.hero{ --gold-hero:#D9C08A; }
.hero .hero-eyebrow{ color:var(--gold-hero); font-size:.76rem; font-weight:700; }
.hero .hero-eyebrow::before{ background:var(--gold-hero); opacity:.9; }
/* Stacked layout: paper, for the reason recorded on the scrim block — gold at
   this size cannot be carried over photography by any scrim that still shows
   the photograph. The hairline stays gold and makes the brand cue instead.

   Same 1199px breakpoint as the scrim and the layout, and it has to be: with
   the switch at 820 the eyebrow measured 4.31:1 at 1024x768 and 4.40 at
   1024x1366 — the two-column gold left on a one-column composition. On paper
   the same measurements are 7.1 and above. Every element of this hero now
   changes shape at exactly one width. */
@media (max-width:1199px){
  .hero .hero-eyebrow{ color:var(--paper); }
  .hero .hero-eyebrow::before{ background:var(--gold-hero); opacity:1; }
}
.hero .hero-caption{ color:var(--paper); font-weight:700; }
/* ⚠ `.hero h1` IS NOT IN THIS LIST ANY MORE — see the note on the eyebrow.
   The homepage hero declares its own, much denser shadow 2,700 lines earlier;
   this weaker one sat later at equal specificity and silently won. */
/* V120 — text-shadow removed here, per Bianca 17 Sep, "remove all the text
   shadows on all pages". The scrim under these heroes is what carries the
   contrast and it was measured at V55; the shadow was belt-and-braces on top
   of it. The rule is kept as an empty signpost because the two comment blocks
   around it are the record of a cascade trap that cost a release, and deleting
   the rule would orphan them. */
/* ⚠ `.hero .hero-eyebrow` IS NOT IN THIS LIST ANY MORE, AND MUST NOT GO BACK.
   It was, with a weaker shadow than the homepage hero's own rule 2,700 lines
   earlier — equal specificity, later in the file, so this one won and the
   hero's stronger shadow never painted. Strengthening it upstream changed
   nothing at all: fourteen viewport sizes reported contrast identical to three
   significant figures before and after. Same cascade trap as the gallery
   filter. The homepage hero owns its own type treatment; these are the other
   heroes. */
.hero .hero-caption,
.page-hero .eyebrow,
.page-hero .lead,
.md-hero .m-main .eyebrow{ /* V120 — text-shadow removed, see above. */ }

/* V120 — AND THE BACKSTOP. The three rules above are the ones that existed on
   17 September. This kills any that get written later, or any that arrive
   inside a plugin or an inline style, so "no text shadows" stays true without
   anyone having to remember it. Deliberately site-wide and deliberately
   !important: it is a stated client rule, not a styling preference. */
*, *::before, *::after{ text-shadow:none !important; }

/* Category hero bands carry the same small-text treatment as the homepage
   hero — with more of the photo now showing, the eyebrow and lead need the
   halo to stay readable. Blog post heroes keep their white-on-dark rules
   (higher specificity, further up). */
.page-hero .eyebrow{ color:var(--gold-glow); }
.page-hero .eyebrow::before{ background:var(--gold-glow); opacity:.9; }
/* The lead ran the full width of the wrap — up to 1100px on a wide
   screen. That is a poor measure on any ground, and on a photograph it
   walks the last two thirds of the line straight out of the scrim and
   onto the brightest part of the picture. Holding it to 52ch keeps it
   both readable and inside the ellipse, without darkening anything. */
/* Full opacity, not .94 — with the scrim this light, every free point of
   contrast counts, and transparency on a hero band is not buying anything. */
.page-hero .lead{ color:var(--paper); max-width:52ch; font-weight:500; }
.page-hero .eyebrow{ font-size:.76rem; font-weight:700; }
.page-hero h1{ max-width:20ch; }
.page-hero-band--post .page-hero h1{ max-width:26ch; }

/* ---- 4. "Download Brochure (PDF)" was invisible on every model and
   townhouse hero ----
   .md-hero .m-info paints itself var(--ink) but never carried the .bg-ink
   class, so .btn.ghost kept its default ink-on-cream colours: ink text
   (#16130D) on an ink panel (#16130D), contrast 1:1. The button has been
   there since V13 and simply could not be seen. Same inversion as
   .bg-ink .btn.ghost. */
.md-hero .m-info .btn.ghost{ --fg:var(--paper); --bd:rgba(255,255,255,.28); }
.md-hero .m-info .btn.ghost::after{ background:var(--gold); }
.md-hero .m-info .btn.ghost:hover{ color:var(--ink); border-color:var(--gold); }

/* ---- 5. (v2.23.1) Scroll cue on the category hero bands ----
   Same pill as the homepage. .page-hero-band is already position:relative, so
   .hero-scroll needs no extra positioning. The landing target is a zero-height
   span immediately after the band; html{scroll-padding-top:96px} keeps it
   clear of the sticky header, and scroll-behavior:smooth is already global
   (and already disabled under prefers-reduced-motion).
   Not applied to /modular-homes/ or /granny-flats/ — neither has a hero band
   (removed in V18.1 §5a) — nor to blog post heroes, which run at 44vh. */
.hero-scroll-target{ display:block; height:0; }

/* ---- v2.27.0 solo townhouse feature card — RETIRED in v2.29.0 ----
   Townhouses 02-05 are still hidden while material supply is delayed, and
   with one design left this turned the grid into a full-width feature card.
   The client asked for the same three-column grid as Modular Homes at every
   count, with no placeholder cards to fill the row, so the modifier is gone
   from template-parts/townhouse-grid.php and these rules with it. A single
   card now sits at a third width, which is what /modular-homes/ would do
   with one model. Restoring any hidden entry simply fills the row. */


/* ============================================================================
   MODULAR UPDATE V20 — TYPOGRAPHY, CONNECTED SCROLL, LOGO TRANSITIONS
   Reference: dvele.com. Everything above this line is the colour re-authoring;
   everything below is new behaviour.
   ============================================================================ */

/* ────────────────────────────────────────────────────────────────────────────
   1 · TYPOGRAPHY  —  ONE FAMILY  (V21)

   The reference sets its entire site in Archivo. So does this one now.
   Fraunces, Hanken Grotesk and Space Mono are gone.

   That is a bigger change than it sounds, because three faces were doing
   three jobs — display, reading, technical — and one face now has to do all
   three. It does it with weight and tracking:

     display   400–500, tracking NEGATIVE and increasingly so with size.
               Large type in a grotesque needs its counters closed up or it
               reads as a list of letters rather than a line.
     reading    400, tracking ~0, leading 1.7. Untouched register.
     technical  500–600, UPPERCASE, tracking POSITIVE and wide. This is what
               replaces the monospace: the register was never really about
               fixed advance widths, it was about the tracked-out capitals.

   Numerals get `tabular-nums` wherever they are read as data — Archivo's
   proportional figures are the default and areas in a spec table must line
   up in a column.
   ──────────────────────────────────────────────────────────────────────── */

h1,h2,h3,h4{
  font-weight:500;
  letter-spacing:-.022em;
  line-height:1.08;
  text-wrap:balance;
}
/* Tracking tightens as size grows: -.022em is right at 2rem and far too
   loose at 5rem. These are measured off the rendered line, not scaled. */
.display-xl{ font-weight:450; letter-spacing:-.036em; line-height:1.02; }
.display-l { font-weight:450; letter-spacing:-.032em; line-height:1.04; }
.display-m { font-weight:500; letter-spacing:-.026em; line-height:1.08; }
.display-s { font-weight:500; letter-spacing:-.02em;  line-height:1.16; }

/* The accent clause.
   V21 set this to `font-style:normal` on the argument that a grotesque
   italic is a slant rather than a voice. V27.1 reverses that on the client's
   instruction, and she is right on the substance as well as the call:
   "we only change the font family not the style." Swapping the typeface was
   the brief; silently dropping the italic changed the writing's emphasis,
   which was never asked for.

   It is worth saying that this is a REAL italic, not a synthesised slant —
   the Google Fonts request already asks for `ital,wght@0,300..700;1,300..700`,
   so Archivo's own drawn italic is loaded and in use. The original objection
   would have been fair against a faux-oblique; it is not against this. */
.serif-accent{ color:var(--gold-deep); font-weight:300; font-style:italic; }
.marquee-track span{ font-style:italic; font-weight:400; letter-spacing:-.01em; }

/* Body. Archivo sits comfortably at zero tracking; the small positive value
   the serif needed on a dark ground would now read as loose. */
body{ letter-spacing:0; }
p{ line-height:1.7; }
.lead{ font-weight:400; letter-spacing:-.004em; line-height:1.55; }

/* Technical register — the tracked-out capitals that replace the mono. */
.eyebrow{ letter-spacing:.16em; font-size:.68rem; font-weight:600; }
.eyebrow::before{ width:32px; }
.spec-code,.dim-rule span{ letter-spacing:.15em; font-weight:600; }
.mono,.count{ font-variant-numeric:tabular-nums; letter-spacing:.06em; }
.model-body .specs,.hero-caption,.hero-meta .k,.stat .lbl,.field label,
.filter-bar .fl-label,.spectable td:first-child,.gal-tab,.form-note,
.model-media .badge,.model-media .code,.proc-step .n,.evz-btn .n,
.evz-readout .r .k,.msz-pick .pc,.floorplan .fp-tag{
  font-weight:600; font-variant-numeric:tabular-nums;
}
.spectable td,.stat .num,.hero-meta .v,.md-hero .strip .s .v,
.evz-readout .r .val,.referral .amt{ font-variant-numeric:tabular-nums; }

/* Model and card names follow the display register. */
.model-body .name,.nc-title,.mtc-name,.feat-title,.fsc-name,
.msz-pick .pn,.evz-btn .t{ font-family:var(--display); font-weight:500; letter-spacing:-.018em; }

/* Navigation and buttons. */
.nav-sub a{ font-weight:500; letter-spacing:-.004em; }
/* V47 — the 7 top-level bar items only (not the Contact Us / Modular Home
   dropdown contents, which keep the .nav-sub treatment above unchanged).
   DVELE runs its top bar in a light weight, wide-tracked, upper case —
   architectural rather than editorial. Archivo is already loaded down to
   weight 300 site-wide (see "one family, Archivo" in functions.php), so this
   is a pure style change, no new font weight to fetch. `.nav-links > a` gets
   the 5 plain items (Home, Configurator, Our Process, Finance, About Us);
   `.nav-links > .nav-has-sub > .nav-sub-toggle` gets the 2 dropdown
   triggers (Modular Home, Contact Us) without reaching into their panels.
   V46.2 — client supplied the exact block below (font-size:.8rem;
   letter-spacing:0.1rem; font-weight:300 !important;), replacing the V47
   figures (.855rem base font-size elsewhere, .06em letter-spacing). Marked
   `!important` throughout, not just on font-weight as the client's snippet
   had it, because `.nav-links a{font-size:...}` is redeclared three more
   times further down this file for narrower viewports (1400px, 1280px) and
   without `!important` here this rule would lose the widths where those
   apply — the client asked for one flat value, not one that only holds
   above 1400px. */
.nav-links > a,
.nav-links > .nav-has-sub > .nav-sub-toggle{
  font-size:.8rem !important;
  letter-spacing:.1rem !important;
  font-weight:300 !important;
  text-transform:uppercase;
}
.btn{ letter-spacing:.09em; text-transform:uppercase; font-size:.76rem; font-weight:600; }
.btn.gold,.btn.ghost{ font-weight:600; }

/* ────────────────────────────────────────────────────────────────────────────
   2 · ONE CONNECTED SCROLL

   The brief: "each section feels connected. They don't shine individually
   per section." On the cream site every section announced itself by changing
   ground — cream, deeper cream, ink, cream. That banding is precisely the
   thing to remove.

   Two moves do it.

   (a) The alternate grounds are now within a few percent of the page ground,
       and their edges are feathered rather than cut, so a section boundary
       reads as a tonal shift rather than a seam.
   (b) The one green band per page keeps its colour but gains the same
       feathered entry and exit, so it emerges from the black instead of
       being stamped on it.
   ──────────────────────────────────────────────────────────────────────── */

.section{ isolation:isolate; }

/* Feathered section seams. A gradient the height of the section's own top
   padding, painted in the PAGE ground, dissolves the joint. Pointer-events
   are off and z-index is negative, so nothing inside a section is affected. */
.bg-paper2,.bg-paper3,.bg-ink{ position:relative; }
.bg-paper2::before,.bg-paper3::before,.bg-ink::before,
.bg-paper2::after, .bg-paper3::after, .bg-ink::after{
  content:""; position:absolute; left:0; right:0; height:clamp(48px,7vh,110px);
  pointer-events:none; z-index:0;
}
.bg-paper2::before,.bg-paper3::before,.bg-ink::before{
  top:0; background:linear-gradient(180deg, var(--ink) 0%, transparent 100%);
}
.bg-paper2::after,.bg-paper3::after,.bg-ink::after{
  bottom:0; background:linear-gradient(0deg, var(--ink) 0%, transparent 100%);
}
/* .ticked draws its corner marks in ::before/::after too. Where both apply,
   the corner ticks win — a section can have feathering or ticks, not both. */
.ticked.bg-paper2::before,.ticked.bg-paper2::after,
.ticked.bg-paper3::before,.ticked.bg-paper3::after,
.ticked.bg-ink::before,   .ticked.bg-ink::after{
  height:14px; background:none; left:auto; right:auto;
}
/* Section content sits above the feather. */
.bg-paper2 > *,.bg-paper3 > *,.bg-ink > *{ position:relative; z-index:1; }

/* The reveal is the connective tissue: one curve, one distance, everywhere,
   so a section entering the viewport behaves exactly like the last one. */
.reveal{
  opacity:0; transform:translateY(34px);
  transition:opacity 1.05s var(--ease-out), transform 1.05s var(--ease-out);
}
.reveal.in{ opacity:1; transform:none; }
.reveal.d1{ transition-delay:.10s; } .reveal.d2{ transition-delay:.20s; }
.reveal.d3{ transition-delay:.30s; } .reveal.d4{ transition-delay:.40s; }

/* Header: clear over the hero, solid once the page has moved — the reference's
   behaviour, and the reason the hero reads full-bleed. */
/* Header: clear over the hero, solid once the page has moved.

   At rest the bar carries NO fill of its own — only a short gradient scrim
   under it, so the hero photograph runs unbroken to the top of the window
   and the header reads as part of the image rather than a lid on it. The
   scrim is tied to the header height rather than clamped separately: two
   independent clamps let the ratio drift with viewport height, and at 700px
   tall the nav measured 4.4:1 over a pale slide. As a multiple it holds
   everywhere.

   The blur only switches on once the bar is solid. Blurring a transparent
   bar over a moving photo costs a full-width composite every frame for no
   visual gain. */
.site-head{
  background:transparent;
  backdrop-filter:none; -webkit-backdrop-filter:none;
  border-bottom:1px solid transparent;
  transition:background .45s var(--ease), border-color .45s var(--ease);
}
.site-head::before{
  content:""; position:absolute; left:0; right:0; top:0;
  height:calc(100% * 1.9); pointer-events:none; z-index:-1;
  background:linear-gradient(180deg, rgba(16,24,32,.48) 0%, rgba(16,24,32,.18) 56%, rgba(16,24,32,0) 100%);
  transition:opacity .45s var(--ease);
}
.site-head.scrolled{
  background:color-mix(in srgb, var(--ink) 93%, transparent);
  backdrop-filter:blur(14px) saturate(1.06);
  -webkit-backdrop-filter:blur(14px) saturate(1.06);
  border-bottom-color:var(--line-gold);
}
.site-head.scrolled::before{ opacity:0; }
/* A sticky element with a negative-z child needs its own stacking context,
   or the scrim paints behind the page rather than behind the bar. */
.site-head{ isolation:isolate; }

/* ────────────────────────────────────────────────────────────────────────────
   3 · THE MASTER MODULAR LOGO TRANSITION

   A curtain in --ink carrying the M monogram covers the outgoing page, is
   held for one beat while the browser fetches, and lifts off the incoming
   one. Total ~500ms of covered time before navigation even starts, which is
   under the threshold where a transition starts to feel like a wait.

   The curtain is a single fixed element. It cannot trap the page: it is
   removed on pageshow, on a watchdog, and on any navigation failure — see
   assets/js/mmh.js.
   ──────────────────────────────────────────────────────────────────────── */

/* The cover is painted by PSEUDO-ELEMENTS ON <html>, not by an element in
   the body, for one reason: <html> exists while the head is still parsing,
   so the incoming page can be covered BEFORE its first paint. A body
   element cannot — by the time script could append one, the page it was
   meant to hide has already flashed.

   Every animation below is `forwards` and ends in `visibility:hidden`, so
   the cover clears ITSELF with no JavaScript involved. Script only starts
   it and tidies the class away afterwards. If the script dies mid-flight,
   the page still comes back. */

/* --mmh-mark is defined by the inline <style id="mmh-v20-boot"> that
   functions.php prints at wp_head priority 3, because only PHP knows the
   theme URL. It is deliberately NOT given a default here: this stylesheet
   is enqueued at priority 8 and would otherwise overwrite the real value
   with the default every time. If the inline block is ever missing the
   background simply does not resolve, and the cover is a plain ground. */

html.mmh-leaving::before,html.mmh-arriving::before,html.mmh-introbg::before{
  content:""; position:fixed; inset:0; z-index:2000;
  background:var(--ink); pointer-events:none;
}
/* V26 — THE FULL LOCKUP, NOT THE MONOGRAM.
   Client: "the introduction logo should be using not just the favicon but the
   main logo… make sure this introduction logo transition when you first load
   the website will be the same introduction logo and style between each page."

   So --mmh-mark now resolves to the reversed horizontal lockup (1534×363) and
   the box is sized to that ratio, at a width that puts it at the same optical
   weight the monogram had. The first-load intro uses the identical file at the
   identical width — they are now one design in two places rather than two
   designs that happened to sit in the same spot. */
html.mmh-leaving::after,html.mmh-arriving::after{
  content:""; position:fixed; z-index:2001; left:50%; top:50%;
  width:clamp(210px,26vw,360px); aspect-ratio:1534 / 363;
  background:var(--mmh-mark) center/contain no-repeat;
  pointer-events:none;
  /* V21 — THE GLITCH. This centring transform was missing, and only two of
     the four keyframe sets happened to supply one of their own. So on the
     ARRIVING page (mmhMarkHold, which sets no transform) the monogram sat
     half its own width right and half its height low, then SNAPPED to the
     centre the instant .mmh-lift swapped in mmhMarkOut — which does start
     from translate(-50%,-50%). A jump, every single navigation.
     The transform lives here now and every keyframe below is written
     relative to it, so the mark cannot move between states. */
  transform:translate(-50%,-50%);
  will-change:opacity, transform;
}
/* Leaving — cover, and stay covered until the document is replaced. */
html.mmh-leaving::before{ animation:mmhCoverIn var(--mmh-tr-out) var(--ease) both; }
html.mmh-leaving::after{  animation:mmhMarkIn  var(--mmh-tr-out) var(--ease-out) both; }
/* Arriving — hold on the mark until the document is ready, THEN lift. The
   hold is what makes the monogram appear to persist across the navigation
   instead of blinking twice, and it is the whole point of the effect.

   Two states, deliberately:

     .mmh-arriving        put up in the head, before the first paint. It
                          holds, and carries a 3s self-clearing failsafe so
                          a page whose script never runs still comes back.
     .mmh-arriving.mmh-lift   added on DOMContentLoaded. This is the real
                          lift, and it overrides the failsafe animation.

   Tying the lift to DOMContentLoaded rather than to a fixed delay from the
   head is what stops a slow page uncovering itself half-built. */
html.mmh-arriving::before{ animation:mmhCoverHold 3s linear both; }
html.mmh-arriving::after{  animation:mmhMarkHold  3s linear both; }
/* V27 — --mmh-tr-lift, not --mmh-tr-in, and --ease-in-out, not --ease-out:
   the intro's curtain is 750ms on cubic-bezier(.65,0,.35,1) and this has to be
   the same motion or "the same transition" is only approximately true. */
html.mmh-arriving.mmh-lift::before{ animation:mmhCoverOut var(--mmh-tr-lift) var(--mmh-tr-hold) var(--ease-in-out) both; }
html.mmh-arriving.mmh-lift::after{  animation:mmhMarkOut  var(--mmh-tr-lift) var(--mmh-tr-hold) var(--ease-in-out) both; }

/* Every mark keyframe now carries the SAME translate(-50%,-50%) the static
   rule sets, and varies only scale. Nothing can shift position between the
   leaving page and the arriving one — which is the whole illusion. */
@keyframes mmhCoverHold{ 0%,88%{ opacity:1; visibility:visible } 99%{ opacity:0 } 100%{ opacity:0; visibility:hidden } }
@keyframes mmhMarkHold { 0%,88%{ opacity:1; visibility:visible; transform:translate(-50%,-50%) scale(1) }
                         99%   { opacity:0; transform:translate(-50%,-50%) scale(1) }
                         100%  { opacity:0; visibility:hidden; transform:translate(-50%,-50%) scale(1) } }

@keyframes mmhCoverIn { from{ opacity:0 } to{ opacity:1 } }
@keyframes mmhMarkIn  { from{ opacity:0; transform:translate(-50%,-50%) scale(.965) }
                        to  { opacity:1; transform:translate(-50%,-50%) scale(1) } }
/* V27 — THE ARRIVAL EXITS BY LIFTING, EXACTLY LIKE THE FIRST-VISIT INTRO.
   Client: "there's an upward exit of the introduction logo when you first
   visit the website. We should apply that to all logo introduction when
   browsing other pages."

   It was a fade. The intro is a lift, and the difference mattered more than
   it sounds: a fade dissolves the cover and reveals a page that was always
   there, while a lift moves the cover off and reveals a page ARRIVING behind
   it. Two different claims about what just happened, one on first visit and a
   different one on every navigation after it.

   The cover now travels its own full height upward over the same 750ms and
   the same --ease-in-out curve the intro uses, so the two are one gesture.

   THE MARK FADES WHILE IT TRAVELS, and that is not decoration. The intro's
   own note records why: the stage fades as the curtain lifts "so the mark
   never smears" — without it the lockup crosses a full screen height at full
   opacity and the eye tracks it the whole way, which turns a reveal into a
   distraction. Here the mark is a sibling of the cover rather than a child of
   it, so it has to be given the travel explicitly; it is gone by 45% and
   coasts the rest of the way invisible.

   Note the transform order: every mark keyframe writes translate(-50%,-50%)
   FIRST and the travel second, matching the static rule. That ordering is the
   V21 no-jump guarantee and must not be reversed. */
@keyframes mmhCoverOut{
  from{ transform:translateY(0);     visibility:visible }
  99% { transform:translateY(-100%) }
  to  { transform:translateY(-100%); visibility:hidden }
}
@keyframes mmhMarkOut {
  from{ opacity:1; visibility:visible; transform:translate(-50%,-50%) translateY(0) }
  45% { opacity:0;                     transform:translate(-50%,-50%) translateY(-42vh) }
  to  { opacity:0; visibility:hidden;  transform:translate(-50%,-50%) translateY(-100vh) }
}

/* ── The first-visit cinematic intro ──────────────────────────────────────
   Same mark, same ground, one beat longer, and only once per session.

   The ground is put up in the head by html.mmh-introbg, exactly like the
   arrival cover and for exactly the same reason. The richer sequence —
   mark, rule, wordmark — is then built inside a real element once the body
   exists, and paints on top of that ground with no seam. mmhIntroFailsafe
   clears the head-level ground on its own after 3s if that never happens. */

html.mmh-introbg::before{ animation:mmhIntroFailsafe 1ms 3000ms both; }
@keyframes mmhIntroFailsafe{ to{ opacity:0; visibility:hidden } }

/* ── THE CINEMATIC INTRO — V24, replicated from the reference build ────────
   modualar-granny-flat.pages.dev / src/components/home/CinematicIntro.astro
   and src/scripts/intro.ts. The client asked twice for "the same transition",
   so this is not an interpretation of it — the structure, the proportions and
   every duration below are the reference's own values.

   THE STRUCTURAL DIFFERENCE, which is what made the two look unalike: the
   reference centres the whole lockup as one GROUP — a grid stack of mark over
   rule with a gap between them. This build centred the MARK and hung the rule
   and the wordmark off fixed offsets below it, so the group's optical centre
   sat high and the spacing drifted with the viewport. It is a stack now.

     mark   clamp(74px, 11vw, 118px)   opacity + transform 700ms ease-out
     rule   clamp(56px,  8vw,  92px)   scaleX 520ms ease-out, 380ms delay
     word                              opacity 420ms ease-out, 620ms delay
     gap    clamp(1.1rem, 2.6vh, 1.7rem)

   Timeline (from intro.ts): lift at 1250ms, curtain travels 750ms on
   ease-in-out, stage fades over 400ms so the mark never smears, removed at
   2000ms. Any key or click skips.

   The wordmark is ours — the reference has no equivalent — and it is the
   third item in the same stack rather than an absolutely positioned one, so
   it inherits the group's centring. */
.mmh-intro{
  position:fixed; inset:0; z-index:2100;
  display:grid; place-items:center;
  background:var(--ink);
  transition:transform 750ms var(--ease-in-out);
  will-change:transform;
}
.mmh-intro-stage{
  display:grid; justify-items:center;
  gap:clamp(1.1rem,2.6vh,1.7rem);
  transition:opacity 400ms var(--ease);
}
/* V26 — the full lockup, at exactly the width the page cover uses, so the
   first-load intro and every page-to-page cover are the same picture at the
   same size. The rule and the typeset name are gone: the lockup carries its
   own wordmark, so the rule separated nothing and the name repeated itself.
   Their rules are kept as no-ops rather than deleted, because the elements
   are no longer built and a stray selector is cheaper than a stale one. */
.mmh-intro-mark{
  width:clamp(210px,26vw,360px); height:auto; display:block;
  opacity:0; transform:scale(.955) translateY(8px);
  transition:opacity 700ms var(--ease-out), transform 700ms var(--ease-out);
}
.mmh-intro-rule,
.mmh-intro-word{ display:none; }

/* Step 1 — in */
.mmh-intro--in .mmh-intro-mark{ opacity:1; transform:none; }

/* Step 2 — lift. The curtain RISES rather than fading, which is what makes
   the hero feel like it was always there behind it. */
.mmh-intro--lift{ transform:translateY(-100%); }
.mmh-intro--lift .mmh-intro-stage{ opacity:0; }

/* Skip / fail-safe exit — quick and quiet, never a lift. */
.mmh-intro--out{
  transform:none; opacity:0; pointer-events:none;
  transition:opacity 200ms var(--ease);
}

/* The page waits behind the curtain and arrives with it. */
html.mmh-intro-lock,html.mmh-intro-lock body{ overflow:hidden; }

/* Reduced motion: no cover, no intro, no exceptions. Navigation is a plain
   navigation and the homepage starts on the hero. */
@media (prefers-reduced-motion:reduce){
  html.mmh-leaving::before,html.mmh-leaving::after,
  html.mmh-arriving::before,html.mmh-arriving::after,
  html.mmh-introbg::before{ display:none !important; }
  .mmh-intro{ display:none !important; }
}

/* ────────────────────────────────────────────────────────────────────────────
   4 · THE NEW LOCKUP

   The supplied artwork is a wider lockup than the one it replaces (4.2:1
   against 2.2:1) because it carries SMRT BUILT under the wordmark. Sized by
   HEIGHT everywhere so the wordmark keeps a constant cap height across the
   header, the footer and the transition, and the extra width is absorbed by
   the layout rather than by shrinking the type.
   ──────────────────────────────────────────────────────────────────────── */

.brand-logo{ height:clamp(38px,4.1vw,52px); width:auto; }
@media (max-width:620px){ .brand-logo{ height:36px; } }
.foot-logo{ max-width:none; width:auto; height:clamp(52px,6vw,68px); }
@media (max-width:620px){ .foot-logo{ height:56px; margin-inline:auto; } }
/* The reverse lockup already knocks its ink to warm white — the old
   brightness/invert filter would flatten the gold and the SMRT blue with it. */
.foot-logo--tint{ filter:none; }
.brand .mark{ width:32px; height:auto; }

/* ────────────────────────────────────────────────────────────────────────────
   5 · DARK-GROUND CORRECTIONS FOUND WHILE VERIFYING
   ──────────────────────────────────────────────────────────────────────── */

/* Photography carries its own light. On a black page a photo with no frame
   floats; a hairline gives every image the same edge the cards have. */
.model-media,.shot-frame,.split-media,.gal-item,.life-media,
.company-visual,.post-media,.map-embed{ border-radius:2px; }

/* Native form controls: the OS paints select/option lists from the page
   colours, and an unstyled <option> renders black-on-black in Chrome on a
   dark <select>. Both need stating explicitly.

   V59 — client: "Fix the select your state dropdown text on all forms.
   Currently, it's not visible and readable." The CLOSED control was never the
   problem — measured 12.3:1 on the model/contact/finance forms and 19.6:1 on
   the homepage, which is fine. What the browser paints is the OPEN popup, and
   that is drawn by the platform, not by this stylesheet.

   Two things were making that popup unreadable, and both are fixed:

   1 · The placeholder was `<option value="" disabled selected>`. A DISABLED
       option is dimmed by the browser toward its greyed-text colour, and it
       does that on top of whatever colour is set here — on a dark popup the
       result is close to invisible. `disabled` is now gone from every
       placeholder (homepage, enquiry form model + state, brochure gate).
       Nothing is lost by that: each of those selects is `required` with the
       placeholder carrying `value=""`, so an unselected form is still
       rejected by the browser's own validation. Verified, not assumed.
   2 · The two colours are stated together below. Setting only one of them is
       what produces an invisible list: if the platform honours the text
       colour but paints its own light background, near-white text lands on
       white. Background and colour are declared as a pair, and
       `color-scheme:dark` tells the browser which way to paint the chrome it
       owns.

   `option:disabled` is still given an explicit, legible colour for any other
   disabled option, rather than leaving it to the platform's dimming. */
.field select,.cfg-select{ color-scheme:dark; }
.field select option,.cfg-select option{ background:#16212C; color:#F4F1EA; }
.field select option:disabled,.cfg-select option:disabled{ color:#9FB0BF; }
.field select option:checked,.cfg-select option:checked{ background:#20303F; color:#FFFFFF; }

/* Placeholders inherit the input colour at 100% on some engines. */
.field ::placeholder{ color:var(--muted-2); opacity:1; }

/* The reading-progress ring and the scroll-to-top mark share the header's
   hairline so the fixed furniture reads as one set. */
.mmh-totop-ring .rt{ stroke:rgba(179,148,94,.28); }

/* Tables, rules and separators inside posts pick up the light hairline. */
.post-content tbody tr:nth-child(even){ background:rgba(179,148,94,.07); }
.post-content mark{ background:rgba(179,148,94,.34); color:var(--paper); }

/* The SMRT partner logo is the client's blue file and is never recoloured.
   V21: the light plate is REMOVED at the client's explicit instruction — the
   lockup sits directly on the black ground, no background of any kind. See
   the V21 block at the foot of this file for the measurement that goes with
   that decision. */
.partner-logo,.partner-card-logo{
  background:none; padding:0; border-radius:0;
}
.partner-logo{ display:inline-block; }

/* Focus is gold on every ground and must clear the new dark surfaces. */
:where(a,button,input,textarea,select,summary):focus-visible{
  outline:2px solid var(--gold-hi); outline-offset:3px;
}


/* ============================================================================
   MODULAR UPDATE V21 — ARCHIVO, MENU, TRANSITIONS, PARTNER LOCKUP
   Reference: dvele.com. Typography moved to a single family in the block
   headed "1 · TYPOGRAPHY — ONE FAMILY" much further up; everything below is
   the rest of this release.
   ============================================================================ */

/* ────────────────────────────────────────────────────────────────────────────
   1 · THE MENU

   Nine items now fit on the bar (Configurator joined it), so the lockup gives
   back the width. It was clamp(38px,4.1vw,52px) tall; at 1280–1440 that left
   the nav crowding the CTA. The mark keeps its proportions — only the height
   changes — and the mobile size is untouched because the bar collapses to a
   burger long before it matters.
   ──────────────────────────────────────────────────────────────────────── */
.brand-logo{ height:clamp(34px,3.3vw,46px); width:auto; }
@media (max-width:620px){ .brand-logo{ height:34px; } }
/* V47 — this is the declaration that actually wins `gap` on the header's
   flex row (3 classes beats `.nav`'s 1, see the comment up at `.nav`).
   Bumped alongside the logo/padding for the same "more spacious" ask. */
.site-head .wrap.nav{ gap:clamp(1.2rem,2.2vw,2rem); }
.nav-links{ gap:clamp(.5rem,1.05vw,1.05rem); }
.nav-links a{ font-size:.82rem; }
/* V50 · item 1 — this rule (and its font-size sibling) is itself dead code:
   a THIRD, later, unqualified `.nav-links{gap:...}` under "MODULAR UPDATE
   V26 — MENU SPACING" wins the cascade at every viewport width, all the
   time, because it carries no media query of its own to lose to one. The
   actual responsive fix lives there now, not here — see that heading. */

/* V87 — THIS GUARD IS LIFTED. Read the history before touching it again.
   V21 removed the spec line under every model name on client feedback:
   twenty-eight lines of 9px grey turned the panel into a wall of small print.
   The rule below was left as a no-op guard so a stray line could not reappear
   after an upload.
   The Gallery card now asks for that second line BACK — "Display the model
   name on the first line and the bedroom/bathroom details on a second line" —
   so the guard has to go or the card cannot be satisfied on desktop. It is
   set larger and lighter than the 9px grey that caused the original
   complaint, and it now carries the client's own wording rather than a
   generated spec string. */
.nav-mega-specs{ display:block; font-size:.72rem; color:var(--muted); letter-spacing:.01em; margin-top:.15rem; }
.nav-mega-card{ padding:.5rem .6rem; align-items:center; }
.nav-mega-name{ font-size:.85rem; font-weight:500; letter-spacing:-.012em; line-height:1.3; }
.nav-mega-thumb{ width:58px; height:40px; }
.nav-mega-grid{ gap:.1rem .9rem; }
/* With the spec line gone the panel is shorter but the names are long — most
   of them carry the bed/bath count in the name itself. A little more width
   is what stops "Lodge Classic · 1 Bedroom, 1 Bathroom" wrapping to two
   lines fourteen times over. */
.nav-mega{ width:min(780px,calc(100vw - 2rem)); }
@media (max-width:1180px){ .nav-mega{ width:min(640px,calc(100vw - 2rem)); } }

/* ── The dropdown reveal ──────────────────────────────────────────────────
   Both panels animated open and then vanished on close. `visibility` was not
   in the transition list, so it flipped to hidden on the same frame the
   pointer left — killing the fade-out entirely and making the whole thing
   read as having no transition at all.

   `visibility` is animatable as a step: give it a delay equal to the fade on
   the closed state and 0s on the open one, and the panel stays rendered for
   exactly as long as it takes to fade out. */
.nav-sub,.nav-mega{
  transition:
    opacity .26s var(--ease-out),
    transform .34s var(--ease-out),
    visibility 0s linear .34s;
}
.nav-has-sub.open .nav-sub,
.nav-has-sub:hover .nav-sub, .nav-has-sub:focus-within .nav-sub,
.nav-has-mega.open .nav-mega,
.nav-has-mega:hover .nav-mega, .nav-has-mega:focus-within .nav-mega{
  transition:
    opacity .26s var(--ease-out),
    transform .38s var(--ease-out),
    visibility 0s linear 0s;
}
/* Opening travels a little further and settles, rather than appearing 6px
   above its resting place. */
.nav-sub{ transform:translateX(-50%) translateY(-8px) scale(.985); transform-origin:top center; }
.nav-mega{ transform:translateX(-50%) translateY(-10px) scale(.985); transform-origin:top center; }
.nav-has-sub.open .nav-sub,
.nav-has-sub:hover .nav-sub, .nav-has-sub:focus-within .nav-sub{
  transform:translateX(-50%) translateY(0) scale(1);
}
.nav-has-mega.open .nav-mega,
.nav-has-mega:hover .nav-mega, .nav-has-mega:focus-within .nav-mega{
  transform:translateX(-50%) translateY(0) scale(1);
}
@media (max-width:1500px){
  .nav-mega{ transform:translateX(0) translateY(-10px) scale(.985); transform-origin:top left; }
  .nav-has-mega.open .nav-mega,
  .nav-has-mega:hover .nav-mega, .nav-has-mega:focus-within .nav-mega{
    transform:translateX(0) translateY(0) scale(1);
  }
}
/* The cards arrive with the panel rather than being in it already. One
   short, shared stagger — six steps, then everything after step six rides
   the last one, so a long list never turns into a queue. */
.nav-mega-card{ opacity:0; transform:translateY(6px); transition:opacity .3s var(--ease-out), transform .3s var(--ease-out), background .18s var(--ease); }
.nav-has-mega.open .nav-mega .nav-mega-card,
.nav-has-mega:hover .nav-mega .nav-mega-card,
.nav-has-mega:focus-within .nav-mega .nav-mega-card{ opacity:1; transform:none; }
.nav-mega-card:nth-child(1),.nav-mega-card:nth-child(2){ transition-delay:.03s; }
.nav-mega-card:nth-child(3),.nav-mega-card:nth-child(4){ transition-delay:.06s; }
.nav-mega-card:nth-child(5),.nav-mega-card:nth-child(6){ transition-delay:.09s; }
.nav-mega-card:nth-child(n+7){ transition-delay:.12s; }
@media (prefers-reduced-motion:reduce){
  .nav-sub,.nav-mega,.nav-mega-card{ transition:none !important; transform:none !important; }
  .nav-sub,.nav-mega{ transform:translateX(-50%) !important; }
}

/* ────────────────────────────────────────────────────────────────────────────
   2 · THE GOOGLE REVIEWS BADGE

   Trustindex renders its own markup with its own colours baked in, all of
   them chosen for a light page: "EXCELLENT" and "Based on N reviews" came
   out near-black on the black ground and were, in the client's words, not
   visible. We do not control that markup and it changes between plugin
   versions, so the override is deliberately broad — inherit the page's text
   colour everywhere inside the wrapper, then hand the few things that need a
   different colour back individually.

   Images and SVG are untouched: the Google wordmark is a coloured asset and
   reads correctly on black as it is.
   ──────────────────────────────────────────────────────────────────────── */
/* ── V23: STOP FIGHTING THE PLUGIN. GIVE IT THE GROUND IT WAS BUILT FOR ────
   Third attempt, and the last, because the first two were the same mistake in
   two directions.

   V21 lightened the text and missed the card backgrounds  → white on white.
   V22 widened the background reset to `[class*="ti-"]`     → STILL white on
   white, because the element carrying the white card does not have a `ti-`
   class at all, while the elements carrying the text do. Half the widget took
   the override and half did not, which is worse than neither.

   The lesson is not "find the right selector". It is that this markup is not
   ours, it is not stable, and it is not fully knowable from outside — so any
   override that has to MATCH it correctly to be safe is the wrong shape of
   solution.

   So nothing is overridden. The widget renders exactly as Trustindex ships it
   — its own dark text on its own white cards — and it gets the light ground it
   was designed for. One declaration, no class names, nothing to drift, and it
   cannot break on a plugin update because it does not touch the plugin.

   This is the same reasoning as the SMRT lockup: when you cannot restyle
   foreign artwork, give it the ground it was made for. */
.trustindex-wrap{
  background:var(--paper);
  border-radius:4px;
  padding:clamp(20px,2.6vw,38px) clamp(16px,2.2vw,32px);
  box-shadow:0 30px 70px -40px rgba(0,0,0,.9);
}
/* The plate is a light island on a dark page, so it needs its own quiet
   boundary rather than floating. */
.trustindex-wrap{ border:1px solid rgba(16,24,32,.10); }

/* V89 — the review plate is the ONE block on these pages whose contents are
   written by a third party (Trustindex). Measured at 360px the placeholder card
   row runs 270px past the viewport and takes the whole document into horizontal
   scroll with it — every other section on the page ends at 0px overflow.

   The widget's own stylesheet may well handle this on the live site; this
   contains it either way, and containment is the point. `overflow-x:auto` keeps
   the row scrollable INSIDE the plate instead of clipping it, so nothing the
   widget renders becomes unreachable, and `max-width:100%` stops a wide child
   from stretching the plate itself. A widget that already fits sees no change.
   Same containment pattern the tables and code blocks use elsewhere. */
.trustindex-wrap{ max-width:100%; overflow-x:auto; }

/* Two things only, and both are OUTSIDE the plugin's own cards: the section
   heading above it stays on the page ground, and the plate must not inherit
   the page's light body colour into any text the plugin did not colour
   itself. Anything the plugin does not set now falls back to ink, not paper. */
.trustindex-wrap{ color:var(--ink); }

/* ────────────────────────────────────────────────────────────────────────────
   3 · THE SMRT BUILT LOCKUP, WITH NO BACKGROUND

   Client instruction, given twice and now explicit: no background. The plate
   is gone (see .partner-logo above).

   The measurement that goes with it, recorded rather than argued: the file's
   blue is #1A47B0. On #101820 that is 1.79:1. WCAG asks 3:1 for a graphic
   that must be distinguished. It is under that, and it is a considered
   client decision about their own partner's mark, not an oversight.

   What IS done: the lockup gets a little more size, because it is now
   carrying itself with no plate to define it, and a soft gold hairline
   underline appears on hover so it still reads as a link.
   ──────────────────────────────────────────────────────────────────────── */
.partner-logo img,.partner-card-logo img{ height:clamp(26px,2.6vw,34px); }
@media (max-width:620px){ .partner-logo img,.partner-card-logo img{ height:30px; } }
.partner-logo:hover,.partner-card-logo:hover{ opacity:1; }
.partner-logo img,.partner-card-logo img{ transition:filter .3s var(--ease), opacity .3s var(--ease); }
.partner-logo:hover img,.partner-card-logo:hover img{
  filter:brightness(1.28) saturate(1.06);
}

/* ────────────────────────────────────────────────────────────────────────────
   4 · SCROLL AND SECTION BEHAVIOUR

   "Very quick, smooth and premium… between each page and each section."

   The reveal was 1.05s over 34px, which at the top of a fast scroll means a
   section is still arriving when the next one starts. Shorter and closer:
   .72s over 22px reads as the page settling rather than as an animation
   playing. The curve is unchanged — one curve everywhere is what makes the
   whole scroll feel like one object.
   ──────────────────────────────────────────────────────────────────────── */
.reveal{
  transform:translateY(22px);
  transition:opacity .72s var(--ease-out), transform .72s var(--ease-out);
}
.reveal.d1{ transition-delay:.07s; } .reveal.d2{ transition-delay:.14s; }
.reveal.d3{ transition-delay:.21s; } .reveal.d4{ transition-delay:.28s; }

/* Anchor scrolling clears the sticky bar with the bar's own height, not a
   number that was right when it was written. */
html{ scroll-padding-top:clamp(84px,9vh,112px); }

/* Whitespace. The reference breathes more than this did between a section's
   heading and its content, and less between the sections themselves — which
   is what stops a long page reading as a stack of slides. */
.sec-head{ margin-bottom:2.6rem; }
.sec-head .eyebrow + h2,.sec-head .eyebrow + h3{ margin-top:1.1rem; }
.sec-head h2 + .lead,.sec-head h2 + p,.sec-head h3 + p{ margin-top:1.1rem; }

/* ────────────────────────────────────────────────────────────────────────────
   5 · THE CONFIGURATOR PAGE
   ──────────────────────────────────────────────────────────────────────── */
.cfg-page-lede{ max-width:62ch; margin-bottom:2.8rem; }
.cfg-page-lede p{ font-size:clamp(1.02rem,1.15vw,1.16rem); }

/* ── The drawer's model list folds (V21) ─────────────────────────────────
   Fourteen model links open by default pushed Townhouses, Configurator and
   Contact roughly two screens down the drawer. */
.drawer .d-fold{ border-bottom:1px solid var(--line-soft); }
.drawer .d-fold > summary{
  display:flex; align-items:center; gap:.5rem; cursor:pointer; list-style:none;
  padding:.85rem 0 .85rem 2.6rem; font-size:.95rem; font-weight:600;
  letter-spacing:.1em; text-transform:uppercase; color:var(--gold-deep);
}
.drawer .d-fold > summary::-webkit-details-marker{ display:none; }
.drawer .d-fold > summary::after{
  content:""; margin-left:auto; width:.5em; height:.5em;
  border-right:1.6px solid currentColor; border-bottom:1.6px solid currentColor;
  transform:rotate(45deg) translateY(-2px); transition:transform .25s var(--ease);
}
.drawer .d-fold[open] > summary::after{ transform:rotate(225deg) translateY(-1px); }
.drawer .d-fold > summary .num{ display:none; }
.drawer .d-fold .d-sublink{ padding-left:2.6rem; }


/* ============================================================================
   MODULAR UPDATE V22 — MENU PARITY, WIDTH, RHYTHM, SECTION MOTION
   ============================================================================ */

/* ────────────────────────────────────────────────────────────────────────────
   1 · THE TWO DROPDOWNS NOW BEHAVE IDENTICALLY

   Client: "the dropdown hover and transition are different for both parent
   menu." They were, in three separate ways, and only one of them was
   deliberate:

   (a) `.nav-sub a` sits inside `.nav-links`, so it inherited
       `.nav-links a::after` — the gold underline that grows on hover. The
       mega-menu cards had that underline explicitly switched off in V18.1;
       the Contact links never did. That is the gold line under "Perth /
       Swan Valley Showroom" in the client's screenshot, and the mega-menu
       has no equivalent.
   (b) The hover fills were different colours: the sub-menu used
       --wash-gold with gold text, the mega card a flat --wash-gold with no
       colour change.
   (c) The panels opened from different origins — the mega-menu from its
       left edge below 1500px, the Contact panel always centred.

   All three are unified below. One hover treatment, one curve, one origin
   rule: each panel opens from the corner nearest its own parent item.
   ──────────────────────────────────────────────────────────────────────── */

/* (a) No inherited underline inside either panel. */
.nav-links .nav-sub a::after,
.nav-links .nav-mega-card::after,
.nav-links .nav-mega-foot::after{ display:none !important; }

/* (b) One hover treatment for every row in either panel. */
.nav-sub a,
.nav-mega-card{
  border-radius:8px;
  transition:background-color .2s var(--ease), color .2s var(--ease);
}
.nav-sub a{ padding:.62rem .7rem; }
.nav-sub a:hover, .nav-sub a:focus-visible,
.nav-mega-card:hover, .nav-mega-card:focus-visible{
  background-color:rgba(244,241,234,.06);
  color:var(--paper);
}
.nav-sub a:hover .nav-mega-name,
.nav-mega-card:hover .nav-mega-name{ color:var(--paper); }
/* The row you are on gets a gold edge — the one piece of the old Contact
   treatment worth keeping, now applied to both. */
.nav-sub a,
.nav-mega-card{ box-shadow:inset 2px 0 0 0 transparent; }
.nav-sub a:hover, .nav-sub a:focus-visible,
.nav-mega-card:hover, .nav-mega-card:focus-visible{
  box-shadow:inset 2px 0 0 0 var(--gold);
}

/* (c) One origin rule: each panel opens from the corner nearest its parent.
   Anchoring both to the same side is what pushed one of them off-screen and
   forced the 1500px exception that made them differ in the first place. */
.nav-has-sub .nav-sub,
.nav-has-mega .nav-mega{
  left:0; right:auto;
  transform:translateY(-10px) scale(.985);
  transform-origin:top left;
}
.nav-has-sub.open .nav-sub,
.nav-has-sub:hover .nav-sub, .nav-has-sub:focus-within .nav-sub,
.nav-has-mega.open .nav-mega,
.nav-has-mega:hover .nav-mega, .nav-has-mega:focus-within .nav-mega{
  transform:translateY(0) scale(1);
}
/* Items in the right-hand half open from their right edge instead, so a
   panel can never leave the window. Contact is the last item on the bar. */
.nav-links > .nav-has-sub:last-child .nav-sub{
  left:auto; right:0; transform-origin:top right;
}
@media (max-width:1500px){
  .nav-has-mega .nav-mega{ left:0; right:auto; transform-origin:top left; }
}

/* One curve and one duration for both, open and closed. `visibility` keeps
   the step-delay that gives the close its fade (see V21). */
.nav-sub,.nav-mega{
  transition:
    opacity .24s var(--ease-out),
    transform .34s var(--ease-out),
    visibility 0s linear .34s;
}
.nav-has-sub.open .nav-sub,
.nav-has-sub:hover .nav-sub, .nav-has-sub:focus-within .nav-sub,
.nav-has-mega.open .nav-mega,
.nav-has-mega:hover .nav-mega, .nav-has-mega:focus-within .nav-mega{
  transition:
    opacity .24s var(--ease-out),
    transform .38s var(--ease-out),
    visibility 0s linear 0s;
}
/* The Contact panel gets the same arrival stagger the mega cards have, so
   the two read as the same component at different sizes. */
.nav-sub a{ opacity:0; transform:translateY(5px);
  transition:opacity .26s var(--ease-out), transform .26s var(--ease-out),
             background-color .2s var(--ease), color .2s var(--ease); }
.nav-has-sub.open .nav-sub a,
.nav-has-sub:hover .nav-sub a,
.nav-has-sub:focus-within .nav-sub a{ opacity:1; transform:none; }
.nav-sub a:nth-child(1){ transition-delay:.03s; }
.nav-sub a:nth-child(2){ transition-delay:.06s; }
.nav-sub a:nth-child(n+3){ transition-delay:.09s; }
@media (prefers-reduced-motion:reduce){
  .nav-sub a{ opacity:1 !important; transform:none !important; transition:none !important; }
}

/* Both parent links carry the same caret and the same underline. */
.nav-sub-toggle{ position:relative; }

/* ────────────────────────────────────────────────────────────────────────────
   2 · RHYTHM AND WHITESPACE

   The section token is retuned at the top of this file. What is left is the
   spacing INSIDE a section, which had grown independently: several sections
   set their own margins inline and ended up further apart from their own
   headings than the sections were from each other.
   ──────────────────────────────────────────────────────────────────────── */
/* V46.2 — twin of the rule retired at the top of this file (search "the
   correct spacing on each section is the showroom section"): this was
   `.tight`'s own reduced figure for the sub-1400px breakpoint. Removed for
   the same reason — `.section{padding-block:var(--sp)}` now sets it alone,
   at every breakpoint, for tight and plain sections alike. */
.sec-head{ margin-bottom:clamp(1.9rem,3.2vw,2.6rem); }
.collection-head{ margin-bottom:clamp(2rem,3.4vw,2.8rem) !important; }

/* The first section after a hero sits closer to it — the hero already has
   its own bottom breathing room built into the image. */
.hero + .section,
.page-hero-band + .hero-scroll-target + .section{ padding-top:clamp(48px,6vh,96px); }

/* Tablet and phone: the gap between sections should shrink faster than the
   type does, or a short section reads as an island. */
@media (max-width:900px){
  :root{ --sp:clamp(48px,6vh,84px); }
  .sec-head{ margin-bottom:1.7rem; }
}
@media (max-width:600px){
  :root{ --sp:clamp(42px,5.4vh,68px); }
  .sec-head{ margin-bottom:1.5rem; }
}

/* ────────────────────────────────────────────────────────────────────────────
   3 · SECTION MOTION — the reference's depth on entry

   The reference does not slide sections up from below; each one arrives with
   a small scale and a soft rise, so it reads as coming FORWARD rather than
   travelling upward. That is the whole difference between "an animation
   played" and "the page has depth".

   Distance stays short (the V21 retune) and the scale is deliberately tiny —
   1.5% is felt rather than seen. Anything larger reads as a zoom.
   ──────────────────────────────────────────────────────────────────────── */
.reveal{
  transform:translateY(20px) scale(.985);
  transform-origin:center 65%;
  will-change:opacity, transform;
}
.reveal.in{ transform:none; will-change:auto; }

/* Media inside a revealing block settles a beat behind its own text, which
   is what gives the entry its layered feel. */
.reveal .photo,
.reveal img:not(.brand-logo):not(.partner-logo img){
  transition:transform .9s var(--ease-out);
}
@media (prefers-reduced-motion:reduce){
  .reveal,.reveal.in{ transform:none !important; transition:none !important; opacity:1 !important; }
}

/* ────────────────────────────────────────────────────────────────────────────
   4 · THE HEADER COMPACTS AS IT SOLIDIFIES

   The other half of the ported behaviour. On the reference build the bar does
   not simply gain a background — it draws itself in: the scrim goes, the
   contents lift a few pixels and the lockup gives back a little height. The
   two moves together are what read as "the header settled", where the colour
   change alone reads as a flash.
   ──────────────────────────────────────────────────────────────────────── */
/* V47 — "more premium, spacious" per the client's DVELE-reference ask.
   Both figures bumped from clamp(10px,1.4vh,18px) / clamp(6px,.9vh,11px);
   the ratio between resting and scrolled state is kept the same so the
   settle-in motion reads the same as before, just from a taller start. */
.site-head .wrap.nav{
  transition:padding-block .42s var(--ease);
  padding-block:clamp(16px,1.8vh,26px);
}
.site-head.scrolled .wrap.nav{ padding-block:clamp(8px,1vh,16px); }
.brand-logo{ transition:height .42s var(--ease); }
.site-head.scrolled .brand-logo{ height:clamp(30px,2.8vw,38px); }
@media (max-width:620px){ .site-head.scrolled .brand-logo{ height:30px; } }
@media (prefers-reduced-motion:reduce){
  .site-head .wrap.nav,.brand-logo{ transition:none; }
}

/* ────────────────────────────────────────────────────────────────────────────
   5 · THE INTRO CURTAIN FADES ITS STAGE AS IT LIFTS

   Straight from the reference build's CinematicIntro: "Fades slightly as the
   curtain lifts so the mark never smears." Without it the monogram travels a
   full viewport height at full opacity and the eye tracks it the whole way,
   which is exactly the thing that reads as cheap.
   ──────────────────────────────────────────────────────────────────────── */
/* V24 — superseded. The stage fades as one group now (see
   .mmh-intro--lift .mmh-intro-stage), which is how the reference does it.
   Fading the three children individually left two mechanisms fighting over
   the same opacity. */

/* ────────────────────────────────────────────────────────────────────────────
   6 · DEPTH ON THE FULL-BLEED PHOTOGRAPHY

   The reference's "3D" feel is not a 3D transform — it is that the large
   photographs are not locked to the page. They settle as they arrive: a
   full-bleed image enters very slightly over-scaled and eases to rest as it
   crosses the viewport, so the picture reads as sitting BEHIND the page
   rather than printed on it.

   Done with a scroll-driven animation (`animation-timeline: view()`), which
   runs entirely on the compositor — no scroll listener, no JavaScript, and
   nothing to go wrong on a slow device. Guarded by @supports, so a browser
   without it simply gets a still photograph, which is the current behaviour
   and perfectly fine.

   The scale is 3%. Enough to feel, not enough to soften the image or to be
   noticed as an effect.
   ──────────────────────────────────────────────────────────────────────── */
/* V24 — this block declared @keyframes mmhSettle a SECOND time (at 1.03),
   duplicating the V23 declaration further down (at 1.045). Keyframe names are
   global and the later one wins, so this one was dead code pretending to be
   the setting. It has been removed; §3 of the V23 block below is the single
   definition. Grep for a @keyframes name before declaring one. */

/* ── V23: align the delivery note's heading with the section heading ───────
   The grid is `align-items:start`, so the note's BOX aligned with the
   eyebrow — which put "Manufactured in 12–14 weeks." about 35px above
   "Different Designs, One Modular System". The client asked for the two
   headings to line up, so the note drops by exactly the height of the
   eyebrow row plus the flex gap that follows it. Expressed in the same
   units those two things use, so it stays aligned if either changes. */
.collection-head > .delivery-note{
  /* The residual 9px is the difference between the two headings’ half-leading
     (h2 runs line-height 1.08, the note 1.25). Measured, not guessed. */
  margin-top:calc(.68rem * 1.6 + 1.1rem - 9px);
}
@media (max-width:900px){
  .collection-head > .delivery-note{ margin-top:0; }
}


/* ============================================================================
   MODULAR UPDATE V23 — MASONRY GALLERY, LIGHTBOX, GREEN AS GROUND
   ============================================================================ */

/* ────────────────────────────────────────────────────────────────────────────
   1 · THE MODEL GALLERY IS A MASONRY

   It was a two-column grid in which every third image spanned the full width
   at its own aspect ratio. On a model with a dozen photographs that is several
   screens of gallery — the client's "it's too large".

   CSS columns rather than a grid, because a grid cannot pack items of
   different heights without either cropping them or leaving gaps, and the
   client asked specifically that nothing be cut off. Every image keeps its
   natural height; the columns do the packing.
   ──────────────────────────────────────────────────────────────────────── */
/* ============================================================================
   V62 — THE MODEL GALLERY BECOMES THE TOWNHOUSE GALLERY
   ============================================================================

   Client, naming her own reference: "The images on this page are perfect:
   /smrt-built-townhouse/ ... but on this page, they are not cropped properly:
   /mps4/", then a list of fourteen pages reading "not cropped properly and
   picture is moving" or "cropped ok but pictures are moving".

   The two galleries were built differently, and measuring them side by side is
   what settles it rather than taste:

     townhouse  .gal-grid--uniform  aspect-ratio 4/3, object-fit COVER
                every tile identical, image fills it edge to edge
     model      .shot-grid--masonry columns:3, aspect-ratio AUTO, fit CONTAIN
                every tile its own shape

   Measured on /mps4/, the four gallery tiles came out at aspect ratios 2.189,
   1.598, 1.128 and 0.958 — better than a 2:1 spread within one grid. That
   ragged, uneven packing is what "not cropped properly" describes: the images
   are not cropped AT ALL, which is the defect, because the reference she is
   comparing against crops every one to a common shape.

   And it is also half of "the picture is moving": in a multi-column masonry
   the column heights are only known once each image has loaded, so the packing
   re-flows as you scroll and tiles below jump. A fixed tile shape removes that
   by construction — the box is reserved before the image exists.

   So the model gallery now IS the townhouse gallery: same 4/3 tile, same
   cover, same three-across, same breakpoints. Nothing is invented here; it is
   the treatment she has already approved, applied to the pages she listed.

   Cropping to a common shape is safe because nothing is lost: every frame is
   a button that opens the full uncropped image, and floor plans are not in
   this grid at all — `role => 'plan'` routes them to #plans, which keeps
   `contain` and is untouched. Verified across all fourteen models: no
   plan, elevation or configuration image is in a gallery. */
.shot-grid--masonry{
  display:grid;
  grid-template-columns:repeat(3,1fr);
  gap:clamp(14px,1.6vw,22px);
  margin-top:2.2rem;
}
.shot-grid--masonry .shot{
  margin:0;
  display:flex; flex-direction:column;
}
/* The tile shape is fixed, so it is reserved before the image loads and the
   grid cannot re-flow underneath the reader. */
.shot-grid--masonry .shot-frame{
  position:relative;
  aspect-ratio:4 / 3;
  display:block; width:100%; padding:0;
  background:var(--paper-3); border:1px solid var(--line-soft);
  cursor:zoom-in; overflow:hidden;
  transition:border-color .35s var(--ease);
}
.shot-grid--masonry .shot-frame img{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover; object-position:center;
  display:block;
}
/* V62 — the hover zoom is gone. `transform:scale(1.028)` over .7s fired on
   every image the pointer crossed, so moving the mouse down a gallery of a
   dozen photographs set them moving one after another. That is the other half
   of "pictures are moving", and it is the one the client can see standing
   still. The townhouse gallery she calls perfect has no hover transform at
   all; the gold border remains as the affordance. */
.shot-grid--masonry .shot-frame:hover{ border-color:var(--line-gold); }
/* The caption pill is a heavy object to repeat a dozen times down a column.
   In the masonry it becomes a quiet line under the picture. */
.shot-grid--masonry .pill-cap{
  background:none; border:0; border-radius:0; color:var(--muted-2);
  padding:.55rem 0 0; margin:0; letter-spacing:.1em; font-size:.6rem;
}
/* A gallery with one or two photographs must not be stretched across three
   columns — one picture in the left cell with two empty cells beside it reads
   as a page that failed to load. Narrow the grid to the number of pictures
   there actually are, and cap the width so a single image stays a picture
   rather than becoming a banner. Four models have one photograph and three
   have two; that is a CONTENT gap and is flagged to the client separately —
   this only stops the layout misrepresenting it. */
/* ⚠ V174 — THE NARROWING ABOVE IS RETIRED, AND THIS REVERSES THE PARAGRAPH
   IMMEDIATELY ABOVE IT. Bianca, 25 Sep: "PLEASE can we make sure all plans and
   design are same in design, height, width, format, AS WELL as the gallery
   section. ALL should be consistent."

   Measured across all 16 model templates before touching anything: 59 tiles at
   429x321, 10 at 457x342, 1 at 558x418. Three tile sizes, and the cause is the
   same one V173 found in the plans grid — the grid narrows to the number of
   pictures, so a page with fewer photographs shows BIGGER ones. A visitor
   moving between two model pages sees the gallery change size for no reason
   they can name.

   So the tile is now one size everywhere, exactly as the plan card is. Pages
   with two photographs fill from the left and leave the third cell empty,
   which is what makes them match the eight-photo pages.

   ⚠ WHAT THE OLD RULE WAS PROTECTING IS REAL, AND IT IS A CONTENT GAP, NOT A
   LAYOUT ONE. Its note warned that one picture beside two empty cells "reads
   as a page that failed to load". That is fair, and exactly one page is in
   that position: the Gem Grande, which has a single gallery photograph. Five
   more have two. Making the layout hide that by inflating one photograph into
   a banner does not give the client more photographs, it just makes the
   shortage harder to see. It stays flagged as a content request. */
.shot-grid--masonry.shot-grid--n1,
.shot-grid--masonry.shot-grid--n2{ grid-template-columns:repeat(3,minmax(0,1fr)); max-width:none; }

/* Same breakpoints as the townhouse grid: three across, then two, then one. */
@media (max-width:900px){
  .shot-grid--masonry,
  .shot-grid--masonry.shot-grid--n2{ grid-template-columns:repeat(2,minmax(0,1fr)); }
}
@media (max-width:560px){
  .shot-grid--masonry,
  .shot-grid--masonry.shot-grid--n1,
  .shot-grid--masonry.shot-grid--n2{ grid-template-columns:minmax(0,1fr); }
}

/* ── The lightbox ─────────────────────────────────────────────────────── */
.mmh-imglb{
  position:fixed; inset:0; z-index:2400;
  display:grid; place-items:center;
  opacity:0; transition:opacity .28s var(--ease);
}
/* `display:grid` overrides the `hidden` attribute's own display:none, so
   without this the closed lightbox stays laid out — an invisible full-viewport
   overlay swallowing every click on the page. The video lightbox above has the
   same line for the same reason. */
.mmh-imglb[hidden]{ display:none !important; }
.mmh-imglb.is-open{ opacity:1; }
.mmh-imglb-backdrop{
  position:absolute; inset:0; background:rgba(8,12,16,.93);
  backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px);
  cursor:zoom-out;
}
.mmh-imglb-dialog{
  position:relative; z-index:1;
  width:min(1280px, 94vw); max-height:92vh;
  display:grid; place-items:center;
}
.mmh-imglb-figure{ margin:0; display:grid; justify-items:center; gap:.9rem; max-height:92vh; }
.mmh-imglb-figure img{
  max-width:100%; max-height:82vh; width:auto; height:auto;
  object-fit:contain; display:block;
  box-shadow:0 40px 90px -40px rgba(0,0,0,.95);
  transform:scale(.985); transition:transform .34s var(--ease-out);
}
.mmh-imglb.is-open .mmh-imglb-figure img{ transform:none; }
.mmh-imglb-figure figcaption{
  font-family:var(--mono); font-size:.66rem; font-weight:600; letter-spacing:.14em;
  text-transform:uppercase; color:var(--muted-2); text-align:center;
}
.mmh-imglb-x{
  position:fixed; top:clamp(14px,2.4vh,26px); right:clamp(14px,2.4vw,26px);
  width:44px; height:44px; border-radius:50%;
  background:var(--ink-2); border:1px solid var(--line-ui); color:var(--paper);
  font-size:1.5rem; line-height:1; display:grid; place-items:center; z-index:2;
  transition:background-color .25s var(--ease), border-color .25s var(--ease);
}
.mmh-imglb-x:hover{ background:var(--ink-3); border-color:var(--gold); }
.mmh-imglb-nav{
  position:fixed; top:50%; transform:translateY(-50%);
  width:52px; height:52px; border-radius:50%;
  background:var(--ink-2); border:1px solid var(--line-ui); color:var(--paper);
  display:none; place-items:center; z-index:2;
  transition:background-color .25s var(--ease), border-color .25s var(--ease);
}
.mmh-imglb.has-nav .mmh-imglb-nav{ display:grid; }
.mmh-imglb-nav svg{ width:22px; height:22px; }
.mmh-imglb-nav:hover{ background:var(--ink-3); border-color:var(--gold); }
.mmh-imglb-prev{ left:clamp(10px,2vw,32px); }
.mmh-imglb-next{ right:clamp(10px,2vw,32px); }
@media (max-width:620px){
  .mmh-imglb-nav{ width:44px; height:44px; }
  .mmh-imglb-figure img{ max-height:72vh; }
}
@media (prefers-reduced-motion:reduce){
  .mmh-imglb,.mmh-imglb-figure img{ transition:none; }
}

/* ────────────────────────────────────────────────────────────────────────────
   2 · WHAT ACTUALLY HAPPENS ON EVERY SCROLL

   Client, three times now: "Notice how unique the dvele.com transition and
   effect every scroll between each section. It's not an ordinary just
   scrolling. Every scroll there's an effect."

   The previous answer was a fade-and-rise on each block. That is the ordinary
   scrolling she is describing — it happens once, when a block enters, and then
   the page is inert again. What the reference does is different in kind: the
   page is bound to scroll POSITION, so something is moving the entire time you
   are moving, in both directions, and it reverses when you scroll back.

   Everything below is a scroll-driven animation (`animation-timeline: view()`).
   That matters for three reasons: it runs on the compositor rather than the
   main thread, it is tied to position rather than to a timer so it scrubs
   backwards as well as forwards, and it costs no JavaScript at all — there is
   no scroll listener anywhere in this file.

   Guarded twice: `@supports` for browsers without it, and reduced-motion.
   Either way the page falls back to exactly what it does now.
   ──────────────────────────────────────────────────────────────────────── */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* (a) THE GROUND ARRIVES — REMOVED, AND WORTH RECORDING WHY.

       The intended effect was a cover in the page colour, painted over each
       ground-changing section and retracting upward as you scrolled into it,
       so a new ground was uncovered rather than switched on.

       It was built, and a harness check found it silently does not run:
       `animation-timeline` is not applied to ::before/::after in the Chromium
       this is verified against, so the cover sat at its `from` state —
       scaleY(1) — and the section stayed COVERED. Five of six pages had at
       least one section completely hidden behind an opaque panel, including
       one that never uncovered even at the bottom of the page.

       It is not being fixed with a real element instead. The failure mode of
       this effect is "the content is invisible", and no amount of scroll
       polish is worth a mechanism that hides the page when a browser does not
       implement one property. The three effects below all fail to "nothing
       moves", which is the only acceptable failure mode for decoration.

       Every one of them is verified as actually running, not just declared:
       the drift scrubs from +26px to -16.5px across a real scroll. */

    /* (b) CONTENT DRIFT — SUPERSEDED IN V24, AND IT CARRIED A REAL BUG.

       This block declared `@keyframes mmhDrift`. That name was ALREADY in use
       at line ~1954 for the hero grain's infinite background-position loop.
       Keyframe names are global and the later definition wins, so from V22
       the grain was silently running this block's transform keyframes instead
       of its own. Nobody would have reported it — the grain is a 2%-opacity
       texture — but it was wrong, and it is the kind of collision that only
       surfaces when a third thing depends on it.

       The drift is replaced by the depth entrance in the V24 block at the
       foot of this file, which does the same job better. The name goes back
       to the grain.

       **Grep the file for a @keyframes name before declaring one.** */

    /* (c) THE PHOTOGRAPHY SETTLES.  (V22, retained.)
       Full-bleed images enter fractionally over-scale and ease to rest, so a
       picture reads as sitting behind the page rather than printed on it. */
    /* V59 — full-bleed photographs settled in from 4.5% over-scale. That is a
       third gesture; "content" fades. */
    @keyframes mmhSettle{
      from{ opacity:0 }
      to  { opacity:1 }
    }
    .company--bg .company-bg-photo,
    .page-hero-band > .photo,
    .fsc-media img,
    .gal-item img{
      animation:mmhSettle linear both;
      animation-timeline:view();
      animation-range:cover 0% cover 42%;
      /* Same reasoning as the section tilt: no permanent layer. These are
         photographs, so the antialiasing argument does not apply, but the
         memory one does. */
    }
    .hero .photo{ animation:none; }

    /* (d) THE SECTION HEADING LEADS.
       A heading resolves its own letter-spacing as its section arrives. It is
       two hundredths of an em — invisible as an effect, unmistakable as a
       feeling that the page is settling into place around you. */
    @keyframes mmhTrack{
      from{ letter-spacing:.006em; opacity:.86 }
      to  { letter-spacing:inherit; opacity:1 }
    }
    /* V59 — a heading must do the hero's rise and nothing else; this added a
       letter-spacing and opacity settle on top of it. Retired. */
    .sec-head h2{
      animation:none;
      animation-timeline:view();
      animation-range:cover 4% cover 30%;
    }
  }
}

/* The reveal stays as the fallback and as the mechanism for cards and lists,
   which are not section grounds and are not bound to scroll position. */
@media (prefers-reduced-motion:reduce){
  .section > .wrap,.company--bg > .wrap{ animation:none !important; transform:none !important; }
}


/* ============================================================================
   MODULAR UPDATE V24 — ONE BODY REGISTER
   ============================================================================ */

/* ────────────────────────────────────────────────────────────────────────────
   Client: "check all the body font sizes and color. They should all be the
   same size and color.  Color: #d8d3c7   Size: .92rem"

   Body copy was running at three sizes and two colours: `p` at
   clamp(1rem,1.05vw,1.12rem) in --muted (#AFA99D), `.lead` at
   clamp(1.12rem,1.5vw,1.34rem) in --ink-soft (#D8D3C7), and around a dozen
   components setting their own value inline or in their own rule.

   One size, one colour, everywhere prose is read.

   V25 — REVISED ON CLIENT INSTRUCTION: "Keep the original body color / content
   color white with the size of 1rem and equal and uniform letter spacing."

   So the register is now **1rem in --paper (#F4F1EA)**, the same white the
   headings are set in, with letter-spacing explicitly normalised to zero.

   #F4F1EA rather than pure #FFFFFF on purpose: --paper IS this site's white,
   it is what every heading and lockup is already set in, and dropping body
   copy to #FFF would make the prose read *brighter* than the headings above
   it — which inverts the hierarchy rather than unifying it. If pure white is
   wanted, it is one token.

   V30 — THE CLIENT ASKED FOR THE PURE WHITE. "Can we maintain the colour
   white for the body/contents please" → --body-color is now #FFFFFF.

   The hierarchy note above was real, not theoretical, so it is answered here
   rather than left to happen: headings inside prose containers are lifted to
   #FFFFFF as well (see the heading rule below). Body copy and the
   headings above it are now the SAME white, which is what "the content is
   white" has to mean if it is not going to make headings look dimmer than the
   paragraphs they introduce.

   --paper itself is untouched. It is a structural token — plate backgrounds,
   the Trustindex ground, borders, the lockup — and repointing it at #FFF to
   solve a text-colour request would have moved a dozen surfaces nobody
   asked about.

   Letter-spacing was the quiet half of "uniform". Several components tracked
   their prose out to +.02em or in to −.01em on their own, so two paragraphs at
   the same size and colour still did not match. `letter-spacing:normal` is
   stated here so nothing below can drift.

   What is deliberately NOT included: headings, eyebrows, spec labels,
   buttons, navigation, captions and figure text. Those are not body copy;
   they are the technical and display registers, and flattening them into
   this one would erase the hierarchy the rest of the design depends on.
   ──────────────────────────────────────────────────────────────────────── */
:root{ --body-size: 1rem; --body-color: #FFFFFF; }

/* V30 — headings travel with the body copy.
   Lifting prose to #FFF while headings stayed at --paper (#F4F1EA) would leave
   every heading measurably dimmer than the paragraph beneath it. These are the
   display registers on a dark ground; they take the same white. Scoped to
   .mmh so nothing in wp-admin or a plugin's own markup is touched, and NOT
   applied inside the light-ground plates (.trustindex-wrap) or the enquiry
   panels, which set their own colours against a different background. */
/* V44 — .bgate-body joined the exclusion list. The brochure-gate heading was
   set to var(--ink) directly on .bgate-body h2 below, and this register still
   overrode it: body.mmh :is(...):not(...) is (0,2,2), the local rule was only
   (0,1,1), and this one wins the cascade regardless of source order. Same
   defect as the .trustindex-wrap carve-out three lines up, on the one other
   light card the register never learned about. Any NEW light-ground component
   needs this same exclusion, or its own heading colour is cosmetic. */
body.mmh :is(h1,h2,h3,h4,h5,h6):not(.trustindex-wrap *, .mmh-imglb *, .post-content *, .bgate-body *){
  color:#FFFFFF;
}

/* Listing the selectors that SHOULD carry the body register was the first
   attempt and it missed 132 of 342 elements, because a dozen components set
   their own size or colour at a higher specificity than a list of tag names
   can reach. So the rule is stated the other way round: every <p> and every
   prose <li> takes the register, and the things that are NOT body copy are
   named as exclusions.

   That inverts the failure mode. Before, anything unlisted silently kept its
   own value; now anything unlisted joins the register, and only a deliberate
   exclusion opts out. The audit is qa/bodyaudit.mjs, over twelve pages. */
body :is(p, li):not(
  .eyebrow, .spec-code, .pill-cap, .cap, .form-note, .badge, .code,
  .imgnote, .fp-tag, .stagelabel, .compass, .hero-caption, .form-success,
  .statplate-label, .statplate-figure, .partner-card-copy,
  /* V124 — THE POP-UP'S LABELS AND FIGURES ARE NOT BODY COPY.
     The register forces color:var(--body-color) !important on every <p>, so
     the "Yes, this is modular." line rendered white instead of the gold the
     client asked for, and no amount of ordinary specificity could reach it.
     This file's own note eight lines up says exactly this: any new component
     that owns its colour needs the exclusion or its colour is cosmetic. The
     paragraph of body copy in the same modal is deliberately NOT listed, so
     it keeps the register like every other paragraph on the site. */
  .th-pop-claim, .th-pop-feat-v, .th-pop-feat-l, .th-pop-time-v, .th-pop-time-l
):not(
  .nav-links *, .drawer *, .foot-bottom *, .warranty-card *, .fx-keys *,
  .hero-meta *, .strip *, .stat *, .cfg *, .trustindex-wrap *, .mmh-imglb *,
  .mmh-ytlb *, .scheme-bar *, .filter-bar *, .dim-rule *, .hsl-thumbs *,
  .gal-tabs *, .mmh-intro *, .post-content *, figcaption *, label *,
  .spectable *, .msz-pick *, .evz-readout *, .proc-step .n,
  /* V46 — `.company-lead *, .company-smrt *` (V40) is removed along with the
     rest of the Company scene it existed for. The scene set its own display-
     scale copy and needed to opt out of this !important register to keep it;
     with the scene gone, the section's lead paragraph goes back to the plain
     body register everyone else's does, exactly like it did before V32 (V31's
     own stylesheet never carried this exclusion either — only
     `.partner-card-copy`, still listed above, ever opted out here). */
  /* V44 — .bgate-body joined the register's opt-out for the same reason as
     the Company scene: this !important color was forcing .bgate-intro to
     #FFFFFF on a #F4F1EA card — invisible, not just low-contrast, exactly
     like the h2 fix a few lines up. A component that sets its own colour
     against its own background needs to be named here; nothing else stops
     the register from reaching it. */
  .bgate-body *,
  /* V98 — the Why band's paragraph joins the opt-out for exactly the reason
     this list exists. That section now inverts on scroll: light ground with
     dark copy at rest, dark photograph with light copy once it expands. The
     colour is driven by a keyframe, and a keyframe cannot beat `!important`
     — so the register pinned the paragraph to #FFFFFF and it sat invisible
     on the white state, measured at 1.14:1. Third instance of this exact
     bug on this site (see the h2 and .bgate-intro notes above); the pattern
     is always a component that owns its own background.
     Its typography is restated at .why--bg .lead so opting out changes the
     colour and nothing else. */
  #why.why--bg .lead
){
  font-size:var(--body-size) !important;
  color:var(--body-color) !important;
  letter-spacing:normal !important;
  line-height:1.75;
}


p,
.lead,
.measure,
li,
dd,
blockquote,
.tick-list li,
.ideal-list li,
.fx-list li,
.incl-col li,
/* V27.1 — `:not(.eyebrow)` ON EVERY CONTAINER-SCOPED SELECTOR HERE.
   These are container-scoped (`.sec-head p`, specificity 0,1,1) and so they
   OUTRANK the `.eyebrow` rule (0,1,0). An eyebrow is a <p>, so every eyebrow
   that happened to sit inside .sec-head, .company-lead or .split-copy was
   being set at body size — 16px against the 10.88px every other eyebrow on
   the page uses. Three sizes of eyebrow on one homepage, which is exactly
   what the client reported as "the subheadings are not the same".
   An eyebrow is the technical register, never body copy. */
.cfg-page-lede p:not(.eyebrow),
.company-lead p:not(.eyebrow),
.delivery-note-copy,
.split-copy p:not(.eyebrow),
.enq-aside p:not(.eyebrow),
.post-content p:not(.eyebrow),
.post-content li,
.sec-head p:not(.eyebrow){
  font-size:var(--body-size);
  color:var(--body-color);
  line-height:1.75;
}

/* V27.1 item 7 — the SMRT partner paragraph, a step down.
   Client: "make this text a lil bit smaller… just a lil bit." It is a
   caption on a logo card rather than running prose, so it steps out of the
   body register rather than bending it: .9rem against the 1rem around it,
   which is one notch, not a new size on the page. */
.partner-card-copy{
  font-size:.9rem;
  color:var(--body-color);
  line-height:1.7;
}
/* The lead keeps its role through weight and measure rather than size — it is
   the same type, set to a narrower column. */
.lead{ font-weight:400; letter-spacing:-.002em; max-width:58ch; }

/* Anything that had a size set on it locally has to give it back, or the
   cascade order decides which paragraphs are the odd ones out. */
.model-body .specs,
.fsc-copy p,
.bx-copy p,
.nc-copy,
.life-copy p,
.proc-step p,
.incl-note,
.expand-box p,
.ideal-box p{
  font-size:var(--body-size); color:var(--body-color);
}

/* Lists inside prose keep the body register; the technical lists that use
   `li` as a data row do not. */
.spectable td,.fx-keys li,.warranty-card *,.hero-meta *,.strip *{
  font-size:inherit;
}

/* Blog prose is longer-form and is allowed one step up, because 0.92rem over
   a 760px measure for two thousand words is a genuinely different job from
   0.92rem in a three-line section intro. Same colour. */
.post-content{ font-size:1rem; color:var(--body-color); }
.post-content p,.post-content li{ font-size:1rem; }

/* ⚠ V181 — THE BLOG ARTICLE SCALES, AND THE CHANGE BELONGS HERE. The rule
   immediately above reasons explicitly from "a 760px measure", and V181 widened
   the article column to about 950px so it would stop leaving half a screen of
   dead gutter beside it. 1rem across 950px is roughly 107 characters a line,
   which is past comfortable — so filling the space without touching the type
   would have traded readability for alignment.

   ⚠ FOUND BY MEASURING THE RENDERED TEXT, NOT BY READING THE CASCADE. A first
   attempt set the size on .post-content at the end of the file and looked
   right: the container computed 19.2px. The paragraphs stayed at 16px, because
   this pair pins them and sits later in the file at equal specificity. The
   container's size was never reaching a single word of the article.

   Scoped to .post-single so the legal pages, which also carry .post-content,
   keep the 1rem they were tuned at. Measured at 1440: 950px column, ~78
   characters a line, against 94 before. */
.post-single .post-content,
.post-single .post-content p,
.post-single .post-content li{ font-size:clamp(1.06rem, 0.92rem + 0.42vw, 1.2rem); }


/* ============================================================================
   MODULAR UPDATE V24 — THE SCROLL REEL AND THE MORPHING GROUND
   ============================================================================ */

/* ────────────────────────────────────────────────────────────────────────────
   1 · THE SCROLL REEL — a pinned, scroll-scrubbed image sequence

   THE MECHANISM, because it is the one non-obvious thing here: a pinned
   element cannot drive its own `view()` timeline. Once it pins it stops
   moving relative to the viewport, so its progress never advances and every
   animation on it sits at 0. The TALL TRACK owns a named timeline instead,
   the section declares `timeline-scope`, and the pinned children reference
   the name. That is the whole trick, and it is why this works where a
   `view()` on the pinned element would not.

   `animation-duration: auto` is stated explicitly on every rule. The
   `animation` shorthand resets duration to 0s, and a scroll-driven animation
   with a zero duration renders as its `from` state forever.
   ──────────────────────────────────────────────────────────────────────── */
.reel{
  position:relative; z-index:1;
  background:var(--ink);
  timeline-scope:--mmh-reel;
}
.reel-track{ block-size:420vh; view-timeline-name:--mmh-reel; }
.reel-pin{
  position:sticky; top:0;
  block-size:100vh; block-size:100svh;
  display:flex; align-items:flex-end;
  overflow:hidden;
}

/* Media — an inset frame that grows to full bleed as the sequence starts. */
.reel-media{ position:absolute; inset:0; clip-path:inset(9vh 9vw round 2px); }
.reel-media > .reel-frame img{ width:100%; height:100%; object-fit:cover; display:block; }
.reel-frame{ position:absolute; inset:0; margin:0; opacity:1; z-index:var(--z,1); }
.reel-scrim{
  position:absolute; inset:0; pointer-events:none; z-index:99;
  background:linear-gradient(180deg,
    rgba(16,24,32,.30) 0%, rgba(16,24,32,.10) 34%,
    rgba(16,24,32,.62) 78%, rgba(16,24,32,.90) 100%);
}

/* Content sits over the sequence, bottom-left, and rises as it scrubs. */
.reel-content{ position:relative; z-index:100; padding-bottom:clamp(40px,7vh,90px); width:100%; }
.reel-support{ margin-top:1rem; }
.reel-meter{ margin-top:1.8rem; display:flex; align-items:center; gap:1.1rem; flex-wrap:wrap; }
.reel-count{
  display:inline-flex; align-items:baseline; gap:.18em; margin:0;
  font-family:var(--mono); font-weight:600; letter-spacing:.1em;
  font-size:.78rem !important; color:var(--gold-deep) !important;
  font-variant-numeric:tabular-nums;
}
.reel-odo{ display:inline-block; height:1em; overflow:hidden; vertical-align:baseline; }
.reel-odo-strip{ display:block; }
.reel-odo-row{ display:block; height:1em; line-height:1; }
.reel-count-sep,.reel-count-total{ opacity:.55; }
.reel-segs{ display:inline-flex; gap:5px; align-items:center; }
.reel-seg{ display:block; width:clamp(14px,2.2vw,30px); height:2px; background:rgba(244,241,234,.22); overflow:hidden; }
.reel-seg-fill{ display:block; height:100%; width:100%; background:var(--gold); transform:scaleX(0); transform-origin:left center; }

@supports (animation-timeline: --mmh-reel) {
  @media (prefers-reduced-motion: no-preference) {
    .reel-media{
      animation:mmhReelOpen linear both;
      animation-duration:auto;
      animation-timeline:--mmh-reel;
      animation-range:contain 0% contain 30%;
    }
    /* The peel. Long, linear, and over an opaque layer beneath — so there is
       no luminance dip in the middle of the dissolve. The last frame never
       peels; there is nothing behind it. */
    .reel-frame:not(.is-last){
      animation:mmhReelPeel linear both;
      animation-duration:auto;
      animation-timeline:--mmh-reel;
      animation-range:contain var(--out-from) contain var(--out-to);
    }
    .reel-content{
      animation:mmhReelRise linear both;
      animation-duration:auto;
      animation-timeline:--mmh-reel;
      animation-range:contain 0% contain 26%;
    }
    .reel-odo-strip{
      animation:mmhReelOdo linear both;
      animation-duration:auto;
      animation-timeline:--mmh-reel;
      animation-range:contain var(--odo-from) contain var(--odo-to);
    }
    .reel-seg-fill{
      animation:mmhReelSeg linear both;
      animation-duration:auto;
      animation-timeline:--mmh-reel;
      animation-range:contain var(--seg-from) contain var(--seg-to);
    }
  }
}
@keyframes mmhReelOpen{
  0%  { clip-path:inset(9vh 9vw round 2px) }
  40% { clip-path:inset(0 0 round 0) }
  100%{ clip-path:inset(0 0 round 0) }
}
@keyframes mmhReelPeel{ from{ opacity:1 } to{ opacity:0 } }
@keyframes mmhReelRise{ from{ transform:translateY(26px); opacity:.72 } to{ transform:none; opacity:1 } }
@keyframes mmhReelOdo{ from{ transform:translateY(0) } to{ transform:translateY(calc(var(--last) * -1em)) } }
@keyframes mmhReelSeg{ from{ transform:scaleX(0) } to{ transform:scaleX(1) } }

/* No support, or reduced motion: the track collapses to one viewport and the
   first frame is a still photograph with the copy over it. Nothing is hidden
   and nothing is broken — it is simply a hero. */
@supports not (animation-timeline: --mmh-reel) {
  .reel-track{ block-size:auto; }
  .reel-pin{ position:static; block-size:clamp(420px,68vh,760px); }
  .reel-media{ clip-path:none; }
  .reel-frame:not(:first-child){ display:none; }
  .reel-meter{ display:none; }
}
@media (prefers-reduced-motion: reduce){
  .reel-track{ block-size:auto; }
  .reel-pin{ position:static; block-size:clamp(420px,68vh,760px); }
  .reel-media{ clip-path:none; }
  .reel-frame:not(:first-child){ display:none; }
  .reel-meter{ display:none; }
}
@media (max-width:700px){
  .reel-track{ block-size:300vh; }
  .reel-seg{ width:12px; }
}

/* ────────────────────────────────────────────────────────────────────────────
   2 · THE MORPHING GROUND

   One fixed backdrop behind the whole page. JavaScript sets its colour from
   whichever section owns the middle of the viewport, and the transition
   between values is what the client saw on the reference: the page's own
   surface changing as you scroll, rather than each section carrying a slab of
   colour that starts and stops on a line.

   `.mmh-ground-on` is added by the script only once the backdrop is in place.
   Until then — and forever, if the script never runs — the sections keep
   their own fills and the page looks exactly as it does today. That is the
   whole safety design: this cannot hide anything.
   ──────────────────────────────────────────────────────────────────────── */
.mmh-ground{
  position:fixed; inset:0; z-index:-2;
  background:var(--ink);
  transition:background-color 900ms var(--ease), background 900ms var(--ease);
  pointer-events:none;
}
html.mmh-ground-on body{ background:transparent; }
/* V66 — `.bg-green` is NO LONGER surrendered to the ground. The ink / paper-2
   / paper-3 grounds are all near-black and reading them as one continuous
   surface is the point of this system; the olive accent is not a page ground,
   it is a band, and letting it drive the backdrop washed whole neighbouring
   sections in it. It keeps its own fill at every scroll position now. */
html.mmh-ground-on .bg-paper2,
html.mmh-ground-on .bg-paper3,
html.mmh-ground-on .reel{ background:transparent; }
/* The feathered seams exist to dissolve a hard edge between two grounds.
   With one continuous ground there is no edge, so they come off — otherwise
   they paint the page colour over a section that is now the same colour. */
html.mmh-ground-on .bg-paper2::before,html.mmh-ground-on .bg-paper2::after,
html.mmh-ground-on .bg-paper3::before,html.mmh-ground-on .bg-paper3::after{
  content:none;
}
/* V66 — the hairlines come back with the fill. They were removed only because
   a section that had surrendered its background had no edge left to mark. */
@media (prefers-reduced-motion:reduce){
  .mmh-ground{ transition:none; }
}

/* ────────────────────────────────────────────────────────────────────────────
   3 · SECTIONS ARRIVE IN DEPTH

   Client: "add a 3d scroll effect or animation entrance for each section."

   A real perspective transform, not a scale standing in for one: the section
   arrives set back in Z and dollies forward to the picture plane as it
   crosses the viewport.

   WHY THERE IS NO rotateX HERE, THOUGH THERE WAS AT FIRST.

   The first cut tipped the section two degrees about its top edge. Two
   degrees is nothing — on a 900px block the far edge swings 31px and the
   perspective divide widens it by about 2%. But a `.wrap` is not 900px tall.
   The modular-homes wrap is 9,350px tall. Its bottom edge swings 326px
   TOWARD the viewer, and at a 1400px perspective that magnifies the bottom
   of the section by 30%: a 430px viewport got a 493px-wide section and a
   horizontal scrollbar on seven pages. The responsive sweep caught it —
   V23 was clean, V24 overflowed at every width below 600px.

   The flaw is structural, not a matter of tuning: ANY rotation about a
   horizontal axis scales with the height of the thing rotated, and these
   sections are arbitrarily tall. So the rotation is gone and the depth is
   carried by Z alone. translateZ under a perspective is a uniform scale —
   the same 6% whether the section is 900px or 9,000px — and because it
   travels from further away to the picture plane the scale is always BELOW
   one. It cannot overflow. It reads as the section coming forward, which is
   what the tilt was reaching for anyway.

   Scroll-driven, so it scrubs both ways, and on the compositor. It fails to
   "nothing moves", which is the only failure mode allowed for decoration.
   ──────────────────────────────────────────────────────────────────────── */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* V26 — THE END STATE IS `none`, NOT AN IDENTITY 3D TRANSFORM.
       `perspective(1400px) translate3d(0,0,0)` moves nothing, but it is still
       a 3D transform, so the browser keeps the section on its own composited
       layer forever and renders every glyph inside it with greyscale rather
       than subpixel antialiasing. That is the "some text looks blurry while
       other text is fine" the client reported: the difference between a
       section that had entered and one that had not. Ending at `none` lets
       the layer be discarded and the text snap back to normal rendering. */
    /* mmhTilt is declared ONCE, in the V26 block at the end of this file,
       where the rest of the staged entrance lives. It was redeclared here and
       the two definitions were competing — keyframe names are global and the
       later one silently wins, which is the same trap that killed the hero
       grain in V24. Removed rather than edited so there is one definition. */
    /* V59 — client: only two animations, the hero heading rise and a fade.
       This was a THIRD: every section's whole .wrap arrived on a perspective
       tilt from 150px of Z. Retired, so each element inside now performs its
       own entrance and nothing moves the container underneath them. */
    .section > .wrap,
    .company--bg > .wrap{
      transform-origin:center center;
      animation:none;
      animation-duration:auto;
      animation-timeline:view();
      animation-range:cover 4% cover 30%;
      /* No `will-change` here. It pins a composited layer for the life of the
         page on EVERY section, which is both the blur above and a large amount
         of GPU memory for an animation that is already on the compositor by
         virtue of being scroll-driven and transform-only. */
    }
    /* Anything pinned, sticky, interactive or being typed into keeps still. */
    .reel-content,
    .proc-scroll > .wrap,
    #configurator > .wrap,
    .cfg,
    .section:has(.enq-form) > .wrap,
    .section:has(form) > .wrap,
    .section:has(.trustindex-wrap) > .wrap,
    .section:has(.map-embed) > .wrap,
    .section:has(.cfg) > .wrap{ animation:none; transform:none; }
  }
}

/* ────────────────────────────────────────────────────────────────────────────
   4 · THE ONLINE BUILDER ON MOBILE

   ⚠⚠ V24 SCALED THIS TO 30% ON A PREMISE THAT WAS WRONG.

   The V24 note said: "a third-party embed with its own fixed internal layout;
   it does not reflow for a phone". Measured on staging, that is not what it
   is. `#lc-widget` contains exactly ONE child — an iframe with
   `style="height:100%"` and width 100% — which means the Canibuild app takes
   whatever box we hand it and lays itself out inside it. It reflows fine. It
   had simply never been given a phone-shaped box to reflow into.

   So the 30% transform was not shrinking an unreflowable desktop layout to fit.
   It was FORCING the desktop layout — `width:calc(100%/.3)` = 333% — and then
   shrinking the result to a third of legible size. That is why the client's
   next note was that it is hard to use: at .3 the type inside was ~4px.

   Rule: before reaching for a scale transform on a third-party embed, check
   whether it is actually fixed-layout. `transform:scale()` is the technique
   for something that cannot reflow; used on something that CAN, it prevents
   the reflow and then hides the evidence by making everything too small to
   read.

   V30 does the opposite: no transform, full width, and a bounded height so the
   builder is a component ON the page rather than the whole page.

   Height uses `svh`, not `vh`. On mobile Safari and Chrome `100vh` is the
   LARGE viewport — the height with browser chrome retracted — so a `vh`-sized
   panel is taller than what the user can actually see, and the builder's own
   controls sit under the address bar. `svh` is the small viewport, which is
   the one that is always visible. `vh` is kept as the fallback value for
   engines without `svh`.
   ──────────────────────────────────────────────────────────────────────── */
@media (max-width:900px){
  .cb-widget{
    margin-top:1.6rem;
    /* Fallback first, then the correct unit where it is supported. */
    height:min(76vh, 620px);
    min-height:420px;
    overflow:hidden;
    border:1px solid var(--line-gold);
    background:var(--paper-3);
    /* The builder captures touch inside its own frame; a visible edge tells
       the user where its scroll ends and the page's resumes. */
    border-radius:3px;
  }
  @supports (height:1svh){
    .cb-widget{ height:min(76svh, 620px); }
  }
  .cb-widget #lc-widget{
    /* Explicitly unset V24's transform: the custom property it used is still
       declared elsewhere and a stale --cb-scale must not be able to revive it. */
    transform:none;
    width:100%;
    height:100%;
    min-height:0;
  }
  .cb-widget #lc-widget iframe{
    width:100% !important;
    height:100% !important;
    border:0;
  }
  /* Anchoring to #builder from the hero CTA must clear the fixed header. */
  #builder{ scroll-margin-top:clamp(84px,12vh,120px); }
}

/* Between phone and laptop the desktop 780px is still taller than the window
   on most tablets in landscape. Same treatment, a little more room. */
@media (min-width:901px) and (max-width:1200px){
  .cb-widget{ height:min(80vh, 720px); }
  @supports (height:1svh){ .cb-widget{ height:min(80svh, 720px); } }
}


/* ============================================================================
   MODULAR UPDATE V25 — NAVIGATION RESTRUCTURE
   ============================================================================

   Top level is seven items now. Townhouses is no longer one of them: every
   home design sits under Modular Home, and the SMRT BUILT Townhouse leads a
   "Two Storey / Townhouses" category that future two-storey models join.

   The mega-menu therefore needs something it never needed before — a way to
   say "these belong together, those are different." A heading per group, and
   a hairline between groups. Nothing louder than that; the panel already
   earns its calm by showing names and thumbnails and nothing else.
   ------------------------------------------------------------------------ */

.nav-mega-group + .nav-mega-group{
  margin-top:.85rem; padding-top:.85rem; border-top:1px solid var(--line-soft);
}
.nav-mega-gtitle{
  margin:0 0 .45rem; padding-left:.6rem;
  font-family:var(--mono); font-size:.6rem; font-weight:500;
  letter-spacing:.16em; text-transform:uppercase; color:var(--gold-deep);
}

/* The foot is a row now: the gallery link keeps its weight, the two
   destinations that left the top level sit beside it as quieter text. */
.nav-mega-foots{
  display:flex; flex-wrap:wrap; align-items:center;
  gap:.35rem 1.3rem; margin-top:.75rem;
  padding-top:.7rem; border-top:1px solid var(--line-soft);
}
.nav-mega-foots .nav-mega-foot{ margin-top:0; }
.nav-links .nav-mega-more{
  font-size:.78rem; font-weight:500; letter-spacing:.01em;
  color:var(--muted); white-space:nowrap;
  transition:color .25s var(--ease);
}
.nav-links .nav-mega-more::after{ display:none; }
.nav-links .nav-mega-more:hover,
.nav-links .nav-mega-more:focus-visible{ color:var(--gold-glow); }

/* ── Drawer ──────────────────────────────────────────────────────────────
   Modular Home is the fold itself, so its summary has to read as a
   top-level row — same display face, same size, same rule underneath as the
   six plain links around it — not as the indented sub-heading the existing
   .d-fold summary was styled to be. */
.drawer .d-fold--top > summary{
  padding:1.05rem 0; padding-left:0;
  font-family:var(--display); font-size:1.7rem; font-weight:340;
  letter-spacing:normal; text-transform:none; color:inherit;
  align-items:baseline; gap:1rem;
}
.drawer .d-fold--top > summary .num{
  display:inline; font-family:var(--mono); font-size:.7rem;
  color:var(--gold-deep); letter-spacing:.1em;
}
.drawer .d-fold--top > summary::after{ align-self:center; }
/* The summary IS the row, so it carries the row's hairline whether the fold
   is open or shut — otherwise Modular Home is the one item in the list with
   no rule under it, which reads as a mistake rather than as a control. */
.drawer .d-fold--top > summary{ border-bottom:1px solid var(--line-soft); }
.drawer .d-fold--top{ border-bottom:none; }

.drawer .d-subhead{
  margin:.9rem 0 .1rem; padding-left:2.6rem;
  font-family:var(--mono); font-size:.62rem; font-weight:500;
  letter-spacing:.16em; text-transform:uppercase; color:var(--gold-deep);
}
.drawer .d-sublink--all{ color:var(--gold-deep); }

/* THE FOLD THAT NEVER FOLDED.
   V21 put the model links inside <details> and called it done. It was not
   done: `.drawer nav a{ display:flex }` sets an explicit display on the very
   elements the UA hides when the element is closed, and an author rule beats
   the UA slot. So every model link was laid out, visible and tabbable with
   the fold shut — which is precisely the thing the client reported, five
   months later, as "the individual home models should not all be displayed
   when the mobile menu is initially opened."
   State it outright, and stop depending on the UA to do it. */
.drawer .d-fold:not([open]) > *:not(summary){ display:none !important; }

/* Model names run long ("Lodge Classic · 1 Bedroom, 1 Bathroom"), and a
   2.6rem indent inside a 343px drawer wrapped almost every one onto two
   lines. Pull the indent in and drop a step: the fold reads as a list again
   rather than as a paragraph per model. */
.drawer .d-fold--top .d-sublink{ padding-left:1.5rem; font-size:.92rem; }
.drawer .d-fold--top .d-subhead{ padding-left:1.5rem; }

/* The mega panel is taller now that it carries two groups and a foot row.
   On a 1440×800 laptop that is close to the full viewport, so cap it and let
   it scroll rather than let it run off the bottom of the screen. */
.nav-mega{ max-height:min(78vh, 760px); overflow-y:auto; overscroll-behavior:contain; }


/* ============================================================================
   MODULAR UPDATE V26 — MENU SPACING AND A HOVER WITH A CHARACTER
   ============================================================================

   Client: "add spacing on the menu just like dvele.com and have a unique
   hover style too."

   SPACING. Measured off the reference: at a 1919px viewport Dvele's top-level
   items sit ~50px apart, which is ~2.6vw. Ours were 11–21px — close enough to
   read as one continuous strip of words rather than a set of destinations.
   The gap now tracks 2.2vw and is clamped at both ends, so it opens up on a
   wide screen the way the reference does without pushing the seventh item
   into the phone number at 1280.

   HOVER. A gold underline growing from the left is what every site does. This
   is a masked swap: the word and a duplicate of itself are stacked inside a
   clipped box, and on hover the pair travels up by exactly one line height.
   The word leaves at the top, its replacement arrives from the bottom, and
   the replacement is gold. It reads as mechanical and precise rather than
   decorative — which is the register the rest of this site is in.

   The underline stays, but only as the ACTIVE state now. Hover and current
   page were previously the same gesture, so you could not tell which page you
   were on while the mouse was anywhere near the bar.
   ------------------------------------------------------------------------ */

/* V47 — bumped slightly further per the client's follow-up DVELE-reference
   ask; V26 above already tuned this against a direct 1919px measurement, so
   this is a small further opening-up, not a re-measurement. */
.nav-links{ gap:clamp(1.1rem, 2.3vw, 2.5rem); }

/* V50 · item 1 — this is the rule that actually governs `.nav-links`'s gap
   at every width: it carries no media query, so it sits after (and wins
   over) BOTH earlier attempts at narrow-viewport tightening — the V18.1 one
   and the V21 one — which made both of those dead code, permanently, since
   whichever V-numbered section happened to load last in the stylesheet. The
   bar (now nine items, phone number included below 1520px — see
   `.nav-phone`) was never actually tightening for a narrower desktop/laptop
   width at all; it stayed at this full, spacious DVELE gap all the way down
   to the 1200px burger breakpoint, clipping up to 64px of the CTA button
   past `body`'s `overflow-x:hidden` — invisible, not scrollable, which is
   why nothing about it ever showed up as a page that scrolled sideways.
   Measured end to end with `qa/responsive-audit.mjs` and fixed here, in the
   one place that is actually live: two tightening steps, each threshold set
   past its own measured clean point rather than guessed. 1366×768 and
   1440×900 — both in the wider step's range — are two of the most common
   laptop screens there are. */
@media (max-width:1520px){
  .nav-links{ gap:clamp(.5rem,1.05vw,1.05rem); }
  .nav-links a{ font-size:.82rem; }
}
@media (max-width:1300px){
  .nav-links{ gap:.35rem; }
  .nav-links a{ font-size:.75rem; }
}

/* The clipped box. `overflow:hidden` on a line box clips descenders unless the
   box is told to be exactly one line tall and the glyphs are given room, hence
   the explicit line-height and the padding on the anchor rather than here. */
.nav-t{
  display:block; position:relative; overflow:hidden;
  line-height:1.5; height:1.5em;
}
.nav-t > i{
  display:block; font-style:normal; height:1.5em; line-height:1.5;
  transition:transform .52s var(--ease-out);
}
.nav-t > i + i{ color:var(--gold-hi); }

.nav-links a:hover .nav-t > i,
.nav-links a:focus-visible .nav-t > i{ transform:translateY(-1.5em); }

/* The two dropdown parents keep their caret outside the clipped box, so the
   caret is free to rotate while the label swaps. */
.nav-sub-toggle{ display:inline-flex; align-items:center; gap:.45em; }

/* The underline is the ACTIVE marker now, not the hover. */
.nav-links a::after{ transition:transform .45s var(--ease); }
.nav-links a:hover::after{ transform:scaleX(0); }
.nav-links a.active::after,
.nav-links a.nav-sub-toggle.active::after{ transform:scaleX(1); }

/* Reduced motion gets the colour change with no travel. */
@media (prefers-reduced-motion: reduce){
  .nav-t{ overflow:visible; height:auto; }
  .nav-t > i + i{ display:none; }
  .nav-links a:hover .nav-t > i{ transform:none; color:var(--gold-hi); }
}


/* ============================================================================
   MODULAR UPDATE V26 — SECTIONS THAT ACTUALLY ARRIVE
   ============================================================================

   Client, twice now: "where is the scroll behavior unique entrances and
   transition for every scroll between each section, heading, texts… We need
   hard 3d transition, effects and animations for each section, or heading or
   content."

   WHY THE PREVIOUS VERSION READ AS NOTHING.

   V24/V25 applied ONE animation — a 6% Z dolly with a fade — to the section
   wrapper, and nothing to anything inside it. Every element in a section
   therefore moved as a single rigid plate, on the same curve, over the same
   range. A plate arriving is not an entrance; it is a slide transition. And
   because it was the same gesture in every section on every page, the eye
   stopped registering it entirely after the second scroll.

   What Dvele actually does is not one bigger movement, it is FOUR SMALLER
   ONES THAT DO NOT AGREE WITH EACH OTHER. The heading arrives before the body.
   The body arrives before the media. Things are revealed from behind an edge
   rather than faded up from nothing. And each part travels a different
   distance on a different curve, so the section assembles itself in front of
   you instead of sliding in finished.

   So this block replaces one animation with a staged set:

     1 · the section still dollies, but half as far — it is the ground the
         other three move against, not the effect itself
     2 · the heading WIPES UP from behind its own baseline, with a real
         rotateX, and leads by ~15% of the range
     3 · the body copy and lists follow, rising and resolving opacity
     4 · media and cards arrive last, from further back, with a slight
         counter-rotation so they read as a separate plane

   ON THE rotateX, AND WHY IT IS SAFE HERE.
   V24 shipped a 2° rotateX on the section WRAPPER and it magnified a 9,350px
   block by 30%, putting horizontal scrollbars on seven pages (see the note on
   mmhTilt). The lesson recorded then was that rotation about a horizontal axis
   scales with the height of the rotated element. That is exactly why the
   rotation lives on HEADINGS here and not on wrappers: a heading is bounded at
   roughly 40–140px tall, so at 6° about its own baseline the near edge swings
   under 15px and the perspective divide adds ~1.5% — about 4px on a 280px
   heading, inside the 20px minimum gutter with room to spare. Bounded height
   is the whole licence for the effect, and the responsive sweep re-runs at
   eleven widths to prove it every release.

   Everything here is scroll-driven, so it scrubs in both directions and runs
   on the compositor. It is inside @supports and prefers-reduced-motion, and it
   fails to "nothing moves" — never to "nothing is visible".
   ------------------------------------------------------------------------ */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* ── 1 · The ground moves less ───────────────────────────────────────── */
    .section > .wrap,
    .company--bg > .wrap{
      animation-range:cover 2% cover 26%;
    }
    /* mmhTilt: single declaration lives in the V28 block at the end of this
       file, where the retuned values are. */

    /* ── RANGES: `cover`, not `entry` ────────────────────────────────────
       The first cut used entry-relative ranges (entry 8% → entry 78%). For a
       full-height section that is a long travel; for a 60px HEADING the entry
       phase is only about sixty pixels of scroll, so the whole wipe finished
       inside two frames of a trackpad flick. The animation was running — a
       "does it animate" check passed — and was completely imperceptible,
       which is the second time on this project that a passing assertion has
       hidden an effect nobody could see.
       `cover` spans the element's height PLUS the viewport height, so a short
       element gets the same generous travel a tall one does. The stagger is
       now carried by where each range ENDS: eyebrow at 14%, heading at 26%,
       media at 30%, body last at 34%. */
    /* ── 2 · The heading wipes up, and leads ─────────────────────────────
       clip-path does the reveal, not opacity: the letterforms emerge from
       behind their own baseline with hard edges, which is what separates a
       wipe from a fade. The end state clips well outside the box so nothing
       — descenders, focus rings, the gold rule under a heading — is cut. */
    /* mmhHeadWipe: single declaration lives in the V28 block at the end of this
       file, where the retuned values are. */
    /* Headings are NOT reliably wrapped in .sec-head — across these templates
       an h2 turns up as a child of .sec-head, .reveal, .company-lead,
       .split-copy and .wrap.center. Selecting by parent caught roughly half of
       them and silently skipped the rest, which is how the last version could
       pass a "does it animate" check and still look like nothing was
       happening. So the heading is selected as ITSELF, anywhere inside a
       section, with the places it must not run named as exclusions.
       V52 — client: "transitions are not reflecting to all pages and only on
       the home page." Grepped every heading class this theme actually uses:
       .display-xl (1 use — the homepage hero h1, correctly excluded, it has
       its own timed entrance), .display-l, .display-m and .display-s. This
       selector only ever listed .display-l/.display-m — .display-s was
       missing since the rule was first written. .display-s is the section
       sub-heading size, used in EVERY model page (model-body.php, all 14),
       plus About, Our Process, SMRT Built, What Sets Us Apart, the brochure
       gate and the blog listing. The homepage leans on .display-l/.display-m
       for its own major sections, so it kept animating and masked the gap;
       almost every inner page leans on .display-s for its sub-headings and
       got nothing. This is why it read as "home page only".
       V55 — `:not([data-mmh-lines])` added to both branches. A heading the
       splitter in mmh.js successfully breaks into real rendered lines gets
       marked `data-mmh-lines` and switches to the per-line reveal just below
       instead; this whole-block wipe stays the fallback for a heading the
       splitter skips (nested inline markup it won't safely reparent),
       reduced motion, or a browser without `animation-timeline`. */
    /* V106 — `:not([data-mmh-wipe])` added to both branches. Once mmh.js has
       marked a heading, the timed system at the end of this file owns it, and
       this scroll-driven rule must stop matching outright rather than be
       out-specified: this selector computes to (0,5,0) and a sane override
       would have needed six classes or an id. If the script never runs, no
       attribute is written and this rule still applies, which is the right
       fallback. */
    /* ⚠ V178 — `.post-content *` EXCLUDED, same reasoning as the paragraph
       rule and one more reason of its own. This wipe parks a heading at
       translateY(103%), a full line BELOW where it belongs, until the scroll
       timeline lifts it. On the live post mmh.js marks each heading with
       data-mmh-wipe and the timed system takes over, so the fallback never
       bites — but the comment above is explicit that it still applies when the
       script does not run, and measured in that state every h2 in the article
       sits on top of the paragraph under it: 12 collisions, up to 57px deep.

       Reduced motion, a browser without animation-timeline, or a JS error is
       enough to put a reader in that state, and the failure mode is an article
       with its headings printed over its text. Long-form prose has nothing to
       gain from a heading wipe in the first place, so the rule simply stops
       matching here rather than being propped up. */
    .section :is(h2, .display-l, .display-m, .display-s):not(.enq-title):not([data-mmh-lines]):not([data-mmh-wipe]):not(
      .model-grid *, .shot-grid *, .th-grid *, .life-grid *, .post-grid *,
      .cfg *, .reel-content *, .proc-scroll *, .trustindex-wrap *,
      .enq-aside *, form *, .post-content *, .post-side *
    ),
    .company--bg :is(h2, .display-l, .display-m, .display-s):not([data-mmh-lines]):not([data-mmh-wipe]){
      transform-origin:center bottom;
      /* V59 — the fallback now performs the hero's clip-and-rise, so it needs
         the hero's clipping box. `clip`, never `hidden`: `hidden` would make
         each heading a scroll container and freeze the very view() timeline
         driving it (rule #56, which cost a release to find the first time).
         The padding/margin pair is the same descender allowance the per-line
         wrapper uses, so nothing is cropped off a g, y or comma. */
      display:block;
      overflow:clip;
      padding-block-end:.12em;
      margin-block-end:-.07em;
      animation:mmhHeadWipe linear both;
      animation-duration:auto;
      animation-timeline:view();
      animation-range:cover 0% cover 26%;
    }

    /* ── 2b · Literal per-line reveal (V55, client ask) ───────────────────
       Client: "I want the animation hero up too applied on all headings."
       The homepage hero H1 reveals by splitting into individual lines
       (hand-coded in tpl-home.php) that each rise from behind a clipped
       edge. Applying that literally to every heading site-wide can't reuse
       the hero's hand-coding — its headline is two short, fixed, tested
       lines; every OTHER heading is arbitrary length and wraps differently
       at every breakpoint, so a hard-coded split would be right at one
       width and visibly wrong at the next.
       So the split happens at runtime instead: `mmh.js`'s
       `mmhSplitHeadingLines` measures each heading's ACTUAL rendered lines
       (re-measured on resize) and wraps each one in `.mmh-ls-line > span`,
       matching the hero's own markup shape, then marks the heading
       `data-mmh-lines` so the rules below take over from the whole-block
       wipe above. A heading with a nested styled run inside it (a coloured
       `<em>`/`<span>`, e.g. "The SMRT BUILT *Townhouse.*") is measured but
       kept as ONE literal line rather than word-split, so the coloured run
       is never torn out of its own element — that heading still gets the
       clip-and-rise, just not sub-line staggered. See mmh.js for exactly
       which headings that applies to (grepped: six across the theme).
       Same clip-and-rise as the hero (`translateY(103%)` behind
       `overflow:hidden`, not the fade+rotate above), driven by the scroll
       timeline instead of the hero's on-load timer, and staggered by
       `animation-range` the same way the eyebrow/heading/body already
       stagger against each other — a scroll-driven animation has no
       `animation-delay` to hand out per line, so each line's range is
       pushed a little later instead. No JS, reduced motion, or a heading
       the splitter skips falls straight back to the whole-block wipe above
       — never to no animation at all.
       ⚠ `overflow:clip`, NOT `overflow:hidden` — same rule as the Company
       background photo further down this file (§4e), for the same reason.
       `hidden` makes an element a scroll container, and `animation-timeline:
       view()` on a child resolves against its NEAREST scroll container. A
       line wrapper sized to one line of text IS its own child span's
       nearest scrollport once `hidden` makes it one — the span is then
       always "fully in view" of its own immediate box, so the timeline
       reports 100% progress regardless of where the page is actually
       scrolled to, and the line renders pre-settled from the first frame.
       Measured directly: with `hidden` every `.mmh-ls-line > span` on the
       page read `transform:none` at scrollY 0, heading still 2,700px below
       the viewport. `clip` clips the overflow the same way without
       registering as a scroll container, so the timeline resolves against
       the real page scrollport instead — confirmed the same spans sit at
       their un-settled `translateY(103%)` until actually scrolled to. */
    [data-mmh-lines] .mmh-ls-line{
      display:block; overflow:clip; padding-bottom:.12em; margin-bottom:-.07em;
    }
    [data-mmh-lines] .mmh-ls-line > span{
      display:block;
      transform:translateY(103%);
      animation:mmhLineUp linear both;
      animation-duration:auto;
      animation-timeline:view();
      animation-range:cover 0% cover 22%;
    }
    [data-mmh-lines] .mmh-ls-line:nth-child(2) > span{ animation-range:cover 4% cover 26%; }
    [data-mmh-lines] .mmh-ls-line:nth-child(3) > span{ animation-range:cover 8% cover 30%; }
    [data-mmh-lines] .mmh-ls-line:nth-child(4) > span{ animation-range:cover 12% cover 34%; }
    [data-mmh-lines] .mmh-ls-line:nth-child(n+5) > span{ animation-range:cover 16% cover 38%; }
    @keyframes mmhLineUp{ to{ transform:translateY(0); } }

    /* ════════════════════════════════════════════════════════════════════
       V102 · THE PINNED SECTION'S HEADING OPTS OUT OF ALL OF THE ABOVE
       ════════════════════════════════════════════════════════════════════
       Bianca, 15 Sep, with a screenshot of the resting state: "the title on
       the left is still cut off ... the title finally shows all but the right
       image changed to dark."

       THIS IS THE PER-LINE REVEAL FREEZING AGAINST THE PIN, AND IT IS THE
       THIRD TIME view() HAS DONE THIS TO THIS BUILD.

       Every line above gets its own `animation-timeline: view()`, staggered by
       RANGE: line 1 finishes at cover 22%, line 2 at 26%, line 3 at 30%. Those
       are measured against each LINE'S OWN box travelling through the
       viewport.

       The Why band is now sticky. The moment it pins, at cover 27.8% of its
       wrapper, every line inside it stops travelling, so every line's timeline
       stops advancing. Lines 1 and 2 are past their end and show. Line 3 needs
       30% and is frozen at 27.8%, so it sits permanently PART-RISEN behind its
       own clip. That is the heading cut in half across "& Granny Flat", with
       the section's ground showing under it.

       It cannot be fixed by retiming: a pinned element has no travel left to
       give, whatever numbers are used.

       So this heading takes none of it. It has its own choreography, and the
       wrapper `<div class="reveal">` around it still gives it an entrance —
       that system is CLASS-driven and TIME-based, so a pin cannot stall it.

       ⚠ THE `animation` ALONE WAS NOT ENOUGH, WHICH IS WHY IT SHIPPED BROKEN.
       The V98 rules already out-specified the whole-block wipe's `animation`,
       and the measured computed value confirmed it: `whyHeadMeasure, whyInk`,
       no `mmhHeadWipe`. What they did NOT override were the other properties
       in the same rule — `overflow:clip`, the descender padding, and the
       per-line branch, which lives on `.mmh-ls-line` and was never in scope at
       all. Overriding an animation does not undo the box it was given. */
    /* ════════════════════════════════════════════════════════════════════
       V107 · THE WHY HEADING HAS NO SPECIAL CASE ANY MORE
       ════════════════════════════════════════════════════════════════════
       Bianca, 15 Sep, with the resting frame and the centred frame side by
       side: "the heading on the screenshots shows different transitions."

       She is right, and tracing it through the pin showed exactly how. The
       heading's lines sat at 0 and full opacity for the whole resting stretch,
       then lifted AWAY; centred, they rose IN. Two different behaviours on the
       same heading, and neither was the one every other heading on the site
       had just been given.

       The V104 scroll-driven out-and-in that produced that is gone, keyframes
       and all. The Why heading now falls through to the site-wide timed rules
       at the end of this file, the same as every other heading: it rises into
       place when it arrives. It does that twice, because it arrives twice,
       once on the left and once in the centre. mmh.js re-fires it at the
       changeover; see the V107 note there for how it knows.

       ⚠ NOTHING SCOPED TO #why DRIVES THIS HEADING NOW. That is the point. If
       it ever needs to differ again, the reason has to be better than "the
       section is unusual", because that reasoning is what produced two
       transitions on one heading. */

    /* The eyebrow above a heading arrives first and from the side — a
       different axis to the heading's, so the two do not read as one move. */
    /* V59 — the eyebrow is text: fade only. Its clip-path wipe and sideways
       travel were a third gesture, and the client asked for exactly two. */
    @keyframes mmhEyebrow{
      from{ opacity:0 }
      to  { opacity:1 }
    }
    .section .eyebrow:not(
      .model-grid *, .shot-grid *, .th-grid *, .cfg *, .reel-content *,
      .proc-scroll *, .enq-aside *, form *
    ),
    .company--bg .eyebrow{
      animation:mmhEyebrow linear both;
      animation-duration:auto;
      animation-timeline:view();
      animation-range:cover 0% cover 16%;
    }

    /* ── 3 · The body follows the heading ────────────────────────────────
       Later range, shorter distance, no rotation. The lag is the point: the
       heading is already settled when the paragraph is still arriving. */
    /* mmhCopyFade: single declaration lives in the V28 block at the end of
       this file, where the retuned values are.
       V55 — client: "FadeIn for the body texts." Was mmhCopyRise (a fade
       plus a 28px rise, renamed here to match what it now does): the client
       asked for the rise itself removed, opacity only. Renamed rather than
       left misnamed — the next person grepping "Rise" in this file for the
       body-copy entrance would otherwise not find it. */
    /* V53 — client, again: "transitions ... not reflecting to all pages and
       only on the home page," after V52's heading fix. Same defect class as
       V52's .display-s gap, one level down: this selector only ever caught a
       paragraph by its PARENT (.sec-head > p, .split-copy > p, .company-lead
       > p) or by an explicit .lead/.measure class. Grepped every <p> outside
       an obviously-excluded grid/form/carousel across About, What Sets Us
       Apart, SMRT Built, model pages and the homepage's own Showroom band —
       most of this site's body copy sits in a plain <div> or a component
       wrapper (.proc-step, a bare two-up .grid column, a green closing band)
       that was never one of the five listed parents, so it just appeared,
       same as .display-s headings did before V52. .lead and .measure are
       kept as concepts, not extra selectors: both are confirmed to land only
       on <p> (grepped — company-lead/hero-lead/bx-lead are different classes
       that merely contain the substring), so selecting p itself already
       covers them. `.eyebrow` is excluded on purpose and is NOT optional —
       it is also always a <p> (home.php, section-finance.php, and others all
       use <p class="eyebrow">) and already has its own entrance (mmhEyebrow,
       just below); animation is not additive, so without this exclusion a
       later-declared rule here would silently replace mmhEyebrow with
       mmhCopyFade on every eyebrow on the site. `.qa *` keeps FAQ answers,
       which sit inside a closed <details>, out of this — they have nothing
       to gain from a scroll entrance while collapsed. */
    /* ⚠ V177 — `.post-content *` IS EXCLUDED, AND THIS IS CARD 103'S WHOLE FIX.
       Max: "I have created a Blog... fix the layout and styling so that it
       match the other internal pages."

       single.php renders the article as <article class="section">, so every
       paragraph of a blog post matched `.section p` and took this entrance.
       Measured on his post: 75 paragraphs, ALL 75 animated, and 63 of them
       sitting at opacity 0 at any one moment. The body copy fades in and out
       as you scroll past it, which is exactly the "doesn't match the rest of
       the site" he is reporting.

       The rule is not wrong — it is right for a marketing section, where a
       band holds two or three paragraphs with a screenful between them. It is
       wrong for long-form prose, where dozens of paragraphs share the viewport
       and the reader is reading rather than being walked through a page. An
       article should behave like a document.

       Same defect class as the V52 and V53 notes above, in the other
       direction: those widened a selector that was catching too little, this
       narrows one that is now catching too much. The blog did not exist when
       this was written.

       Verified on the live page before shipping: with the exclusion applied,
       all 75 paragraphs compute to opacity 1 and none is animated. */
    .section p:not(.eyebrow):not(
      .model-grid *, .shot-grid *, .th-grid *, .life-grid *, .post-grid *,
      .cfg *, .reel-content *, .proc-scroll *, .trustindex-wrap *,
      .enq-aside *, form *, .qa *, .post-content *, .post-side *
    ),
    .company--bg p:not(.eyebrow){
      animation:mmhCopyFade linear both;
      animation-duration:auto;
      animation-timeline:view();
      animation-range:cover 6% cover 34%;
    }

    /* ── 3b · The buttons arrive last (V31, client item 3) ───────────────
       Excluded on purpose, and each for a reason:
         form *          a submit button must never be mid-animation when a
                         validation error scrolls it into view
         .hero *         the hero has its own timed entrance, not a scroll one
         .cfg, grids     these carry their own card-level entrances already
         .site-head      the header CTA is persistent furniture
       Two ranges only, alternating: a primary/secondary pair is the common
       case and a second button that lags the first by a beat is the whole
       effect. Beyond two, the third would lag so far it reads as broken, so
       it rejoins the first range. */
    /* V177 — .post-content excluded here too, for the same reason as the
       paragraph rule above. A post body is prose; a button the client drops
       into one should not stage itself on scroll while the text around it
       does not. */
    .section :is(a.btn, button.btn):not(
      .model-grid *, .shot-grid *, .th-grid *, .life-grid *, .post-grid *,
      .cfg *, .reel-content *, .proc-scroll *, .trustindex-wrap *,
      .enq-aside *, .hero *, .site-head *, .drawer *, form *, .post-content *,
      .post-side *
    ),
    .company--bg .company-cta .btn{
      animation:mmhBtnRise linear both;
      animation-duration:auto;
      animation-timeline:view();
      animation-range:cover 10% cover 40%;
    }
    /* The second of a pair lands a beat later. Scroll timelines take no
       animation-delay — the delay IS the range — so the stagger is written as
       a later start and a later end. */
    .section :is(.hero-actions, .cta-row, .company-cta, .bx-cta) > :is(a.btn, button.btn):nth-of-type(2):not(
      .cfg *, .enq-aside *, .hero *, form *
    ){
      animation-range:cover 16% cover 46%;
    }

    /* ── 4 · Media and cards arrive last, on their own plane ─────────────
       Further back in Z and counter-rotated a degree, so the grid reads as a
       separate surface catching up with the text rather than part of it.
       Cards are bounded in height like headings, so the rotation is safe. */
    /* mmhPlane: single declaration lives in the V28 block at the end of this
       file, where the retuned values are. */
    /* V53 — .proc (About page's 4-step process strip) and .config-grid (the
       model page's birds-eye + configuration-sheet figures) are the same
       shape as the grids already listed here — a row of cards/figures with
       nothing driving their entrance — and had none at all, on either
       element or its heading (.proc-step h4 isn't covered by mmhHeadWipe
       either, since it's an h4, not an h2/.display-*). Added as two more
       units. Note this is a DIFFERENT .proc from .proc-scroll's own
       .proc-journey/.proc-node component on the standalone Process page,
       which already has its own scroll-driven entrance and stays excluded
       everywhere above. */
    .section .model-grid > *,
    .section .shot-grid > *,
    .section .th-grid > *,
    .section .life-grid > *,
    .section .post-grid > *,
    .section .proc:not(.proc-carousel *) > *,
    .section .config-grid > *{
      transform-origin:center top;
      animation:mmhPlane linear both;
      animation-duration:auto;
      animation-timeline:view();
      animation-range:cover 0% cover 30%;
    }

    /* ── 4b · A standalone content photo settles in too (V51, client item 1:
       "smooth and premium entrance transitions of each image, headings,
       text, buttons? For all pages"). Headings (2), body copy (3) and
       buttons (3b) above have had a scroll entrance since V26/V31; a plain
       content photograph — the About page's "Our mission" photo, the
       homepage Showroom render, the Custom Online Builder's site-plan panel
       — had none and just appeared. A GRID of photos already arrives as a
       unit via mmhPlane directly above, so this targets only a photo that
       ISN'T one of those: .split-media (used by tpl-about.php, tpl-home.php
       and section-finance.php) and .cob-media. Gentler than mmhPlane on
       purpose — a soft zoom-settle, no rotation — because a single large
       photo reads as an image, not one of a stack of cards. The hero,
       forms, nav and the Company band are excluded the same way rule 3b
       excludes them: each has its own entrance already, or must never be
       mid-animation when it matters (a hero's timed intro, a submit button
       scrolled into view mid-fade). */
    /* V59 — photographs are "content": fade only, no scale, no rise. */
    @keyframes mmhPhotoRise{
      from{ opacity:0 }
      to  { opacity:1 }
    }
    .section :is(.split-media .photo, .cob-media img):not(
      .hero *, .site-head *, .drawer *, form *, .company--bg *
    ){
      animation:mmhPhotoRise linear both;
      animation-duration:auto;
      animation-timeline:view();
      animation-range:cover 0% cover 32%;
    }

    /* V27.1 — THIS BLOCK USED TO SAY `animation:none !important` AND IT BROKE
       THE HERO HEADLINE.

       It was meant as a safety net: "whatever my selectors above reach into,
       switch it off here." That is the wrong shape of rule, because
       `animation:none !important` does not switch off MY animation — it
       switches off EVERY animation on the element, including ones that were
       already there and load-bearing.

       The hero headline is built as `.hero h1 .line > span`, sitting at
       `translateY(103%)` inside an `overflow:hidden` line box, and the ONLY
       thing that brings it into view is its own `heroUp` keyframe. Killing
       animations on `.hero *` left every line parked below its own mask.
       The headline did not fade or glitch — it was simply never there.

       The correct mechanism is the `:not()` lists on each selector above,
       which stop my animations being applied in the first place rather than
       stamping on whatever is applied. What remains here is only `clip-path`,
       which nothing else on this site uses, so resetting it cannot take
       anything away that was not mine. */
    .reel-content *,
    .proc-scroll *,
    #configurator *,
    .cfg *,
    .hero *,
    .site-head *,
    .drawer *,
    .mmh-imglb *,
    .trustindex-wrap *,
    .map-embed *,
    .cb-widget *{
      clip-path:none !important;
    }
  }
}

/* The fallback is the existing .reveal, which is class-driven and does not
   depend on scroll-timeline support at all. */
@media (prefers-reduced-motion:reduce){
  .section :is(h2, .display-l, .display-m, .eyebrow, .lead, .measure, p),
  .company--bg :is(h2, .display-l, .display-m, .eyebrow, .lead, .measure, p),
  .section .model-grid > *,
  .section .shot-grid > *{
    animation:none !important; transform:none !important;
    clip-path:none !important; opacity:1 !important;
  }
}


/* ============================================================================
   MODULAR UPDATE V26 — THE SPEC PLATE
   ============================================================================

   Client: "redesign the second image wherever there is a layout like that so
   it will look more premium."

   The layout in question was a thin gold rectangle, everything centred inside
   it, a hairline under the first line. That is certificate language — it reads
   as an award or a testimonial plaque. On a page arguing factory precision it
   was saying the opposite of the thing it contained.

   Three changes, and the first two are the ones that do the work:

   1 · NOTHING IS CENTRED. Centred text has no edge to read down. Left-aligned
       copy against a hard left edge is how specifications are set, and it is
       the same axis the rest of the page already uses.
   2 · THE BOX BECOMES AN EDGE. A closed rectangle isolates its contents from
       the page; a single weighted rule anchors them to it. The gold is doing
       more work at 2px on one side than it was at 1px on four.
   3 · THE FIGURE IS THE SUBJECT. "12–14" is the reason the panel exists, so
       it is set at display scale with the unit beside it in the technical
       register, rather than buried mid-sentence at body size.
   ------------------------------------------------------------------------ */

.statplate{
  position:relative;
  background:linear-gradient(90deg,
    color-mix(in srgb, var(--paper) 5%, transparent),
    color-mix(in srgb, var(--paper) 1.5%, transparent) 60%,
    transparent);
  border:0;
  border-left:2px solid var(--gold);
  padding:clamp(18px,2.2vw,26px) clamp(20px,2.6vw,34px);
  text-align:left;
}
/* A hairline closes the bottom only — enough to say the block has ended,
   not enough to make it a box again. */
.statplate::after{
  content:""; position:absolute; left:0; right:0; bottom:0; height:1px;
  background:linear-gradient(90deg, var(--line-gold), transparent 70%);
}
.statplate-label{
  margin:0 0 .35rem;
  font-family:var(--mono); font-size:.62rem; font-weight:600;
  letter-spacing:.2em; text-transform:uppercase; color:var(--gold-deep);
}
.statplate-figure{
  margin:0; display:flex; align-items:baseline; gap:.5em;
  line-height:1;
}
.statplate-num{
  font-family:var(--display); font-weight:450;
  font-size:clamp(2.4rem,5.2vw,3.9rem); letter-spacing:-.03em;
  color:var(--paper); font-variant-numeric:tabular-nums;
}
.statplate-unit{
  font-family:var(--mono); font-size:.7rem; font-weight:600;
  letter-spacing:.2em; text-transform:uppercase; color:var(--muted);
}
.statplate-copy{
  margin:clamp(.8rem,1.4vh,1.15rem) 0 0; max-width:46ch;
}

/* The old rules are neutralised rather than deleted, so any page still using
   the previous markup degrades to plain text on the page ground instead of
   inheriting half a box. */
.delivery-note{ border:0; background:none; text-align:left; padding:0; }
.delivery-note-head{ border-bottom:0; }
.bg-ink .delivery-note, .bg-green .delivery-note{ background:none; }
.statplate.delivery-note{
  padding:clamp(18px,2.2vw,26px) clamp(20px,2.6vw,34px);
  border-left:2px solid var(--gold);
}

@media (max-width:640px){
  .statplate{ padding-inline:clamp(16px,4vw,22px); }
}


/* ============================================================================
   MODULAR UPDATE V28 — THE TWO SIGNATURE ENTRANCES, PORTED FROM THE ASTRO BUILD
   ============================================================================

   Client, pointing at modualar-granny-flat.pages.dev:
     · "make the company section the same scroll entrance of Inside the build"
     · "make the Why Choose Us the same scroll entrance of Master Modular"
     · "I don't see the hard 3d scroll transition entrances of the other sections"

   I read the reference's own source rather than working from the screenshots
   — src/components/home/ScrollReel.astro and BrandStatement.astro — because
   the thing that makes each of these read is a specific mechanism, not a
   quantity of movement.

   WHAT "INSIDE THE BUILD" ACTUALLY DOES (ScrollReel.astro):
     @keyframes reelOpen {
       0%   { clip-path: inset(9vh 9vw round 2px); }
       40%  { clip-path: inset(0 0 round 0); }
     }
   The media starts as an INSET, ROUNDED CARD and opens out to full bleed as
   you scroll. It is not a fade and not a zoom — it is an aperture. Meanwhile
   the copy counter-drifts against it (`translate: 0 34px → 0 -22px`), so the
   frame grows one way while the words move the other. That opposition is the
   whole effect; without it the section just gets bigger.

   WHAT "MASTER MODULAR" ACTUALLY DOES (BrandStatement.astro):
     · the heading rises out of a mask — translateY(105%) → 0 over 950ms
     · a hairline rule draws itself, scaleX(0) → 1, 150ms behind the heading
     · the whole panel arrives, HOLDS, then leaves again:
         0% translateY(72px) opacity .15 → 34% settled → 70% still settled
         → 100% translateY(-64px) opacity 0
     · the side column drifts independently
   The hold is what makes it feel composed rather than animated. Most scroll
   effects only have an arrival; this one has an arrival, a stay and a
   departure, so the section behaves like a slide being presented.

   ONE DELIBERATE DIFFERENCE FROM THE REFERENCE.
   Those sections PIN on the Astro build, so it drives them with
   `scroll(root)` and hard viewport-unit ranges — its own comment explains
   that a view() timeline stalls the moment an element sticks. Our sections do
   not pin, so `view()` is correct here and, unlike fixed vh ranges, it stays
   right at every viewport height and section length. Same motion, driven by
   the thing that actually applies.
   ------------------------------------------------------------------------ */

/* ⚠ NEVER MIX RANGE PHASES. AN `entry` START WITH A `cover` END IS DEGENERATE.

   Every scroll range in this file uses ONE phase at both ends, and that is not
   a style preference — a mixed range silently pins progress at 1.

   Measured on the Company background photo (765px tall, 900px viewport), the
   same element, the same timeline, only the range changed:

     entry 0% cover 40%   progress: 1 → 1 → 1 → 1 → 1        (dead)
     cover 0% cover 35%   progress: 0.07 → 0.36 → 0.65 → 0.94 → 1
     entry 0% entry 100%  progress: 0.05 → 0.26 → 0.47 → 0.68 → 0.89
     contain 0% …100%     progress: 0 → 0 → 0 → 0 → 0        (never contained)

   The dead one reports a live animation name, a live ViewTimeline and a live
   range in the computed styles. The ONLY way to see it is to read
   `element.getAnimations()[0].effect.getComputedTiming().progress`, or the
   animated property itself, across several scroll positions.

   That makes three separate scroll-animation failures on this project that
   all looked healthy from the stylesheet: a pinned element driving its own
   view() timeline, an `overflow:hidden` ancestor becoming the scrollport, and
   this. In every case the fix was cheap and the DETECTION was the hard part.
   Measure the value, never the presence of the animation. */

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {

    /* ── ITEM 2 · THE COMPANY — the aperture opens ───────────────────────── */
    /* BOTH ENDS MUST HAVE THE SAME SHAPE STRUCTURE OR NOTHING INTERPOLATES.
       The first version went `inset(9vh 9vw round 3px)` → `inset(0 0 0 0
       round 0)` — two components at one end, four at the other. CSS cannot
       interpolate between inset() functions of different arity, so it did not
       animate at all: it snapped to the end state and the aperture never
       existed. Throughout, it reported a live animation name, a live timeline
       and a live range, which is why this had to be caught by reading the
       computed clip-path rather than by checking the animation was applied. */
    /* V31 — 9vh/9vw round 2px, matching the reference frame exactly
       (`ScrollReel.astro`). It was 7vh/7vw round 3px, which read as a slightly
       rounded panel rather than an aperture opening. */
    /* ── V38 · CLIENT — THE PHOTOGRAPH OPENS, NOT THE SECTION ────────────
       "make the image where my red box is on the screenshot then as you
        scroll, it will be larger to be a fullscreen. We will use the same
        effect."

       Same mechanism, different subject. Until now the whole SECTION clipped
       from a 9vh/9vw card to full bleed — copy and photograph together. The
       client drew a box on the photograph alone, on the right, clear of the
       words. So the aperture moved onto the photograph and its start shape
       became that box rather than a symmetric inset.

       The four numbers are read off the client's screenshot (1887 × 776), not
       invented: the box runs 952→1755 across and 78→495 down, which is
       10% from the top, 7% from the right, 34% from the bottom and 50% from
       the left. Percentages rather than vh/vw so the card keeps its place on
       the section at any window size.

       ⚠ SAME ARITY AT BOTH ENDS. The V28 note below is about clip-path but the
       trap is the property, not the function: write all four values at both
       ends. `inset:0` as a shorthand does expand to four longhands and would
       interpolate, but the failure mode when it does not is silent — it snaps
       to the end state and the aperture simply never exists. Not worth the
       saved characters. */
    /* V39 — the scene is now TWO PHASES, per the PIQUE reference the client
       pointed at (pique.com.au): copy left + photo card right, then the card
       opens to a fullscreen background and the centred SMRT content replaces
       the copy. Every animation in the scene runs the SAME range —
       `contain 0% → contain 100%` — so a keyframe percentage means the same
       moment in every keyframe block below. The phases live in the keyframes,
       not in five different ranges that have to be cross-referenced:

         0–12%    hold — two columns, card at rest (the PIQUE first screen)
         12–55%   the photograph opens to full bleed
         12–48%   the scrim rises with it, so the crossover stays legible
         22–48%   the copy fades up and away
         58–80%   the centred SMRT content fades in
         80–100%  hold — the PIQUE second screen

       The card is the right 40% of the frame, vertically centred, which is
       where the client's red box sat and where PIQUE parks its card. */
    /* ⚠ V42 — THE FOUR SCENE KEYFRAMES ARE GONE. THE SCENE IS DRIVEN BY
       JAVASCRIPT, NOT BY A SCROLL TIMELINE.

       They were `mmhPhotoOpen` (plus a <900px override), `mmhScrimIn`,
       `mmhLeadOut` and `mmhSmrtIn`, all running `contain 0% → contain 100%`
       on the named `--company` timeline. The phase map above is still the
       contract — it now lives in `assets/js/mmh.js`, in mmhCompanyScene(),
       as the same six percentages.

       Why: two clients' browsers in a row could not run it. CSS scroll-driven
       animations need `animation-timeline` AND `timeline-scope` (a separate,
       later feature), and a browser with one and not the other took the
       overlay layout with nothing animating it — the V41 defect. Gating on
       `html.mmh-scene` made that fail SAFELY, but a safe failure is still a
       failure: the client saw a flat section where the scene should be.

       One driver, every browser, one appearance. The pinned layout below is
       plain CSS; the four animated properties are written as inline styles
       from a rAF-throttled scroll listener. Nothing here depends on a feature
       test any more. */
    /* ⚠ `view()` IS THE WRONG TIMELINE INSIDE THIS SECTION, AND IT FAILS
       SILENTLY.

       `.company--bg` is `overflow:hidden` (it clips the full-bleed photo).
       That makes it the nearest SCROLLPORT for everything inside it, and a
       `view()` timeline resolves against the nearest scrollport — not the
       document. The background photo is `position:absolute; inset:0`, so it
       fills that scrollport exactly and never moves within it. Progress is
       therefore constant forever: the animation is attached, named, ranged
       and completely frozen. Measured, it sat at one value at every scroll
       position on the page.

       This is the same shape of problem as a pinned element driving its own
       timeline, and it takes the same solution: the SECTION owns a named
       view-timeline — it does move in the document scrollport — declares
       `timeline-scope` so its descendants can see the name, and every child
       animation references that name instead of `view()`.

       Any future scroll animation inside an `overflow:hidden` container needs
       this treatment. `view()` will look correct in the stylesheet and do
       nothing on the page. */
    /* THE PHOTO CANNOT DRIVE ITS OWN TIMELINE, EVEN WITH THE SCROLLPORT FIXED.
       It is `position:absolute; inset:0; z-index:-2`, and as a view() subject
       it reported a timeline sitting at ~60% while the section had barely
       entered the viewport — measured via
       `getAnimations()[0].timeline.currentTime`. An in-flow sibling on the
       same section reads correctly at the same moment, so the fault is the
       subject, not the scrollport.
       So the IN-FLOW wrap owns a named timeline — it moves with the section
       and measures cleanly — the section declares `timeline-scope` so the
       name is visible to its descendants, and the photo references the name.
       This is the same structure the scroll reel uses for its pinned frames:
       when an element cannot be its own subject, name a timeline on one that
       can. Note the two declarations are on DIFFERENT elements; putting
       `view-timeline-name` and `timeline-scope` on the same element does not
       work. */
    /* THE APERTURE IS ANIMATED WITH `inset`, NOT `clip-path`.
       clip-path was the reference's mechanism and it refused to drive on this
       layer: the photo is `position:absolute; z-index:-2`, and as a view()
       subject its timeline read ~60% while the section had barely entered —
       measured through `getAnimations()[0].timeline.currentTime`. Naming the
       timeline on the in-flow wrap did not move it either.
       `inset` gets to the same picture by a shorter route. The photo is
       absolutely positioned, so its inset IS its frame: 9vh/9vw of inset is
       a card, 0 is full bleed, and `object-fit:cover` re-crops as the box
       grows, which is exactly what an aperture does. No clip-path, no named
       timeline, no dependence on which layer the element sits on. */
    /* THE APERTURE IS ON THE SECTION, NOT ON THE PHOTO — AND THAT COST FIVE
       ATTEMPTS TO ESTABLISH.

       `.company-bg-photo` is `position:absolute; inset:0; z-index:-2`, and it
       cannot drive a correct view() timeline here. Measured through
       `getAnimations()[0].timeline.currentTime`, its timeline sat at ~60%
       while the section had barely entered the viewport — and it read the
       same 60% whether the timeline was anonymous or named, whether the
       animated property was clip-path or inset, and whether the section
       clipped with `hidden` or `clip`. An in-flow sibling in the SAME section
       (.company-lead) reads correctly at the same instant, which is what
       proves the fault is the subject rather than the section or the range.

       The section itself is in-flow and measures correctly, so it owns the
       aperture. This is also closer to the reference: "Inside the build"
       opens the whole frame — media and copy together — not just the
       photograph behind it. */
    /* V38 — the subject is the frame, and it references the NAMED `--company`
       timeline rather than `view()`. Everything above about the photograph
       being unable to drive its own view() timeline still holds; the named
       timeline declared on the reel track (V31, further down this file) is
       what makes animating this layer possible at all. It is in scope here
       because `timeline-scope:--company` sits on `.company-reel`, an ancestor
       of everything in this section. */
    /* V39 — NO unpinned animated variant any more. The two-phase scene only
       makes sense pinned (the phases need somewhere to play out); everywhere
       the pin is unavailable the section shows the static fallback: photo
       full bleed, copy on it, SMRT band below. The pinned rules further down
       are the only place these keyframes are attached. */
    /* V39 — the counter-drifts are GONE, and so is the V38 collision note
       that governed them. The copy no longer drifts against the frame; it
       fades out entirely (mmhLeadOut) and the partner card it used to close
       on no longer exists — its content is the centred .company-smrt overlay.
       An element that is not on screen cannot collide with one that is. */

    /* ── ITEM 3 · WHY CHOOSE US — arrive, hold, depart ───────────────────── */

    /* The heading rises out of a mask. The reference splits its statement
       into per-line spans; ours has to keep wrapping naturally at any width,
       so the mask is on the heading block instead of on each line. Same
       gesture, one element, no markup that fights the responsive wrap. */
    /* V59 — this box existed to mask mmhMaskRise. The heading now uses the
       site-wide treatment, which brings its own clipping, so this is reduced
       to the descender padding. `hidden` is deliberately NOT reinstated here:
       it makes the element a scroll container and breaks
       animation-timeline:view() on anything inside it (rule #56). */
    #why .display-m,
    #why .display-l{
      display:block;
      padding-block-end:.12em;
      margin-block-end:-.08em;
    }
    @keyframes mmhMaskRise{
      from{ transform:translateY(105%) }
      to  { transform:translateY(0) }
    }
    #why .display-m > *,
    #why .display-m{ will-change:auto; }
    /* V59 — client: "Can we remove the maskrise animations." This was it.
       #why's heading now takes the same treatment as every other heading on
       the site (the hero rise, per line where the splitter can divide it),
       so it is left with no rule of its own here. */
    #why h2{
      animation:none;
    }
    /* The eyebrow's own hairline draws itself, a beat behind — this is the
       reference's .bs__rule, which we already have as .eyebrow::before. */
    @keyframes mmhRuleDraw{
      from{ transform:scaleX(0) }
      to  { transform:scaleX(1) }
    }
    /* V59 — the eyebrow's hairline drew itself. Retired with the rest of
       #why's bespoke choreography. */
    #why .eyebrow::before{
      transform-origin:left center;
      animation:none;
    }
    /* Arrive → hold → depart. The hold between 30% and 70% is the part that
       makes it read as composed rather than as something sliding past.

       V33 — the flat translateY became a real camera move. The panel now
       arrives tilted back and away from the viewer, rotates flat as it
       reaches centre, holds square, then falls away over the top edge.

       Four things this depends on, each of which breaks it if changed:

       1 · `perspective()` must be the FIRST function in the list. Transform
           functions apply in order; a perspective written after the rotate
           projects nothing and the tilt collapses to a flat vertical squash.
       2 · Every keyframe carries the SAME function list in the SAME order.
           Mismatched lists fall back to matrix interpolation, which takes a
           different (and visibly wrong) path through the rotation.
       3 · The Z travel is NEGATIVE at both ends, so the panel is smaller than
           its box whenever it is tilted. A positive Z would push it toward
           the camera, scale it past 100% and put a scrollbar on the page.
       4 · The hold keyframes resolve to the IDENTITY matrix — perspective
           with zero rotation and zero Z is identity — so type is rasterised
           square and crisp for the whole time it is being read. Leaving any
           residual tilt in the hold is what makes 3D panels look soft. */
    /* ⚠ 5 · WHERE the tilt resolves matters more than how big it is.
       Over a `cover` range on a full-height panel, the panel's centre only
       enters the viewport at 25% and leaves it at 75%. Squaring up at 30%
       and starting to leave at 70% therefore spends almost the whole camera
       move off-screen — measured, correct, and invisible. 38% / 66% puts the
       tilt inside the band the panel is actually being looked at, and still
       leaves the entire readable middle square.

       Opacity is keyed SEPARATELY (16% / 82%). CSS interpolates each property
       across the keyframes that declare it, so the panel is solid for the
       whole tilt instead of fading in through it — a 3D move that arrives
       transparent reads as a fade, which is the thing it is not. */
    @keyframes mmhPanel3D{
      0%   { transform:perspective(1200px) translate3d(0,150px,-460px) rotateX(22deg);  opacity:0 }
      14%  { opacity:1 }
      45%  { transform:perspective(1200px) translate3d(0,0,0)          rotateX(0deg)             }
      63%  { transform:perspective(1200px) translate3d(0,0,0)          rotateX(0deg)             }
      84%  { opacity:1 }
      100% { transform:perspective(1200px) translate3d(0,-130px,-380px) rotateX(-17deg); opacity:0 }
    }
    /* V59 — the arrive/hold/depart camera move (V33) is retired: it is a
       third gesture and it moved the whole panel, including its heading, in a
       way nothing else on the site does. */
    #why > .wrap{
      transform-origin:50% 50%;
      animation:none;
    }
    /* The side column drifts on its own, so the two halves are never rigid. */
    /* V59 — retired. (It had also been referencing a keyframe deleted back in
       V39, so it was already doing nothing — see the note below.) */
    #why .grid.two > *:last-child{
      animation:none;
    }
    /* #why owns its whole entrance, so the generic heading wipe and copy rise
       must not ALSO run on its children and fight for the same transform.
       These rules come later in the file than the generic ones and match with
       an id, so they win outright — the `animation` shorthand replaces the
       generic animation entirely rather than layering on top of it. */
    /* V59 — #why no longer owns a bespoke entrance, so its eyebrow is no
       longer excluded from the generic one: it fades with every other
       eyebrow. */
    /* ⚠ V39 — #why's body copy had BORROWED the Company section's
       mmhCounterDrift keyframe. V39 deleted that keyframe with the rest of
       the Company drift system and this rule silently went static — an
       unknown animation-name is not an error, it is nothing. Caught by
       qa/entrances.mjs, which is exactly the probe that reads values instead
       of trusting that an animation is attached. The drift now has its own
       name; a keyframe shared across unrelated sections is a hidden coupling,
       and this is what it costs. */
    @keyframes mmhWhyDrift{
      from{ translate:0 30px }
      to  { translate:0 -46px }
    }
    /* V59 — #why's body copy drifted 76px against the scroll. Body copy fades,
       like all other body copy. The rule is DELETED rather than set to
       `animation:none`: an id selector here beats the generic paragraph rule,
       so `none` would have left this one paragraph with no entrance at all
       while every other paragraph on the site fades. Caught by
       qa/entrances.mjs, which reads computed values instead of trusting that
       something is attached — the same probe that caught the V39 borrowed
       keyframe. */

    /* ── ITEM 6 · THE GENERIC ENTRANCE, WITH ENOUGH TRAVEL TO SEE ────────
       The V26 version was correct in structure and far too timid in
       quantity — 22px of rise and 6° of tilt over a range that finished
       early. Doubled, and the ranges are pushed later so the movement
       happens where the eye already is rather than at the bottom edge. */
    /* V54 · client — "I want it to be live Pique's heading, check the Take
       another PIQUE section on pique.com.au." Read directly off Pique's own
       GSAP ScrollTrigger tween (their .section__title elements carry
       data-animaate-title="step-in"): the "to" state is opacity:1,
       rotation:0, yPercent:0 — a translate and a small rotation settling to
       flat, NOT a clip-path mask wipe or a 3D perspective tilt. This replaces
       the V26 clip-path/rotateX wipe with the same three properties Pique
       actually animates. It stays on this site's native
       `animation-timeline:view()` mechanism rather than adopting Pique's own
       GSAP+SplitText driver — same visual character, no library, nothing
       that can go dead on a frozen JS timeline the way a scene driver can
       (see CURRENT-STATE.md rule #44). */
    /* V59 — client: "For heading: All should be the same as the 'Premium
       Modular Granny Flats & Modular Homes' title on homepage hero section."
       That title is a clip-and-rise with NO fade and NO rotation: each line
       sits at translateY(100%) inside an overflow-clipped wrapper and rises to
       0 (see `heroUp` and `.hero h1 .line` above). Headings the JS splitter
       can divide already run exactly that, per line, as `mmhLineUp`. This is
       the whole-block fallback for a heading it cannot split safely, so it is
       now the SAME gesture on the block instead of the old 34px rise with a
       2-degree rotation and a fade — which was a different move altogether.
       The clipping and the descender padding are on the selector, below. */
    @keyframes mmhHeadWipe{
      from{ transform:translateY(103%) }
      to  { transform:translateY(0) }
    }
    /* V55 — client: "FadeIn for the body texts." The V54 version (28px rise
       + fade, itself lightened from V28's 46px to match Pique's own body
       reveal) had the rise taken out entirely — opacity only, no transform,
       which is also why this no longer needs `transform-origin` or a
       counter-rotation the way the heading above does. */
    @keyframes mmhCopyFade{
      from{ opacity:0 }
      to  { opacity:1 }
    }
    /* V31 · client item 3 — the buttons were the missing third beat.
       Headings and body copy have had entrances since V26; the calls to
       action, which are the point of the section, simply appeared. They
       arrive LAST and travel least: a heading can afford 44px and a tilt, a
       button is a target and a target that slides far reads as unstable. */
    /* V59 — client: "For body texts/content/buttons: Fade in." The 26px rise
       is gone; buttons fade, like every other non-heading element. The name is
       kept because it is referenced from one selector and from the QA probes;
       what it DOES is now a fade. */
    @keyframes mmhBtnRise{
      from{ opacity:0 }
      to  { opacity:1 }
    }
    /* V59 — card grids are "content": fade only. The 3D plane (perspective +
       rotateX + 120px of Z) is removed. */
    @keyframes mmhPlane{
      from{ opacity:0 }
      to  { opacity:1 }
    }
    @keyframes mmhTilt{
      from{ transform:perspective(1400px) translate3d(0,40px,-150px); opacity:.72 }
      to  { transform:none;                                           opacity:1   }
    }
  }
}

@media (prefers-reduced-motion:reduce){
  #why > .wrap, #why h2, #why .lead, #why .eyebrow::before,
  .company--bg .company-bg-photo, .company--bg .company-bg-scrim,
  .company--bg .company-lead, .company--bg .partner-card{
    animation:none !important; transform:none !important;
    translate:none !important; clip-path:none !important; opacity:1 !important;
  }
  #why .display-m, #why .display-l{ overflow:visible; }
}


/* ════════════════════════════════════════════════════════════════════════
   V30 — THE SMRT BUILT PAGE, AND THE MELBOURNE SHOWROOM BLOCK
   ════════════════════════════════════════════════════════════════════════

   ⚠ WHY /smrtbuilt/ LOOKED BROKEN, AND THE RULE THAT COMES OUT OF IT

   The client's note was "it doesn't look good at the moment". Measured on
   staging, `.smrt-split` computed to `display:block` — because
   `.smrt-split`, `.smrt-split-copy`, `.smrt-split-media` and `.smrt-lede`
   had NO CSS anywhere in the theme. Not overridden, not lost to a
   specificity fight: never written. The template had been shipping four
   class names that styled nothing, so its two-column layout fell back to
   block flow, the copy ran to a single narrow measure against a column of
   empty black, and the photographs rendered at their raw intrinsic width
   (1200px inside a 1543px section) with 800px gaps between them.

   Rule: a class name in a template is a CLAIM that a rule exists. Grep the
   stylesheet for the classes a new template introduces before shipping it —
   an unstyled class is invisible in review because block flow always
   produces *something*, and "something" is what got signed off here for
   several releases.
   ──────────────────────────────────────────────────────────────────────── */

.smrt-split{
  display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  gap:clamp(28px,4.2vw,80px);
  align-items:center;
}
/* `.reverse` swaps the visual order without moving the copy in the DOM, so
   the reading order stays copy-then-image on a screen reader and on mobile. */
.smrt-split.reverse .smrt-split-media{ order:2; }
.smrt-split.reverse .smrt-split-copy{ order:1; }

.smrt-split-copy{ min-width:0; }
.smrt-lede{ max-width:56ch; }
.smrt-lede + .smrt-lede{ margin-top:clamp(1.6rem,2.6vh,2.4rem); }
.smrt-lede .eyebrow{ display:block; }

/* The media column is a FRAME with a fixed ratio, not a bare <img>. Left to
   itself the image dictated the section's height from its own pixel
   dimensions, which is why the two sections were 1282px and 949px tall for
   the same amount of copy. */
.smrt-split-media{
  margin:0; min-width:0;
  position:relative; overflow:hidden;
  border:1px solid var(--line-gold);
  background:var(--paper-3);
  aspect-ratio:4/3;
}
.smrt-split-media img{
  width:100%; height:100%; display:block;
  object-fit:cover; object-position:center;
}

@media (max-width:900px){
  .smrt-split{ grid-template-columns:1fr; gap:clamp(22px,3.4vh,34px); }
  /* Stacked, the image belongs under the copy it illustrates on BOTH
     variants — a reversed order only means anything side by side. */
  .smrt-split.reverse .smrt-split-media,
  .smrt-split.reverse .smrt-split-copy{ order:0; }
  .smrt-split-media{ aspect-ratio:16/10; }
}

/* ---- The SMRT partner lockup on its own page ----
   The page is about SMRT BUILT and did not carry their mark anywhere. It is
   shipped byte-for-byte on the plate the brand was drawn for — the standing
   rule on this project after four rounds of trying to restyle it. */
/* Block-level with a fit-content width: `inline-flex` put the plate on the
   same line as the eyebrow that follows it and the two overlapped. */
.smrt-mark{
  display:flex; width:fit-content; align-items:center; justify-content:center;
  background:var(--paper); border-radius:3px;
  padding:clamp(12px,1.5vw,18px) clamp(18px,2.2vw,28px);
  margin-bottom:clamp(18px,2.4vh,26px);
}
.smrt-mark img{ display:block; height:clamp(30px,3.4vw,44px); width:auto; }

/* ---- Melbourne showroom block — REMOVED in V31 ----
   The V30 block that paired an address with the map is gone at the client's
   request: the address is already stated in the enquiry panel's Location item
   directly above it, and stating it twice on one page was the redundancy, not
   the fix. `.showroom-locate`, `.showroom-addr` and `.showroom-note` are
   removed with the markup rather than left behind — an unused rule is how a
   stylesheet grows a class nobody can find the markup for. */


/* V46 · client — "Why Choose Us" and "Built with experience" — "didn't
   want how it looks and the large spacings because we make these full
   screen, so revert also to the original section layout." The V31
   full-height treatment (body.home .builder-xp and .why-panel both
   min-height:100vh/100svh, the .why-grid/.why-statement/.why-aside
   restyle, and the unused OPTION B html.mmh-cover sticky-panel variant)
   is removed in full. Builder Experience is back to .bx-grid at ordinary
   section height (template-parts/builder-experience.php, unchanged);
   Why Choose Us is back to the plain .grid.two layout (tpl-home.php).
   Full history in CURRENT-STATE.md. */

/* ---- Client item 8 · the model header spec ----
   Reported as: "on smaller desktop screens, laptops, tablets, and mobile the
   property spec line inside the product header starts breaking awkwardly",
   e.g. "MPS4 · 2 Bedroom, 2 / Bathroom".

   The cause was structural, not typographic: the spec was TEXT INSIDE THE
   TITLE, so it wrapped like prose at whatever width ran out first. No amount
   of font-size tuning fixes that, because the break point moves with every
   model name and every viewport.

   Two rules together make it impossible instead of unlikely:
     1 · the spec is no longer in the title at all (see mmh_model_title)
     2 · every remaining unit — a title segment, a spec item — is its own
         nowrap span, with the separators as separate breakable elements.
   So a line can break BETWEEN "Sanctuary" and "One Bedroom", and never
   between "One" and "Bedroom". */
.md-title{ text-wrap:balance; }
.md-title-part{ white-space:nowrap; }
.md-title-sep{ padding-inline:.3em; opacity:.55; }

.md-spec{
  margin-top:.75rem;
  display:flex; flex-wrap:wrap; align-items:baseline;
  font-family:var(--mono);
  font-size:clamp(.68rem,.72vw,.76rem);
  letter-spacing:.13em;
  text-transform:uppercase;
  color:var(--gold-deep);
}
.md-spec-item{ white-space:nowrap; }
.md-spec-sep{ padding-inline:.55em; opacity:.5; }

/* The card title carries the same guarantee. Its own spec line is printed
   directly beneath it by the card, so the title never needs to state one. */
.model-body .name .md-title-part{ white-space:nowrap; }
.model-body .name .md-title-sep{ padding-inline:.28em; opacity:.55; }

/* V46 · client — "didn't want the new style on The Company and smrt built
   section on homepage." The V31-V43 pinned-scroll Company scene documented
   here (a 420vh track, a sticky panel, an animated aperture, a two-phase
   PIQUE reveal, a JS driver, a measured pin-height threshold) is removed in
   full, along with the html.mmh-scene gate that drove it — see
   mmh_scene_capability() in functions.php (removed) and the Company-scene
   driver at the end of assets/js/mmh.js (removed). The section is back to
   the flat, static band from before V32: a plain full-bleed photo, the
   copy, and the partner card — see the CSS above ("§4e: The Company as a
   full-width background band") and tpl-home.php's #about markup. Full
   history in CURRENT-STATE.md. */


/* The anchor from the nav must land on the panel, not under the fixed header. */
#about{ scroll-margin-top:clamp(72px,10vh,110px); }



/* ════════════════════════════════════════════════════════════════════════
   V33 · CLIENT — "the font colour here is still yellow" (the footer)
   ════════════════════════════════════════════════════════════════════════

   Measured on staging (already running v2.42.0, so this was not a stale
   upload): the three column headings and the two paragraphs were correctly
   #FFFFFF from the V30 white-content pass. EVERYTHING ELSE in the footer was
   `--muted-dk` #C5BAA2 — a warm khaki that reads as yellow next to white:

     the nine nav links · both showroom names · both addresses · the phone ·
     the email · the opening hours · the warranty line · the copyright

   The V30 pass lifted `p`, `li` and headings. The footer states almost none of
   its content in those elements — it uses `a`, `b.fc-shopname` and
   `span.fc-txt` — so the rule swept straight past it. That is the same class
   of miss as the enquiry panel's Perth address: a container whose content is
   not built from the elements the site-wide rule targets.

   `--muted-dk` itself is NOT repointed. It is used on ink and green grounds
   across the site where a genuine second register is wanted. Only the footer,
   which the client is looking at, moves to white.

   Gold survives exactly where it is an accent rather than body text: link
   HOVER, and the warranty numerals. */
.site-foot .foot-col a,
.site-foot .foot-contact .fc-txt,
.site-foot .foot-contact b.fc-shopname,
.site-foot .foot-bottom,
.site-foot .foot-bottom span,
/* PRIVACY and TERMS carry their own `.foot-bottom a{color:var(--muted-dk)}`
   further up the file, which out-specifies a rule on the container. The two
   links that were left khaki after the first pass. */
.site-foot .foot-bottom a,
.site-foot .warranty-nowrap{
  color:#FFFFFF;
}
.site-foot .foot-bottom a:hover{ color:var(--gold-deep); }
/* The hover still resolves to gold, which is the footer's only interaction
   signal — white-to-white would remove it. */
.site-foot .foot-col a:hover,
.site-foot .foot-contact a.fc-txt:hover{ color:var(--gold-deep); }

/* The one deliberately quieter line. Kept below the rest, but as dimmed WHITE
   rather than a warm tint, so it reads as the same ink at lower emphasis
   instead of as a second colour. */
.site-foot .fc-sub{ color:rgba(255,255,255,.66); }

/* ════════════════════════════════════════════════════════════════════════
   V33 · ONE SPACING RHYTHM FOR EVERY SECTION HEAD
   ════════════════════════════════════════════════════════════════════════
   Client: "the gaps from sub heading and heading and description are not
   consistent."

   Measured across the homepage before this change — the gap between an
   eyebrow and the heading it labels:

     Why Choose Us            266px      (eyebrow parked at the panel top)
     Built with experience     48px
     The Collection            34px
     The Company               20px

   Four sections, four rhythms, and the outlier by a factor of five was the
   one the client was looking at. The eyebrow there has been moved next to its
   heading (see tpl-home.php), and all four now take the same two numbers.

   Stated as tokens, not repeated values: a rhythm that lives in four places
   is a rhythm that drifts in four places. */
:root{
  --head-gap-eyebrow: 1.15rem;   /* eyebrow  -> heading */
  --head-gap-body:    1.4rem;    /* heading  -> body copy */
}

/* The eyebrow sits directly above its heading, everywhere — and its LINE BOX
   is pinned, which is the part that actually made the gaps differ.

   Equal margins were not producing equal gaps: measured from rendered pixels,
   three heads sat at 31px and the Collection at 49px with identical
   margin-top. The margin was never the variable — the eyebrow's own line box
   was, because it is a <p> in some heads and a <span> in others, with and
   without the gold hairline, and its height moved with the glyphs in it (the
   Collection eyebrow contains an arrow). An explicit line-height makes the box
   deterministic, so one margin gives one optical gap. */
.builder-xp .bx-lead .eyebrow,
.company-lead .eyebrow,
.sec-head .eyebrow{ display:block; margin-bottom:0; line-height:1.5; }

/* NOTE — `.sec-head` is a FLEX COLUMN carrying `gap:1.1rem`. Flex gap ADDS to
   the child margins rather than merging with them, so every head built on
   .sec-head sat 17.6px looser than the three heads that are ordinary block
   flow. That is precisely the 17.6px that survived making every computed
   margin identical: the margins were never the variable. The gap is retired
   here so the tokens above are the single source of head spacing. */
.sec-head{ gap:0; }

.builder-xp .bx-lead .eyebrow + h2,
.company-lead .eyebrow + h2,
.company-lead .eyebrow + .display-m,
.sec-head .eyebrow + h2,
.sec-head .eyebrow + .display-m,
.sec-head .eyebrow + .display-s{
  margin-top:var(--head-gap-eyebrow);
}

.builder-xp .bx-lead h2 + p,
.company-lead h2 + p,
.company-lead .display-m + .lead,
.sec-head h2 + p,
.sec-head .display-m + p,
.sec-head .display-s + p{
  margin-top:var(--head-gap-body);
}

/* V46 — the .why-panel/.why-grid/.why-aside spacing tweaks that used to sit
   here (giving the statement column's eyebrow its own top padding once it
   carried the head row) are retired along with those classes — see the V31
   full-height removal above. Why Choose Us is back to .grid.two, whose
   spacing is the same plain block flow as the section's V30 markup. */

/* ══ V34 · `hidden` MEANS NOT RENDERED ═════════════════════════════════════
   The model gallery's scheme filter was setting `hidden` on the right shots
   and nothing was disappearing.

   `.shot[hidden]{ display:none }` (line ~1289) and
   `.shot-grid--masonry .shot{ display:block }` (line ~4431) are BOTH
   specificity (0,2,0) — a class plus an attribute selector weighs exactly the
   same as two classes. Equal specificity is decided by source order, and the
   masonry rule is written 3000 lines later, so it won. The filter ran
   perfectly and the browser painted every image anyway.

   That trap is not specific to the gallery: any layout rule that sets
   `display` on an element can silently defeat `[hidden]` on it, and the
   failure looks like broken JavaScript rather than a cascade problem. So the
   fix is stated once, for the whole theme, at the end of the stylesheet:
   `hidden` is a statement that the element is NOT RENDERED, and no layout
   rule may override it. This is what a CSS reset would normally do; this
   theme's reset omitted it.

   ⚠ It follows that `hidden` may no longer be used as "invisible but still
   occupying space" anywhere. Nothing in this theme did that — every existing
   `[hidden]` rule already declared `display:none`, and one had already
   reached for `!important` for the same reason. */
[hidden]{ display:none !important; }

/* V34 — the brochures scope the kitchen and bathroom schedules to three model
   families. Stating that beside the heading is the difference between a
   specification and a promise the smaller modules cannot keep. */
/* ⚠ SPECIFICITY, not just declaration. `.fx-scope` alone is (0,1,0) and the
   theme sets body type on `.section p` shapes that outweigh it, so the first
   version of this rule rendered at full paragraph size — it applied, and lost.
   Both selectors are qualified by their container. */
.fx-card .fx-scope,
.incl-col h4 .incl-scope{
  display:block;
  font-family:var(--mono);
  font-size:.62rem;
  letter-spacing:.1em;
  text-transform:uppercase;
  color:var(--gold-deep);
  line-height:1.5;
  font-weight:500;
}
.fx-card .fx-scope{ margin:-.2rem 0 .7rem; }
.incl-col h4 .incl-scope{ display:inline-block; margin-left:.55rem; vertical-align:.1em; }
@media (max-width:640px){
  .incl-col h4 .incl-scope{ display:block; margin-left:0; }
}

/* V36 — the model page's "Key specifications" section.
   The chips now carry this model's own numbers as well as the range-wide
   ones, so there are seven rather than five; they wrap on a narrow screen. */
.fx-configs{
  list-style:none; margin:1.6rem 0 0; padding:0;
  display:grid; gap:.55rem 2rem;
  grid-template-columns:repeat(auto-fit,minmax(min(100%,270px),1fr));
}
.fx-configs li{
  position:relative; padding-left:1.15rem;
  color:var(--body-color,#fff); line-height:1.6;
}
.fx-configs li::before{
  content:""; position:absolute; left:0; top:.62em;
  width:6px; height:6px; border-radius:50%; background:var(--gold-deep);
}


/* V36 — the floor plan moved to fourth on the page, and the brochure plans are
   tall portrait sheets. Left at full width one plan is 2100px of scrolling,
   which undoes the reordering it was part of. Capped to most of a screen and
   centred; the frame is already a button, so clicking still opens it full
   size in the lightbox. */
#plans .shot-frame img{
  /* V47 fix: the base rule positions this <img> absolutely (`inset:0`), which
     takes it out of normal flow — an absolutely positioned element never
     contributes height to its parent, aspect-ratio:auto or not. Without
     `position:static` here, the frame had nothing left to size itself by and
     collapsed to zero. Static positioning lets the capped, auto-width image
     size itself normally, and the frame (aspect-ratio:auto, just above) then
     simply wraps that actual rendered size. */
  position:static;
  max-height:78vh;
  width:auto;
  max-width:100%;
  margin-inline:auto;
  display:block;
}
/* V47 — client: "Floor plans seems to have a large gap on the bottom."
   The base `.shot-frame` reserves its box height from `aspect-ratio:var(--ar)`,
   computed from the ORIGINAL image's real proportions at the frame's grid
   width. The rule above this one overrides the <img> itself to a capped,
   auto-width, normal-flow block (`max-height:78vh; width:auto`) so a tall
   brochure sheet doesn't force 2100px of scrolling — but nothing told the
   FRAME to stop reserving height for the original, uncapped proportions.
   The frame kept the old box size; the image inside it shrank to fit the
   cap; the gap between them is `var(--paper-3)` showing through as empty
   space below the plan. `aspect-ratio:auto` lets the frame collapse to
   whatever height the capped image actually renders at — no more, no less. */
#plans .shot-frame{ text-align:center; background:var(--paper-3); aspect-ratio:auto; }
@media (max-width:640px){
  #plans .shot-frame img{ max-height:none; width:100%; }
}

/* ============================================================
   V36 — BROCHURE DOWNLOAD GATE
   The short lead form shown before a visitor's first brochure download.
   Shell copied from .mmh-ytlb above (same backdrop, same z-band, same
   [hidden] contract) so the site has one modal behaviour, not two.
   ============================================================ */
.bgate{ position:fixed; inset:0; z-index:1400; display:grid; place-items:center; padding:clamp(14px,4vw,48px); }
.bgate[hidden]{ display:none; }
.bgate-backdrop{ position:absolute; inset:0; background:rgba(11,9,6,.78); backdrop-filter:blur(4px); }
.bgate-dialog{
  position:relative; z-index:1; width:min(540px,100%);
  max-height:calc(100dvh - 2*clamp(14px,4vw,48px)); overflow:auto;
  background:var(--paper); border:1px solid rgba(22,19,13,.10);
  box-shadow:0 30px 90px rgba(0,0,0,.45);
  animation:bgateIn .32s var(--ease-out) both;
}
@keyframes bgateIn{ from{ opacity:0; transform:translateY(14px) } to{ opacity:1; transform:none } }
@media (prefers-reduced-motion:reduce){ .bgate-dialog{ animation:none } }
.bgate-body{ padding:clamp(1.6rem,4vw,2.4rem); }
.bgate-body[hidden]{ display:none; }
.bgate-body .eyebrow{ display:block; margin-bottom:.5rem; }
/* V44 — h1-h4's SITE-WIDE default is color:var(--paper) (near-white), because
   almost every section on this site is a dark ground and --paper is the text
   colour that sits on it. .bgate-dialog is one of the only LIGHT cards in the
   whole design system (background:var(--paper), the same token used the other
   way round) — so the unmodified default painted this heading near-white text
   on a near-white card: not low-contrast, not legible-if-you-squint, actually
   invisible. The client's screenshot of "Where should we send it?" shows
   exactly that. Every other light-ground exception in this file (.bg-ink /
   .bg-green flipping TO --paper at line ~301) goes the direction dark-first
   sites expect; this is the one place that needed the opposite flip, and nothing
   was there to make it. */
.bgate-body h2{ margin:0 0 .7rem; color:var(--ink); }
.bgate-intro{ font-size:.98rem; line-height:1.65; color:rgba(16,24,32,.64); margin:0 0 1.4rem; }
.bgate-note{ margin-top:.9rem; }
.bgate-x{
  position:absolute; top:.55rem; right:.55rem; z-index:2;
  width:38px; height:38px; line-height:1; font-size:1.5rem;
  background:transparent; border:0; color:var(--muted); cursor:pointer; border-radius:50%;
}
.bgate-x:hover{ background:rgba(22,19,13,.07); color:var(--ink); }
.bgate-done{ text-align:center; }
.bgate-done .ty-tick{ margin:0 auto .9rem; }
/* The page behind a modal must not scroll under it. */
html.bgate-open, html.bgate-open body{ overflow:hidden; }

/* ════════════════════════════════════════════════════════════════════════
   V89 · CINEMATIC HERO DRIFT  (.kb)
   ════════════════════════════════════════════════════════════════════════
   Client card "Site-Wide 3D & Cinematic Effects", section 1: a slow Ken
   Burns movement on the hero photograph of every individual Modular Home
   page, the SMRT Town House, Can I Build? and Finance. Explicitly NOT on
   Contact Us (Perth or Melbourne) or FAQ — that card lists those three under
   "Pages to Keep Clean".

   The card asks for "a much softer/slower version than the demo — approx.
   100% -> 103-105% over 10-15 seconds", and for only the background image to
   move: heading, specs and buttons stay put. So the animation is on the <img>
   alone, never on the band, and the scrim/copy layers above it are untouched.

   ONE class, added at the three markup sites that between them cover all
   seventeen pages (model-body, townhouse-body, and the two page templates),
   rather than seventeen separate rules — the card asks for a shared solution.

   Four deliberate choices:

   1. ONE-WAY, HELD. 100% -> 104% once over 14s, then `forwards`. It does NOT
      loop or reverse. A looping Ken Burns on a hero visibly "snaps" back at
      the loop point or, if alternated, breathes in and out — both read as an
      effect. A single slow settle reads as a camera.

   2. `transform`, NOT width/height/object-position. Transform is composited;
      animating box metrics on a full-bleed hero forces layout on every frame
      and is exactly what the card's "don't allow them to interfere with page
      speed / scrolling" line is guarding against.

   3. NO `will-change`. The animation runs once, early, and then is over for
      the life of the page. A permanent `will-change:transform` would hold a
      compositor layer for the largest image on the page forever, which costs
      more memory than the 14 seconds of animation saves.

   4. The parents already clip. `.page-hero-band` and `.md-hero .m-main` are
      both `overflow:hidden` (lines ~492 and ~1346). VERIFIED before writing
      this — a 104% scale inside a non-clipping parent would push 4% of the
      photo over the section boundary and, on the band heroes, straight into
      the horizontal-overflow bug this same release just fixed elsewhere. If a
      future hero uses a different wrapper, check its overflow before adding
      .kb to it.

   Reduced motion: the image simply sits at its final 104%, so the framing is
   identical to what everyone else ends up looking at — no second layout. */
@keyframes mmh-kb{ from{ transform:scale(1); } to{ transform:scale(1.04); } }

.photo.kb{
  transform-origin:50% 50%;
  animation:mmh-kb 14s cubic-bezier(.22,.61,.36,1) both;
}

@media (prefers-reduced-motion:reduce){
  .photo.kb{ animation:none; transform:scale(1.04); }
}

/* ════════════════════════════════════════════════════════════════════════
   V90 · GOOGLE REVIEWS RESTYLE — REMOVED IN V92
   ════════════════════════════════════════════════════════════════════════
   V90 restyled the Trustindex widget in place: ~90 lines of overrides against
   .ti-widget / .ti-review-item / .ti-inner and friends, several of them forced
   with !important to beat the plugin's own !important.

   All of it is deleted, because nothing renders those elements any more. V92
   replaced the widget with the theme's own component (template-parts/reviews.php
   + inc/reviews.php), and dead overrides against a third party's class names are
   worse than no overrides: they look load-bearing to the next reader and they
   quietly rot when the plugin changes.

   The history is in the project docs. If the widget is ever reinstated, that
   block is recoverable from v2.79.15 — do not resurrect it from memory.

   RESIDUE, left deliberately: a handful of older `.trustindex-wrap` rules
   survive elsewhere in this file (the light-plate styling around line 5349, and
   several `:not(.trustindex-wrap *)` exclusions inside long shared selectors).
   They are INERT — no element carries that class any more — and unpicking them
   would mean editing a dozen complex multi-line selectors for no rendered
   change. Not worth the risk. Delete them the next time one of those selectors
   is being touched for another reason.                                        */


/* ════════════════════════════════════════════════════════════════════════
   V91 · DELIVERY NOTE AS A SUPPLIED GRAPHIC  (.statplate--img)
   ════════════════════════════════════════════════════════════════════════
   The client supplied a finished "Delivered & Installed – 12 to 14 Weeks"
   stopwatch graphic to replace the typed figure in the delivery note. Both the
   Homepage card (item 5) and the Gallery card (item 4) ask for it, and both
   pages include template-parts/delivery-note.php, so one part serves both.

   The plate chrome has to come OFF for this. `.statplate` draws a gold
   left-border, a soft gradient wash and a bottom hairline — framing that was
   right for large type on the page ground, and wrong behind a graphic that
   already carries its own dark ground, its own gold rule and its own badge
   shape. Two frames inside each other is what makes a supplied asset look
   pasted on. So this modifier strips the wash, the border and the hairline and
   lets the graphic be the object.

   Specificity note: `.statplate.delivery-note` (line ~7149) re-applies the
   padding and the gold border at (0,2,0). `.statplate--img.delivery-note` is
   also (0,2,0), so this block wins ONLY because it sits later in the file.
   That is deliberate and fragile — if these rules are ever reordered, restate
   this one with a third class rather than reaching for !important.

   The alignment rule that positions this block against the Collection heading
   selects on `.statplate`, which is why that class is kept on the element. */
.statplate--img.delivery-note{
  background:none;
  border-left:0;
  padding:0;
}
.statplate--img::after{ content:none; }

/* Cap the graphic so it reads as a plate, not a banner. Its native 1505x688
   is ~2.19:1; at 420px it lands about 420x192, which sits comfortably beside
   the Collection heading without out-shouting it. */
/* No border-radius any more — the asset is transparent as of V93, so there is
   no panel edge to round. Rounding a transparent PNG just clips the artwork's
   corners. */
.statplate-img{
  display:block;
  width:100%;
  max-width:420px;
  height:auto;
}

/* The supporting paragraph stays — the Gallery card is explicit that the text
   below the figure is kept. It loses the plate's left padding with the border,
   so it aligns to the graphic instead. */
.statplate--img .statplate-copy{ margin-top:clamp(.9rem,1.5vh,1.2rem); }

/* V108 — Max, 16 Sep: "rebalance the sub text under Our Designs on the left
   and logo on the right". The two-column split was already left/right, but the
   graphic is capped at 420px and was sitting at the LEFT edge of a column that
   is 543px wide at 1440, so it floated in the middle of the header with dead
   space to its right. It now hugs the right edge, and the caption is held to
   the graphic's width so the two share one right edge instead of the caption
   running 48px past it. Both revert to full width once the column is narrower
   than the cap, which is where the grid collapses anyway. */
.collection-head > .statplate--img .statplate-img,
.collection-head > .statplate--img .statplate-copy{
  margin-left:auto; max-width:420px;
}
@media (max-width:900px){
  .collection-head > .statplate--img .statplate-img,
  .collection-head > .statplate--img .statplate-copy{ margin-left:0; }
}

@media (max-width:640px){
  .statplate-img{ max-width:min(100%, 360px); }
}

/* ════════════════════════════════════════════════════════════════════════
   V91 · WHY MASTER MODULAR — FULL-WIDTH BACKGROUND BAND  (.why--bg)
   ════════════════════════════════════════════════════════════════════════
   Homepage card item 3: "Use Image 18 as the background image."

   Modelled on the .company--bg band that already exists rather than inventing
   a second mechanism, but with ONE deliberate difference: that band layers its
   photo and scrim at z-index -2 / -1, which only stays behind the copy because
   an unrelated rule elsewhere gives the section its own stacking context.
   Negative z-index children escape to the nearest stacking ancestor when that
   is not guaranteed, and the failure mode is the photo painting OVER the
   section background on some pages and not others.

   So this band uses `isolation:isolate` to create the stacking context
   explicitly, and positive layers inside it: photo 0, scrim 1, content 2.
   Nothing about it depends on a rule in another block.

   No `overflow:hidden` — the photo is `inset:0` with `object-fit:cover`, so it
   cannot exceed the band, and #why sits in the same document flow as the
   pinned-scroll sections below it. Adding a clip here would be a new
   containing block for no benefit.

   The scrim is weighted to the LEFT, where the heading sits, and released
   across the right where the paragraph column has the photo's darker sky
   behind it. Contrast measured after the fact, not assumed — see the V91
   note in the project docs. */
.why--bg{ position:relative; isolation:isolate; }
.why--bg .why-bg-photo{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; object-position:50% 55%; z-index:0;
}
.why--bg .why-bg-scrim{
  position:absolute; inset:0; z-index:1; pointer-events:none;
  /* Weighted left, where the heading sits, and released across the right.
     The first pass measured 8.7:1 at its WORST — comfortably past AA, and the
     photograph was mud for it. Pulled back to spend that headroom on the
     image: the dusk lighting, the pool and the planting all read now, and the
     re-measured worst case still clears 4.5:1. Contrast is a floor to clear,
     not a number to maximise. */
  background:
    linear-gradient(90deg, rgba(16,24,32,.84) 0%, rgba(16,24,32,.72) 38%, rgba(16,24,32,.50) 68%, rgba(16,24,32,.42) 100%),
    linear-gradient(180deg, rgba(16,24,32,.16) 0%, rgba(16,24,32,.04) 42%, rgba(16,24,32,.24) 100%);
}
/* Give the photograph room to be a photograph. The band was ~300px tall, which
   cropped a 16:9 dusk shot to a letterbox strip and threw away the composition.
   No `aspect-ratio` here — a min-height beside an aspect-ratio lets the browser
   derive WIDTH from height, which has bitten this project three times. */
.why--bg{ min-height:min(58vh, 460px); display:flex; align-items:center; }
.why--bg > .wrap{ flex:1 1 auto; }
.why--bg > .wrap{ position:relative; z-index:2; }

/* The band was previously on the page ground with no photo, so it never
   needed light text. It does now. */
.why--bg h2, .why--bg .lead{ color:var(--paper); }

/* ════════════════════════════════════════════════════════════════════════
   V96.3 · GOOGLE REVIEWS — REVERTED TO THE V92 COMPONENT
   ════════════════════════════════════════════════════════════════════════
   The V95 layout that stood here (a tall photograph beside a one-up
   testimonial carousel, with arrows and a counter) is GONE, and deliberately.

   Max, on the Google Reviews card: "I can see that the existing Google
   Reviews section has already been redesigned into the new dark Master
   Modular card layout, WHICH I REALLY LIKE and is a great improvement over
   the original Google widget ... I'm already very happy with the new Google
   Reviews layout currently on the site."

   The layout he is describing is V92 — the masonry of dark testimonial cards
   — because that is what was on staging when he wrote it. V95 replaced it
   after the fact while exploring alternatives he had not asked for. He was
   answering a question about where those alternatives would go, not asking
   for one of them.

   So this is V92, restored byte for byte from the 2.79.16 build. The one
   thing that does NOT revert is the "Read them on Google" link, which was a
   real defect he reported separately: see inc/reviews.php.

   The V95 markup, its slider script and its photograph are all removed with
   it. Leaving a component's CSS behind after its markup has gone is how a
   stylesheet fills up with rules that look load-bearing to the next reader.
   ════════════════════════════════════════════════════════════════════════ */

/* ════════════════════════════════════════════════════════════════════════
   V92 · GOOGLE REVIEWS — THE SITE'S OWN COMPONENT
   ════════════════════════════════════════════════════════════════════════
   Replaces the Trustindex widget and its V90 skin. Markup in
   template-parts/reviews.php, content in inc/reviews.php.

   The brief was "feels like a custom part of the website rather than a standard
   third-party review widget", with Brian Burke Homes as a DIRECTION only.
   Nothing is copied from it; this is built from this site's own tokens.

   What the widget did that this deliberately does not: a platform badge on every
   card, a verified tick beside every star row, avatar circles, and text clamped
   into a "Read more". Those five things are what a review widget looks like. The
   attribution they were carrying is not lost — it moves to .rv-summary, stated
   once and properly, with the score, the count and a link to the listing.

   Cards are set as testimonials: stars, the review in full, then the name and
   month. `align-items:stretch` (the grid default) equalises heights per ROW, so
   a nine-character review and a five-hundred-character one sit level without
   either being cut. That is where the "more whitespace" the card asks for comes
   from — it is a consequence of not truncating, not an added margin.           */

.rv-summary{
  display:flex; flex-wrap:wrap; align-items:center; justify-content:center;
  gap:.55rem clamp(.7rem, 1.6vw, 1.15rem);
  margin:clamp(1.6rem, 3vw, 2.6rem) 0 clamp(2rem, 3.6vw, 3.2rem);
}
.rv-score{
  font-family:var(--display); font-size:clamp(1.6rem, 2.6vw, 2.1rem);
  font-weight:600; color:var(--paper); line-height:1;
}
.rv-count{ color:var(--muted); font-size:.94rem; }
.rv-link{
  color:var(--gold-deep); text-decoration:none; font-size:.94rem; white-space:nowrap;
}
.rv-link:hover{ color:var(--gold-hi); }
.rv-link .arrow{ transition:transform .25s var(--ease, ease); display:inline-block; }
.rv-link:hover .arrow{ transform:translateX(3px); }

/* One star treatment for both the summary and the cards. Gold, because gold is
   this site's accent — the widget's Google-yellow was the platform's colour, and
   with the rating now attributed once and linked, the stars are ours to set. */
.rv-stars{ display:inline-flex; gap:.14em; line-height:0; }
.rv-star{ width:1.02em; height:1.02em; fill:var(--gold-deep); flex:none; }
.rv-summary .rv-star{ width:1.25rem; height:1.25rem; }

/* MASONRY, via multi-column — NOT a grid, and the reason matters.
   These reviews run from nine characters ("Amazing!!") to five hundred and
   twenty-six. A grid equalises heights per row, so the short cards were being
   stretched to match the long one and carried ~380px of dead space inside the
   border. That is a hole, not whitespace, and it looked like a bug.

   Columns let every card size to its own content and pack tightly.
   `break-inside:avoid` keeps a card whole. The trade is reading order: columns
   run DOWN then across rather than left-to-right. For unordered testimonials
   that is fine — nothing here is a sequence — and it is the right trade for
   removing the holes. Do not "fix" this back to a grid without re-checking the
   short cards. */
.rv-grid{
  list-style:none; margin:0; padding:0;
  columns:3; column-gap:clamp(1rem, 1.8vw, 1.6rem);
}
.rv-card{
  break-inside:avoid;
  /* Safari still honours the older property name in some versions. */
  -webkit-column-break-inside:avoid;
  page-break-inside:avoid;
  display:flex; flex-direction:column;
  margin:0 0 clamp(1rem, 1.8vw, 1.6rem);
  background:var(--card);
  border:1px solid var(--line-soft);
  border-radius:4px;
  padding:clamp(1.5rem, 2.3vw, 2.2rem);
  transition:border-color .3s var(--ease, ease);
}
.rv-card:hover{ border-color:var(--line-gold); }

.rv-text{
  margin:clamp(.9rem, 1.6vw, 1.15rem) 0 0;
  color:var(--ink-soft); font-size:1rem; line-height:1.8;
  /* A <blockquote> carries a UA margin and, in some stacks, quote marks. Both
     are cleared: the card IS the quotation, and decorative quote glyphs are the
     kind of ornament the brief calls gimmicky. */
  quotes:none; border:0; padding:0;
}
.rv-text::before, .rv-text::after{ content:none; }

/* `margin-top:auto` pins the attribution to the bottom of the card, so the
   names line up across a row however long the reviews are. Without it, a short
   review floats its name halfway up a stretched card. */
.rv-by{
  /* No `margin-top:auto` — that pinned the footer to the bottom of a STRETCHED
     card, which is exactly the stretching the masonry removed. */
  padding-top:clamp(1rem, 1.8vw, 1.5rem);
  display:flex; flex-direction:column; gap:.15rem;
}
.rv-name{
  font-style:normal; font-weight:600; font-size:1rem; color:var(--paper);
}
.rv-date{
  font-size:.76rem; letter-spacing:.05em; text-transform:uppercase; color:var(--muted-2);
}

@media (max-width:1000px){
  .rv-grid{ columns:2; }
}
@media (max-width:640px){
  .rv-grid{ columns:1; }
  .rv-summary{ gap:.5rem .8rem; }
}


/* ════════════════════════════════════════════════════════════════════════
   V96 · HOMEPAGE PROPERTY CHECKER  (#property-check)
   ════════════════════════════════════════════════════════════════════════
   Homepage card item 4. Copy is the client's; this is only its setting.

   The field and the button share a row and collapse to a stack under 620px.
   The input is allowed to shrink (min-width:0) because a flex item's default
   min-width is its CONTENT width, and a text input's content width is its
   `size` attribute — which is how an input in a flex row silently pushes a
   button off the end of a phone screen. It is the same class of trap as the
   headline mask above: nothing errors, the layout is just wrong. */
.pc-form{
  max-width:720px; margin:clamp(1.8rem,3.4vw,2.8rem) auto 0;
  text-align:center;
}
/* V175 — card 101, Max's 25 Sep comment: the aerial sits beside the form on
   the homepage checker, per his screenshot. Two equal columns, vertically
   centred on each other, with the four steps keeping their own full-width row
   underneath. The form's own 720px cap is released inside the column so it
   fills its half rather than sitting narrow inside it. */
.pc-top{
  display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  gap:clamp(24px,3.4vw,54px);
  align-items:center;
  /* ⚠ Bianca, 25 Sep, with a screenshot: "Can we fix the lines please".
     MEASURED, and it was not one line but two fighting each other. The fine
     print sat exactly 0px off the gold rule .cib-steprow draws, and the
     aerial's own bottom border landed 13px above that rule — two near-parallel
     horizontals a few pixels apart, which reads as one broken line rather than
     as a divider.

     This mirrors .cib-steprow's own padding-block, so the rule now carries the
     same breathing space above it as below and sits centred in its own gap
     instead of being crowded from one side. Nothing is repositioned; the row
     just stops touching the rule. */
  padding-bottom:clamp(2rem,3.6vw,3.2rem);
}
.pc-top .sec-head{ max-width:none; margin-bottom:0; }
.pc-top .pc-form{ max-width:none; }
.pc-shot{ margin:0; }
.pc-shot img{
  display:block; width:100%; height:auto;
  border:1px solid var(--line); border-radius:2px;
}
/* One column below 900, copy and form first: the address field is the action,
   the aerial is the illustration of it. */
@media (max-width:900px){
  .pc-top{ grid-template-columns:1fr; gap:clamp(20px,4vw,32px); }
}
.pc-label{
  display:block; margin-bottom:.7rem;
  font-family:var(--mono); font-size:.72rem; letter-spacing:.18em;
  text-transform:uppercase; color:var(--muted);
}
.pc-row{ display:flex; gap:.7rem; align-items:stretch; }
.pc-input{
  flex:1 1 auto; min-width:0;
  padding:.95rem 1.1rem;
  background:var(--ink-2); color:var(--paper);
  border:1px solid var(--line); border-radius:2px;
  font-family:var(--body); font-size:1rem;
}
.pc-input::placeholder{ color:var(--muted-2); }
.pc-input:focus{ outline:none; border-color:var(--gold-deep); }
.pc-input:focus-visible{ outline:2px solid var(--gold-hi); outline-offset:2px; }
.pc-go{ flex:0 0 auto; white-space:nowrap; }
.pc-fine{
  margin:.9rem 0 0;
  font-size:.82rem; color:var(--muted-2);
}

/* The five steps, as a row of five. Numbers lead, titles sit beside them —
   the descriptions stay on the Can I Build page, because this block is a
   summary of a process the visitor is about to start, not the process. */
.pc-steps{
  list-style:none; margin:clamp(2.2rem,4vw,3.2rem) 0 0; padding:0;
  display:grid; grid-template-columns:repeat(5,1fr);
  gap:clamp(.8rem,1.6vw,1.4rem);
  counter-reset:none;
}
.pc-step{
  display:flex; flex-direction:column; gap:.45rem;
  padding-top:1rem;
  border-top:1px solid var(--line-gold);
}
.pc-step-n{
  font-family:var(--mono); font-size:.72rem; letter-spacing:.16em; color:var(--gold-deep);
}
.pc-step-t{ font-size:.95rem; line-height:1.35; color:var(--paper); }

@media (max-width:900px){
  .pc-steps{ grid-template-columns:repeat(2,1fr); }
}
@media (max-width:620px){
  .pc-row{ flex-direction:column; }
  .pc-go{ justify-content:center; }
  .pc-steps{ grid-template-columns:1fr; gap:.7rem; }
  .pc-step{ flex-direction:row; align-items:baseline; gap:.8rem; padding-top:.7rem; }
}

/* ════════════════════════════════════════════════════════════════════════
   V96.4 · 3D TOWNHOUSE  (tpl-3d-townhouse.php)

   The scene is an iframe on a cross-origin domain. Nothing in here can
   reach inside it, so everything below is about the FRAME around it: the
   space it occupies, and the panel that covers it while it is empty.

   HEIGHT. An aspect-ratio box is the wrong instrument for a 3D scene. On
   a phone a 16:9 box leaves a 200px window to navigate a house through;
   on a wide monitor it grows taller than the screen and the controls
   fall off the bottom. Viewport height with a floor and a ceiling is the
   honest measure: as tall as the screen allows, never taller than is
   usable, never shorter than is navigable.

   TOKENS. Grounds are --ink / --ink-2 / --ink-3; --paper is TEXT on a
   dark ground, not a background, and --gold is for lines and fills, not
   for text (--gold-deep is the one gold for text, see the V23 note at
   the top of this file). A first pass here had --paper as a band
   background and --ink-2 as a muted text colour, which is both of those
   rules backwards.
   ════════════════════════════════════════════════════════════════════════ */

.tv-head{
  padding:clamp(2.4rem,5.4vw,4rem) 0 clamp(1.3rem,2.4vw,2rem);
  background:var(--ink);
}
.tv-section{ padding:0 0 clamp(2.6rem,5vw,4.4rem); }

.tv-stage{
  position:relative;
  width:100%;
  height:clamp(420px, 74vh, 860px);
  border:1px solid var(--line-gold);
  border-radius:14px;
  overflow:hidden;
  background:var(--ink-3);
}
.tv-frame{
  position:absolute; inset:0;
  width:100%; height:100%;
  border:0; display:block;
}

/* The stage in the browser's own full screen. The radius and the border
   are page furniture; edge to edge they read as a frame drawn on the
   glass, so both go. */
.tv-stage:fullscreen{ height:100%; border:0; border-radius:0; }
.tv-stage:-webkit-full-screen{ height:100%; border:0; border-radius:0; }

/* ── The loading panel ─────────────────────────────────────────────── */

.tv-load{
  position:absolute; inset:0;
  display:flex; align-items:center; justify-content:center;
  text-align:center;
  background:var(--ink-3);
  z-index:2;
  /* NO SCRIPT, NO TRAP. mmh.js removes this on the iframe's load event.
     If the script never runs, this animation removes it anyway: a
     visitor must never be left looking at a panel over a scene that has
     already loaded underneath it. Ninety seconds is past the worst load
     measured, so on a page whose script is working the animation never
     wins. */
  animation:tv-failsafe 1s linear 90s forwards;
}
.tv-load.is-gone{
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity .45s ease, visibility 0s linear .45s;
}
@keyframes tv-failsafe{ to{ opacity:0; visibility:hidden; pointer-events:none; } }

.tv-load-bg{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover;
  opacity:.5;
}
/* The scrim is not styling, it is the contrast. The backdrop is a real
   photograph with a bright sky in it, and white type laid straight onto
   it measured under 3:1 across the top third. */
.tv-load::after{
  /* ::AFTER, NOT ::BEFORE. Both the backdrop image and the scrim are
     positioned with no z-index, so they paint in DOM order, and ::before
     is inserted ahead of the image — the scrim went in first and the
     photograph painted straight over the top of it. The panel looked
     exactly as it had before the scrim was added, which is the kind of
     fix that reads as done and is not. */
  content:"";
  position:absolute; inset:0;
  background:linear-gradient(180deg, rgba(11,17,22,.72), rgba(11,17,22,.86));
}
.tv-load-inner{
  position:relative; z-index:1;
  padding:clamp(1.4rem,3vw,2.4rem);
  max-width:38ch;
}
.tv-load-t{
  margin:1rem 0 .45rem;
  font-family:var(--display);
  /* 400, not the 340 the display face carries everywhere else on the
     site. At this size the lighter weight rendered THINNER than the
     sentence below it, which inverts the hierarchy: the label read as
     the caption and the caption read as the heading. */
  font-weight:400;
  font-size:clamp(1.2rem,2vw,1.5rem);
  color:var(--paper);
  letter-spacing:-.01em;
}
.tv-load-d{
  margin:0;
  font-size:.92rem;
  line-height:1.55;
  color:var(--muted);
}
.tv-load-alt{
  display:inline-block;
  margin-top:1.1rem;
  font-size:.9rem;
  color:var(--gold-deep);
  text-decoration:none;
  border-bottom:1px solid var(--line-gold);
  padding-bottom:2px;
}
.tv-load-alt:hover{ color:var(--gold-hi); border-bottom-color:var(--gold-hi); }

/* An indeterminate spinner, deliberately. This page cannot read the
   scene's progress across origins, and a bar that invents a percentage
   is a lie the visitor catches the moment it sticks. */
.tv-spin{
  display:block;
  width:34px; height:34px;
  margin:0 auto;
  border:2px solid var(--line);
  border-top-color:var(--gold-deep);
  border-radius:50%;
  animation:tv-spin 1s linear infinite;
}
@keyframes tv-spin{ to{ transform:rotate(360deg); } }

@media (prefers-reduced-motion:reduce){
  .tv-spin{ animation:none; border-top-color:var(--muted-2); }
}

/* ── The bar under the stage ───────────────────────────────────────── */

.tv-bar{
  display:flex; align-items:center; justify-content:space-between;
  gap:1rem; flex-wrap:wrap;
  margin-top:1rem;
}
.tv-hint{ margin:0; font-size:.88rem; color:var(--muted-2); }
.tv-acts{ display:flex; gap:.6rem; flex-wrap:wrap; }

/* Not every browser grants an iframe full screen. The button is switched
   on by mmh.js only where the API exists, so it is never a dead control. */
.tv-full{ display:none; }
.tv-full.is-on{ display:inline-flex; }

/* ── Specs ─────────────────────────────────────────────────────────── */

.tv-specs{
  list-style:none; margin:clamp(1.8rem,3.4vw,2.6rem) 0 0; padding:0;
  display:grid; grid-template-columns:repeat(4,1fr);
  gap:clamp(.8rem,1.6vw,1.4rem);
}
.tv-specs li{
  display:flex; flex-direction:column; gap:.45rem;
  padding:clamp(1rem,1.8vw,1.4rem);
  border:1px solid var(--line);
  border-radius:10px;
  background:var(--card);
  text-align:center;
}
.tv-spec-n{
  font-family:var(--display);
  font-weight:340;
  font-size:clamp(1.4rem,2.4vw,1.9rem);
  line-height:1.1;
  color:var(--paper);
}
.tv-spec-l{
  font-family:var(--mono);
  font-size:.62rem; letter-spacing:.14em; text-transform:uppercase;
  color:var(--muted-2);
}
.tv-more{ margin:clamp(1.6rem,3vw,2.4rem) 0 0; text-align:center; }

/* V97 — Max, 15 Sep: "make the text underneath The Smrt Town House wider
   horizontally ... increase the paragraph container width".

   THE PARAGRAPH'S OWN CAP WAS NOT WHAT WAS HOLDING IT. Widening .measure to
   .measure-w on the <p> changed nothing measurable: the paragraph rendered at
   590px before and after, because its PARENT, .sec-head, caps at 60ch — 611px
   at this size — and a child cannot be wider than the box it sits in. Reading
   the width off the element you edited is how a change like this gets
   reported as done when nothing moved.

   So the container is released here, and the paragraph's own cap is set
   below. Scoped to this section, not to .sec-head globally, which sets the
   width of every centred section head on the site. The same pattern is
   already used for #property-checker on the Can I Build page. */
#detail .sec-head{ max-width:none; }
/* AND the paragraph itself, which is the half that actually bit.

   A first pass put the new width on a .measure-w class up beside .measure at
   the top of this file. It did nothing, and the reason is worth writing down:
   `.lead` is re-declared at line ~6413 WITH its own max-width:58ch, later in
   the file and at the same specificity (0,1,0), so it wins on source order
   over any bare class added earlier. The element carried .measure-w, the rule
   existed, the width never changed. Two separate caps were holding this
   paragraph and only one of them was visible from the element's class list.

   Scoped to #detail so it outranks .lead on specificity rather than on
   position, which is the same shape as #property-checker above. */
#detail .sec-head .lead{ max-width:min(100%, 780px); }

@media (max-width:860px){
  .tv-specs{ grid-template-columns:repeat(2,1fr); }
}
@media (max-width:720px){
  .tv-stage{ height:clamp(400px, 66vh, 620px); }
  .tv-bar{ justify-content:center; }
  .tv-hint{ text-align:center; width:100%; }
  .tv-acts{ width:100%; justify-content:center; }
}

/* ════════════════════════════════════════════════════════════════════════
   V97.7 · THE JOIN — EVERYTHING TAKEN BACK OUT, AND BOTH BANDS LIGHTENED
   ════════════════════════════════════════════════════════════════════════
   Bianca, 15 Sep: "It looks weirder and weirder. Can we try to lessen the
   black overlay on the hero sliding section so the background images can be
   seen clearly."

   The second sentence explains the first, and it indicts seven rounds of
   work. EVERY treatment built into this join since v2.83.0 ADDED DARKNESS —
   a fade on the hero's base, a band of ink between the sections, a mask on
   the second photograph, nine slats of ink, an angled cut with ink behind it.
   Each round made the join more elaborate and the photography dimmer, which
   is the exact opposite of what this client has asked for again and again:
   the notes at .hero-scrim record four separate requests to lighten the
   overlay, from V14 onward.

   They did not want a better transition. They wanted to see the photographs.
   A transition is what I kept hearing.

   REMOVED: .hero::after (the fade to ink at the hero's base), the .why--bg
   ink background and top margin, the .why-bg-photo mask, .why-slats in both
   markup and CSS, and the angled clip-path. The two photographs meet plainly
   again.

   ⚠ IF A TRANSITION IS ASKED FOR AGAIN: six of the seven attempts were made
   of darkness. The next one has to be made of something else, or of nothing.
   ════════════════════════════════════════════════════════════════════════ */
/* ════════════════════════════════════════════════════════════════════════
   V98 · THE WHY BAND EXPANDS ON SCROLL  (the Pique transition)
   ════════════════════════════════════════════════════════════════════════
   Bianca, 15 Sep, with three screenshots of pique.com.au and a written spec:

     "the default would be 'Why Master Modular Homes & Granny Flat' on the
      left and on the right is the image. The background would be white. And
      as you scroll down, probably just like 3 smooth scrolls, the image on
      the right would grow and be a background, then the title would be on
      the center as well as the texts ... then vice versa when you scroll up."

   That is the whole design, and it is the first brief on this join that
   described a BEHAVIOUR rather than a surface. Ten previous attempts were
   all one static picture plus decoration.

   TWO STATES:
     A · at rest    light ground, heading left, photograph as a panel right
     B · scrolled   photograph full bleed, heading and copy centred on it

   THE REVERSAL IS FREE. Every property below is driven by
   `animation-timeline: view()`, and a scroll timeline is POSITIONAL, not
   temporal — its progress is a function of where the section sits in the
   viewport, so scrolling back up runs the whole thing backwards with no
   extra code. "Vice versa when you scroll up" needs nothing written for it.

   ⚠ NO `overflow` CLIP ON THIS SECTION. An overflow-clipped element is a
   scroll container, and `view()` on its descendants would then resolve
   against a box that never scrolls and freeze at one end. That exact failure
   cost a version earlier in this build (see the V97.5 note). Nothing here
   needs a clip: the photograph is inset at state A and exactly flush at
   state B, so it never exceeds the section.

   ⚠ THE DEFAULT CSS STATE IS STATE B, NOT STATE A. Where scroll timelines
   are unsupported, an `auto`-duration animation resolves straight to its end
   state, and reduced-motion drops the animations entirely. Both land on B —
   full-bleed photograph with centred copy — which is a finished design that
   reads on its own. Landing on A instead would leave a half-empty light band
   on browsers that cannot animate it.
   ════════════════════════════════════════════════════════════════════════ */
/* ════════════════════════════════════════════════════════════════════════
   V100 · ONE SPACING TOKEN, AND THE SECTION IS PINNED
   ════════════════════════════════════════════════════════════════════════
   Bianca, 15 Sep, on the v2.94.0 resting state: "Observe spacings on all
   sides please. So from this default section, you have to smooth scroll 3x
   to see the full section."

   TWO THINGS, AND THE FIRST ONE WAS MEASURED BEFORE IT WAS TOUCHED.
   At 1900px the resting state had four different gaps: photograph 41px from
   the top, 48px from the right, 41px from the bottom, and the heading 132px
   from the left. Three separate rules had been setting them, each reasonable
   on its own — a vh-based vertical inset, a vw-based right inset, and the
   copy column's left offset plus the site's own gutter padding on .wrap.
   Nothing was wrong with any of them and the frame still read as crooked.

   --why-gap replaces all four. Every outer gap in the resting state is that
   one value, including the gutter between the copy and the photograph, and
   the section zeroes .wrap's own padding so the heading lands exactly on it
   rather than on it plus a gutter. Change the token, the whole frame moves
   together.
   ════════════════════════════════════════════════════════════════════════ */
.why-dwell{
  position:relative;
  /* THE TIMELINE LIVES HERE, ON THE THING THAT SCROLLS. The section is stuck
     to the top of the viewport for the whole transition, so a view timeline
     measured on the section would freeze at one value the moment it pinned.
     This wrapper travels normally. */
  view-timeline-name:--whyTL;
  view-timeline-axis:block;

  /* ⚠ RANGES ARE HELD IN CUSTOM PROPERTIES, DELIBERATELY.
     Pinned and unpinned are two different scroll geometries, so every range
     below has two sets of numbers. Nine rules would otherwise have to be
     restated inside the media query, and the next person to retime this would
     have to find all nine. They inherit from here; the media query redefines
     six values and nothing else.

     UNPINNED (phones, and any browser without sticky support): the wrapper is
     exactly as tall as the section.

     ⚠ THE ORDER IS DIFFERENT HERE, AND IT HAS TO BE. Stacked, the copy moves
     DOWN to the centre while the photograph grows UP from below, so the two
     cross. Measured at 390 with the desktop ordering, the heading bottomed out
     at 4.02:1 and the paragraph at 4.18:1 mid-move, both over a bright patch
     of sky, because the copy was still travelling when the photograph reached
     it and so was nowhere near the plate that is centred behind its resting
     position.

     So on a phone the copy finishes centring BEFORE the photograph's top edge
     rises: scrim up by 49, colours invert at 50, copy centred by 55, and only
     then, 55 to 58, does the photograph come up under it. Nothing crosses
     anything. */
  --r-photo-a:42%;  --r-photo-b:58%;
  --r-scrim-a:50%;  --r-scrim-b:58%;
  --r-step-a:57.9%; --r-step-b:58.1%;
  --r-copy-a:46%;   --r-copy-b:66%;
}

.why--bg{
  /* ONE TOKEN FOR EVERY GAP IN THE RESTING STATE. See the V100 note above. */
  --why-gap:clamp(24px, 3.4vw, 64px);
  position:relative;
  isolation:isolate;
  margin-block-start:0;
  display:block;
  /* SHORTER THAN THE VIEWPORT, DELIBERATELY. At 88vh the band could not be
     seen whole until it was most of the way up the screen — by which point
     the transition had already run, so the light resting state was never
     properly visible. At ~70vh the whole section is on screen for a long
     stretch of scroll, and the transition is timed to play inside that
     stretch. */
  min-height:clamp(480px, 70vh, 700px);
  /* Set once, below, and never animated. See the V102 note. */

  /* ⚠ ONE NAMED TIMELINE, DECLARED HERE AND USED BY EVERY MOVING PART.
     The first build gave each element `animation-timeline: view()`, which
     looks right and is not: `view()` measures the ELEMENT'S OWN box. The
     section, the photograph, the scrim and the copy block are four different
     boxes at four different heights, so they each started and finished at a
     different scroll position — the ground changed on cue and the copy sat
     parked at its end state the whole way, because a small vertically-centred
     box had already passed `cover 46%` before the section had.

     A named view timeline is declared once on the subject that matters and
     every descendant reads the SAME progress. All five properties now move
     as one gesture. */
  /* THE RANGE IS THE "THREE SMOOTH SCROLLS". `entry 62% cover 46%` was tried
     first and crammed the whole change into the last moment before the
     section settled — sampled at the halfway point it had not started. A
     range measured in `cover` runs against the section's own travel through
     the viewport, so 16% to 58% is a wide, even band: the change begins once
     the section is properly on screen and completes as it reaches centre.
     ⚠ THE WINDOW IS BOUNDED BY WHEN THE SECTION IS WHOLLY ON SCREEN, and
     that is a narrower band than it looks. For a band of height H in a
     viewport V, the section is fully visible only between cover H/(H+V) and
     cover V/(H+V) — at 630px in 900px that is 41% to 59%, about 275px of
     scroll. Both states have to be reachable inside it or one of them is
     never seen whole: the first timing ran to cover 80% and state B only
     arrived after the section had started leaving the top of the screen.

     43% to 58% fits the whole gesture inside the visible window. If the
     client later wants it slower, the section has to be PINNED (a tall
     wrapper with a sticky inner, as `.models-dwell` does further up this
     file) — that is the only way to buy more scroll without the section
     moving off screen. Not built here; it needs a markup change.

     ⚠ THE FIVE PROPERTIES DO NOT SHARE ONE RANGE, AND THEY CANNOT. Run
     together and linearly, the middle of this transition is unreadable: the
     ground is crossing through mid-grey, the type is crossing through
     mid-grey with it, and the photograph's left edge has already slid under
     the copy while the scrim is only a third up. Measured across the whole
     travel, the heading bottomed out at 1.00:1 — literally invisible — and
     the paragraph at 1.56:1. A cross-fade between a light state and a dark
     state passes through a state that is neither, and text cannot live there.

     So the darkening LEADS and the movement FOLLOWS:

       ground, type colour, scrim   cover 15% → 27%   fast, first
       photograph growth            cover 15% → 58%   slow, underneath
       copy move to centre          cover 24% → 58%   last

     By 27% the section is already dark with light type on it, so the
     photograph spends its whole growth under a full scrim and the copy never
     crosses a bright surface. Re-measured after: worst case on the heading
     across the entire travel is now well clear of AA. */

  /* ⚠ V102 — THE GROUND NO LONGER TURNS NAVY, AND whyGround IS GONE.
     Bianca, 15 Sep: "the image will slightly enlarge and become a background
     WITHOUT making the full section navy."

     She is right and it was always the weaker half of the idea. The navy was
     only ever there to carry light type during the middle of the move. The
     copy now cross-fades instead of travelling, so there is no middle to carry
     anything through: the type is dark on cream until it disappears, and light
     on the photograph after it comes back. The ground is cream from end to
     end, and by the time the photograph is full bleed the ground is behind it
     and cannot be seen anyway. */
  background-color:#F2F0EB;
}

/* THE PHOTOGRAPH'S GEOMETRY LIVES ON <picture>, NOT ON THE <img>.
   An absolutely positioned REPLACED element with `width:auto` uses its
   INTRINSIC width, not the width implied by its left and right offsets — so
   animating `inset` on the image moved its left edge and left it 1440px wide
   with its right offset silently ignored. The wrapper is not replaced, so it
   honours all four offsets; the image simply fills it. */
.why--bg picture{
  position:absolute; inset:0; display:block; overflow:hidden;
  z-index:0; border-radius:0;
  animation:whyPhotoGrow linear both;
  animation-timeline:--whyTL;
  animation-range:cover var(--r-photo-a) cover var(--r-photo-b);
}

/* THE PHOTOGRAPH — a panel on the right, growing to fill the band.
   `inset` interpolates as four lengths, so a clamp()-and-percentage start
   and a flat 0 end resolve through calc without a jump. */
.why--bg .why-bg-photo{
  position:static;
  width:100%; height:100%;
  object-fit:cover; object-position:50% 55%;
  display:block;
}
/* ⚠ THE LEFT EDGE MOVES LAST, AND THAT IS THE WHOLE TRICK.
   Animated as one linear inset, the photograph's left edge slides under the
   copy immediately — within a few percent of the start — so the text spends
   most of the transition over a photograph while the ground, the scrim and
   the type colour are all still mid-way. Measured, the heading bottomed out
   at 1.00:1 there: literally invisible.

   Here the photograph fills upward, right and down first and holds its left
   edge at 51% until 70% of its travel. That leaves the copy on clean ground
   for the whole first phase, so the ground and the type can invert while
   nothing is over them, and the photograph only sweeps across the copy once
   the type is already light on a dark ground. There is no moment where a
   mid-grey type sits on a mid-grey surface, because the two changes never
   overlap. */
@keyframes whyPhotoGrow{
  0%{
    /* Three sides on the one token. The fourth, the left edge, is the split
       with the copy column and is a percentage by necessity. */
    inset:var(--why-gap) var(--why-gap) var(--why-gap) 51%;
    border-radius:14px;
  }
  70%{ inset:0 0 0 51%; border-radius:0; }
  100%{ inset:0; border-radius:0; }
}

/* THE SCRIM only exists in state B, where type sits on the photograph. In
   state A the photograph carries no text at all and needs nothing over it —
   which is the answer to the standing "let the images be seen" note, for
   half the scroll at least. */
.why--bg .why-bg-scrim{
  display:block;
  position:absolute; inset:0; z-index:1; pointer-events:none;
  background:
    /* V100 — A LOCAL PLATE BEHIND THE COPY, NOT A DARKER BAND.
       Measured after the pin went in: the heading bottomed out at 1.00:1 and
       the paragraph at 3.19:1 on a phone. Both over the same thing, a bright
       patch of sky and lit ceiling that sits in the middle of this photograph
       exactly where the centred copy lands.

       The obvious answer is to raise the flat gradient underneath. That is
       the answer this project has reached for seven times and the client has
       rejected four: it darkens the whole photograph to fix a problem that
       only exists behind about a third of it.

       This is the plate the removed V91 note recommended and nobody built.
       It is an ellipse, centred where the copy is centred, fading to nothing
       well before the edges of the frame. The corners and the sides of the
       photograph are LIGHTER than they were in v2.94.0, not darker. The type
       sits on its own ground.

       It travels with the scrim, so it only exists in state B. The resting
       state still has nothing at all over the photograph. */
    radial-gradient(ellipse 72% 54% at 50% 46%,
      rgba(16,24,32,.58) 0%, rgba(16,24,32,.46) 52%, rgba(16,24,32,0) 100%),
    /* Weighted to the lower two thirds, where the centred copy sits. The
       first setting (.30/.54/.62) measured 3.81:1 on the paragraph against a
       4.5 bar once it was over the photograph — the heading passed on the
       large-text rule and the body copy did not. Raised and re-measured
       rather than eyeballed. */
    linear-gradient(180deg, rgba(16,24,32,.34) 0%, rgba(16,24,32,.66) 38%, rgba(16,24,32,.76) 100%);
  opacity:1;
  /* TWO ANIMATIONS, ONE TIMELINE, DIFFERENT RANGES. The scrim runs the same
     geometry keyframes as the photograph so it covers exactly the photograph
     and never the light ground — an earlier version darkened the whole
     section, which turned the white state grey for half the scroll. Its
     opacity rises well before the sweep, so the photograph is already dark by
     the time it passes under the copy. */
  animation:whyPhotoGrow linear both, whyScrim linear both;
  animation-timeline:--whyTL, --whyTL;
  animation-range:cover var(--r-photo-a) cover var(--r-photo-b), cover var(--r-scrim-a) cover var(--r-scrim-b);
}
@keyframes whyScrim{ from{ opacity:0; } to{ opacity:1; } }

/* THE COPY — one block, moved and re-aligned. Deliberately NOT two blocks
   cross-fading: duplicating the heading and the paragraph would put the
   section's only <h2> and its body copy into the page twice, which is a real
   cost to a screen reader and to search for a purely visual effect. */
/* ⚠ SCOPED WITH THE ID, DELIBERATELY. This section already carries a V28
   scroll choreography that resets these two elements with `#why h2 {
   animation:none }` and `#why > .wrap { animation:none }` — specificity
   (1,0,1). A class-based rule at (0,2,0) loses to that however late it sits
   in the file, so the heading and the copy block sat inert while the ground,
   the photograph and the scrim all moved correctly. The stylesheet looked
   right and half the gesture did nothing.

   `#why.why--bg` is (1,1,1) and wins. It also documents that this transition
   SUPERSEDES the older arrive-hold-depart choreography for this section: the
   two cannot both drive the same elements. */
#why.why--bg > .wrap{
  position:absolute; z-index:2;
  top:50%; transform:translateY(-50%);
  left:8%; right:8%;
  max-width:none;
  text-align:center;
  animation:whyCopy linear both;
  animation-timeline:--whyTL;
  animation-range:cover var(--r-copy-a) cover var(--r-copy-b);
}
/* ════════════════════════════════════════════════════════════════════════
   V102 · THE COPY CROSS-FADES. IT DOES NOT TRAVEL.
   ════════════════════════════════════════════════════════════════════════
   Bianca, 15 Sep: "while the image is becoming a background, the title will
   quickly fade and show on the center as well as the description (the
   transition of this should be the same transition when the heading show as
   default)."

   So: fade out where it is, reappear in the centre. The default entrance she
   is pointing at is `.reveal` — opacity plus a 22px rise on the site's own
   easing — so the centred state arrives the same way, rising 22px as it fades
   up.

   THE REPOSITION HAPPENS AT 60% OF THE RANGE, WHILE OPACITY IS 0, AND THAT IS
   THE WHOLE TRICK. `left`, `right` and `text-align` jump between two adjacent
   keyframes rather than interpolating across the range, so nothing is ever
   seen sliding. It also means the COLOUR can invert at the same instant with
   nothing on screen to catch it mid-change — which is what finally removes the
   navy ground, because the navy existed only to carry type through a middle
   state that no longer exists.

   One block, not two. Duplicating the heading and the paragraph to cross-fade
   them would put this section's only h2 and its body copy into the page twice,
   which is a real cost to a screen reader and to search for a purely visual
   effect.

   ⚠ transform CARRIES THE VERTICAL CENTRING. `.wrap` is centred with
   `translateY(-50%)`; a keyframe that sets `transform` at all replaces it, so
   every keyframe here has to restate it or the block jumps to the top. */
@keyframes whyCopy{
  0%      { opacity:1; left:var(--why-gap); right:calc(49% + var(--why-gap)); text-align:left; transform:translateY(-50%); }
  60%     { opacity:0; left:var(--why-gap); right:calc(49% + var(--why-gap)); text-align:left; transform:translateY(-50%); }
  60.001% { opacity:0; left:8%; right:8%; text-align:center; transform:translateY(calc(-50% + 22px)); }
  100%    { opacity:1; left:8%; right:8%; text-align:center; transform:translateY(-50%); }
}
/* The site's own edge gutter is removed inside this section only. It was
   adding itself to the copy column's left offset, which is why the heading
   sat at 132px while the photograph sat at 48px from its own edge. The
   section now owns every offset in the frame. */
#why.why--bg > .wrap{ padding-inline:0; }

/* The section's inner two-column grid held the heading and the paragraph
   side by side across a full-bleed band. Both states stack them now. */
.why--bg .grid.two{ display:block; }
/* ════════════════════════════════════════════════════════════════════════
   V99 · THE PARAGRAPH BELONGS TO STATE B ONLY, AND BOTH TEXTS STEP UP
   ════════════════════════════════════════════════════════════════════════
   Bianca, 15 Sep, against the v2.93.0 screenshots: "on the default settings,
   title on the left only then image only on the right / then when scrolled,
   it should be like the screenshot, as you can see the texts are on the
   center and the size went a lil bit bigger too."

   So state A is a heading and a photograph. Nothing else. The paragraph
   arrives with the centring, and both texts are larger once they are centred
   than they were in the column.

   ⚠ WHY max-height AND NOT opacity. The copy block is vertically centred as
   one box. A paragraph hidden with `opacity:0` still occupies its five lines,
   so the heading in state A would sit ~70px above the centre of the band with
   an empty column under it and no way to tell why. `max-height` with
   `overflow:hidden` takes the paragraph out of the box's measured height, so
   the heading centres on its own in state A and is LIFTED by the paragraph
   growing beneath it in state B. The lift is the reveal.

   The cap ends at 80vh, not at `none`: `none` does not interpolate. 80vh is
   past the paragraph's height at every width measured (worst case 360px wide,
   where it runs 11 lines), so the clip is fully open well before the range
   ends and nothing wipes at the finish.

   ⚠ EVERY MOVING PROPERTY ON AN ELEMENT MUST BE DECLARED IN ONE `animation`.
   Before this version the heading carried `animation:whyHeadMeasure` at
   (1,3,1) and `animation:whyInk` at (1,2,1) in two separate rules. The
   shorthand is one property: the higher-specificity rule won outright and the
   colour inversion never ran. Measured, the heading in state A was
   rgb(244,241,234) on rgb(242,240,235) — 1.01:1, invisible, and it had
   shipped that way. Both elements now carry their whole animation list in a
   single rule, and there is no other `animation` declaration for either.
   ════════════════════════════════════════════════════════════════════════ */
/* The register's own typographic values, restated. Opting out of it above
   releases the colour; without these it would also release the size and the
   line height and this paragraph would silently grow. The size is now the
   STATE A size and the keyframe carries it up. */
#why.why--bg .lead{
  margin-top:1.2rem; margin-inline:auto; max-width:74ch;
  font-size:var(--body-size); letter-spacing:normal; line-height:1.75;
  color:var(--paper);
  overflow:hidden; max-height:80vh; opacity:1;
  animation:whyLeadIn linear both, whyInk linear both;
  /* opacity belongs to the copy block above, not to this element. */
  animation-timeline:--whyTL, --whyTL;
  animation-range:cover var(--r-copy-a) cover var(--r-copy-b), cover var(--r-step-a) cover var(--r-step-b);
}
/* V102 — the paragraph no longer fades on its own; the whole copy block does.
   What is left here is the collapse, and it now OPENS at the same 60% mark as
   the reposition, while the block is invisible. Nothing is ever seen growing:
   the heading is alone on the left, then the heading and the paragraph fade up
   together in the centre. */
@keyframes whyLeadIn{
  0%      { max-height:0;    margin-top:0;      font-size:var(--body-size); }
  60%     { max-height:0;    margin-top:0;      font-size:var(--body-size); }
  60.001% { max-height:80vh; margin-top:1.2rem; font-size:calc(var(--body-size) * 1.12); }
  100%    { max-height:80vh; margin-top:1.2rem; font-size:calc(var(--body-size) * 1.12); }
}
/* max-width:none — the heading inherits a measure cap from the display type
   scale, which is right for a heading ranged left in a column and wrong for
   one centred across a full-bleed band: it stayed ~600px wide inside a
   1210px block, so it centred on its own box and read as off-centre against
   the paragraph below it. */
/* The measure cap is released with a selector deep enough to win outright.
   `#why.why--bg h2` was not: the heading still computed a 454px max-width
   from elsewhere in the cascade and centred on its own narrow box, reading
   as off-centre against the paragraph. Measured, not eyeballed — the
   computed max-width is the tell. */
/* TYPE COLOUR TRAVELS WITH THE GROUND. Dark on the light state, light on the
   photograph. Both ends measured — see the project doc for this version.
   ⚠ The colour animation is declared HERE, in the same `animation` list as
   the measure and the size, not in a separate lower-specificity rule. See the
   V99 note above for what happened when it was not. */
#why.why--bg .grid.two h2,
#why.why--bg .grid.two .display-m{
  /* STATE A size. The keyframe carries it up as the heading centres —
     "the size went a lil bit bigger too". */
  font-size:clamp(1.8rem,3.1vw,2.9rem);
  /* Capped in the resting state, where the heading is ranged left in a
     narrow column and a measure is what keeps it readable. Released as it
     centres, so it spans the band with the paragraph instead of centring on
     a box a third the width. */
  /* V105 — NO MEASURE CAP ANY MORE. It existed to force the heading to wrap
     into a narrow column; the two lines are now written into the template, so
     a cap would only fight them and wrap "Why Master Modular Homes" again. */
  max-width:none;
  color:var(--paper);
  animation:whyHeadMeasure linear both, whyInk linear both;
  animation-timeline:--whyTL, --whyTL;
  animation-range:cover var(--r-copy-a) cover var(--r-copy-b), cover var(--r-step-a) cover var(--r-step-b);
}
/* Size only. The measure is gone; see the note above. */
@keyframes whyHeadMeasure{
  from{ font-size:clamp(1.8rem,3.1vw,2.9rem); }
  to  { font-size:clamp(2.05rem,3.7vw,3.45rem); }
}
/* ⚠ THE INVERSION IS A STEP, NOT A BLEND, AND IT HAS TO BE.
   Interpolated over even a 4% window, the ground crosses through mid-grey at
   the same moment the type does, and they meet: measured, both bottomed out
   at 2.63:1 right at the crossover. That is not a tuning problem, it is what
   a cross-fade between a light state and a dark state IS — it passes through
   a state that is neither, and no single type colour is readable there.

   Stepping both at the same keyframe removes the intermediate entirely.
   Before: near-black on off-white. After: off-white on near-black. There is
   no frame in between, so there is nothing to measure badly.

   It lands mid-scroll on a plain ground — the photograph has not swept across
   the copy yet and its scrim is already up — so the swap reads as the section
   changing state, which is what it is. */
/* whyGround removed in V102. The ground is cream at every scroll position. */
@keyframes whyInk{
  0%, 49.9%  { color:#101820; }
  50%, 100%  { color:var(--paper); }
}

/* ── THE PIN ───────────────────────────────────────────────────────────
   "from this default section, you have to smooth scroll 3x to see the full
   section."

   THE ARITHMETIC, BECAUSE THE NUMBERS BELOW ARE NOT GUESSES.
   The wrapper is 100vh + T tall in a viewport V. Its view timeline runs over
   wrapperH + V = 2V + T of scroll. The inner is stuck from the moment the
   wrapper's top reaches 0 until it has travelled T, so the pinned stretch is
   cover V/(2V+T) to cover (V+T)/(2V+T).

   V103 - T IS NOW 205vh. FOUR MORE SCROLLS, AND "FOUR" IS A REAL NUMBER.
   Bianca, 15 Sep: "Lets make it 4 more total scrolls to make it more smooth
   like https://pique.com.au/"

   A wheel notch in Chrome is 100px of deltaY, and Lenis is running at
   wheelMultiplier 1, so one scroll is 100px of target travel. Four more is
   400px, which at a 900px viewport is 44.4vh. 160 + 44.4 rounds to 205vh.

   Measured after: the band is now held for 1845px, up from 1438px.

   The earlier V101 derivation stands and is why the shape is right: Pique's
   own section was opened and measured, their wrapper is 1897px in a 730px
   viewport, so their pin travel is 1.60 screens. 160vh matched them exactly;
   205vh is deliberately slower than they are, which is what was asked for.

   T = 205vh. At V=900 that is 1845px of held scroll, and 2V+T = 405vh, so the
   band is held from cover 24.7% to cover 75.3%.

   Inside it:
     24.7% -> 32%     state A, held         266px
     32%   -> 67%     the transition       1276px
     67%   -> 75.3%   state B, held         302px

   ⚠ THREE NUMBERS LAND ON 57% AND THEY MUST STAY TOGETHER: the photograph
   finishes filling the band, the copy block repositions (60% of its own
   42-to-67 range is 57), and the type colour steps. Move one and the other two
   have to move with it, or pale type ends up over cream. See the copy note.

   ONE WARNING ABOUT MEASURING THEIR PAGE, BECAUSE IT COST A PASS. The first
   probe reported that nothing on their section moves at all. It was reading
   the LEFT image, which is static, and its samples were landing mid-flight
   because ScrollSmoother lags real scroll by about a second. Screenshots
   across the pin showed the truth immediately. Look at the frames before
   trusting the rects.

   The other half of the Pique feel is that their whole PAGE scrolls with
   momentum, GSAP ScrollSmoother at smooth:1. That is handled in mmh.js and it
   is not a property of this section.

   T IS STILL THE ONLY NUMBER TO TOUCH. Raise it and re-derive the six range
   values from the two formulas above; they do not scale on their own. */
@media (min-width:901px){
  .why-dwell{
    height:305vh;
    /* V102 RETIMING, DRIVEN BY TWO OF HER NOTES.

       "the right image changed to dark" — the wash used to start rising the
       instant the panel began growing, so the raw photograph she wants at rest
       went murky while it was still an inset panel. It now starts at 46%, by
       which time the copy on the left is already fading out, and it is only
       ever over the photograph, never over the cream.

       The colour step is a 0.2% window centred on 52%, which is exactly the
       40% mark of the copy range below: the instant the block is invisible and
       repositions. Nothing on screen changes colour. */
    --r-photo-a:32%;  --r-photo-b:57%;
    --r-scrim-a:49%;  --r-scrim-b:58%;
    --r-step-a:56.9%; --r-step-b:57.1%;
    /* The copy column widens as it centres, and the photograph holds its left
       edge at 51% until 70% of its own travel, which lands at cover 56.4% on
       this timing. Starting the copy any earlier put the paragraph over the
       inset panel's edge: legible, because the scrim is already up, but
       untidy. They release together and the copy follows the panel across
       rather than reaching it. */
    /* ⚠ THE REPOSITION MUST LAND WHERE THE PHOTOGRAPH GOES FULL BLEED, AND
       THE FIRST TIMING DID NOT.

       The centred copy spans from 8% to 92% of the band and its type is light.
       The photograph arrives from the RIGHT. Measured at the first attempt:
       at the reposition the photograph's left edge was still at 973px while
       the copy started at 153px, so 800px of light type was fading up over
       cream. Invisible, and no scrim could have helped because the scrim only
       covers the photograph.

       So the photograph now finishes at 56% and the copy repositions at 56%,
       the same instant. Nothing light is ever over cream:

         34 -> 56   the photograph grows, left edge released at 49.4%
         49 -> 57   the wash rises, and only once the panel has stopped being
                    a panel, so the raw photograph is what you see while it
                    is inset
         55.8      reposition, colour step, paragraph opens, all at opacity 0
         56 -> 65   the centred block fades up over a full-bleed photograph

       Range 42 to 65 with the jump at 60% of it puts that jump at cover 55.8.
       The mobile block below uses the same 60% against its own two numbers, so
       one keyframe shape serves both. */
    --r-copy-a:42%; --r-copy-b:67%;
  }
  .why-dwell-in{
    position:sticky; top:0;
    height:100vh;
    /* ⚠ NO overflow HERE. A clipped element is a scroll container and the
       view timeline on the wrapper would then resolve against a box that
       never scrolls. That failure has already cost this build one version. */
  }
  #why.why--bg{ min-height:100vh; height:100%; }
}

/* ── Phone ─────────────────────────────────────────────────────────────
   Not pinned. Pinning a section on a phone fights the browser's own address
   bar collapse and reads as a stuck page, and there is no wheel to meter the
   gesture with. The wrapper is exactly as tall as the section here, so the
   timeline behaves as it did before the pin and the inherited ranges apply
   unchanged.

   State A's side-by-side split does not exist under 900px — a 47% text
   column is four words wide. The photograph still grows, from an inset
   panel BELOW the copy to a full-bleed background, so the gesture survives;
   only its geometry changes. */
@media (max-width:900px){
  .why--bg{ min-height:clamp(520px,80vh,720px); }
  /* V99 — the panel starts at 30%, not 54%. 54% was the top of the space the
     heading AND the paragraph took in the stacked resting state; with the
     paragraph now belonging to state B, holding the photograph down there
     left 230px of empty cream between the heading and the panel. Measured
     against the heading's three lines at 390px, which end at about 140px of a
     675px band: 30% puts 76px of air under them. */
  /* ⚠ THE TOP EDGE MOVES LAST HERE, FOR THE SAME REASON THE LEFT EDGE DOES
     ON DESKTOP. Grown as one linear inset, the photograph's top edge rises
     under the stacked heading within the first few percent, so the heading
     spends most of the transition over a photograph while it is still dark
     type and the scrim is only three quarters up. Measured at 768: the
     heading bottomed out at 1.47:1, dark ink over a bright patch of sky.

     Holding the top at 30% until 70% of the travel puts the rise at cover
     53.5%, which is after the ink step at 51%. By the time the photograph
     reaches the copy, the copy is light type on a dark ground and the scrim
     is at full. */
  /* ⚠ V160 — THE START INSET IS TIED TO THE HEADING, NOT TO A PERCENTAGE.
     Bianca, 24 Sep, with three reference shots: "there's a large space here
     ... the default on tablet and mobile should show the title and the image
     in screen".

     Measured on live at the resting state, heading bottom to photograph top:
     146px of empty cream at 512 wide, 198px at 768. Her first screenshot is
     exactly that gap.

     30% was right when it was written and is wrong now. V99 chose it against
     "the heading's THREE lines at 390px, which end at about 140px of a 675px
     band" — 30% put 76px of air under them. V105 then pinned the heading to
     TWO lines at Bianca's request, so it now ends around 100px, and the same
     30% leaves half again as much air. A percentage of the band cannot track
     a heading that changes line count, and it gets worse as the band grows:
     at 768 the band is taller, so 30% is further down and the gap is bigger.

     calc(13% + 7.5rem) instead: the 13% matches whyCopy's own state-A offset
     (see V161 below — it was 4% until the band was pinned), so the two start
     from the same line, and 7.5rem covers the two-line heading plus a small
     breath. That is a rem figure against a rem
     heading, so it tracks the type rather than the viewport. Tuned by
     measurement, not derived — see the numbers in the project doc. */
  @keyframes whyPhotoGrow{
    0%  { inset:calc(13% + 7.5rem) var(--why-gap) var(--why-gap) var(--why-gap); border-radius:12px; }
    /* 80%, not 70%: the rise starts at cover 55%, which is the moment the copy
       finishes centring. See the range note on .why-dwell. */
    80% { inset:calc(13% + 7.5rem) 0 0 0; border-radius:0; }
    100%{ inset:0; border-radius:0; }
  }
  /* The centred paragraph already runs eleven lines at this width. It takes
     the smaller step up, or state B overruns the band. */
  @keyframes whyLeadIn{
    0%      { max-height:0;    margin-top:0;    font-size:var(--body-size); }
    60%     { max-height:0;    margin-top:0;    font-size:var(--body-size); }
    60.001% { max-height:80vh; margin-top:1rem; font-size:calc(var(--body-size) * 1.05); }
    100%    { max-height:80vh; margin-top:1rem; font-size:calc(var(--body-size) * 1.05); }
  }
  /* Same cross-fade as desktop, with this width's two positions. */
  /* ⚠ V161 — STATE A STARTS AT 13%, NOT 4%, BECAUSE THE BAND IS PINNED NOW.
     4% was written for an UNPINNED band, where the section scrolled up through
     the viewport and 4% of it was nowhere near the top of the screen. V158
     pinned this band at every width so the transition could match desktop —
     and a pinned section sits at `top:0`, so 4% became 4% OF THE VIEWPORT:
     33px at 824 tall, under a 77px sticky header.

     Captured at rest after V158: the first line of the heading was hidden
     behind the header and only "& Granny Flat" was legible. That is the
     regression V158 introduced, and it is the one thing her reference shot
     shows clearly — the heading sits well clear of the header there.

     13% of 824 is 107px, which clears the 77px header with about 30px of air,
     and scales with the viewport rather than sitting at a fixed offset. The
     photograph's start inset below tracks the same 13%, so the gap under the
     heading stays the one measured in V160 rather than growing by the shift. */
  @keyframes whyCopy{
    0%      { opacity:1; top:13%; transform:none; left:clamp(12px,4vw,24px); right:clamp(12px,4vw,24px); text-align:left; }
    60%     { opacity:0; top:13%; transform:none; left:clamp(12px,4vw,24px); right:clamp(12px,4vw,24px); text-align:left; }
    60.001% { opacity:0; top:50%; transform:translateY(calc(-50% + 22px)); left:6%; right:6%; text-align:center; }
    100%    { opacity:1; top:50%; transform:translateY(-50%); left:6%; right:6%; text-align:center; }
  }
  #why.why--bg > .wrap{ top:50%; transform:translateY(-50%); }
  /* The copy fills most of the frame at this width, so the plate is wider and
     taller than the desktop ellipse. It still fades to nothing at the edges. */
  .why--bg .why-bg-scrim{
    background:
      radial-gradient(ellipse 115% 62% at 50% 46%,
        rgba(16,24,32,.60) 0%, rgba(16,24,32,.48) 58%, rgba(16,24,32,0) 100%),
      linear-gradient(180deg, rgba(16,24,32,.34) 0%, rgba(16,24,32,.66) 38%, rgba(16,24,32,.76) 100%);
  }
}

/* ── Narrow screens: the two lines win over the size step ──────────────
   "make it 2 lines only for the heading" and "the size went a lil bit bigger
   too" are both true requests and below about 600px they fight.

   Measured at 390: the resting column is 359px and the centred one is 343px.
   "Why Master Modular Homes" needs 380px at the 28.8px floor, so it wrapped to
   a third visual line, and the centred state's larger size made it worse, not
   better. There is no size at which the heading both grows AND stays on two
   lines at this width.

   So below 600px the heading holds ONE size across both states, chosen so the
   longer line fits the narrower of the two columns with room to spare. Two
   lines everywhere, which is what was asked for. The paragraph keeps its own
   small step up. */
@media (max-width:600px){
  #why.why--bg .grid.two h2,
  #why.why--bg .grid.two .display-m{ font-size:clamp(1.35rem,6.3vw,1.8rem); }
  @keyframes whyHeadMeasure{
    from{ font-size:clamp(1.35rem,6.3vw,1.8rem); }
    to  { font-size:clamp(1.35rem,6.3vw,1.8rem); }
  }
}

/* Reduced motion: no travel, state B, which is a finished design.
   ⚠ THE SELECTORS HERE HAVE TO OUT-SPECIFY THE ANIMATED RULES ABOVE.
   `.why--bg h2` is (0,2,1) and loses to `#why.why--bg .grid.two h2` at
   (1,3,1), so `animation:none` did nothing for the heading. Matched below.
   The paragraph's state B values are declared on the element itself
   (max-height:80vh, opacity:1, the centred size is the only thing lost), so
   dropping the animation leaves a readable finished section. */
@media (prefers-reduced-motion:reduce){
  .why--bg,
  .why--bg picture,
  .why--bg .why-bg-photo,
  .why--bg .why-bg-scrim,
  #why.why--bg > .wrap,
  #why.why--bg .grid.two h2,
  #why.why--bg .grid.two .display-m,
  #why.why--bg .lead{ animation:none; }
  /* Without the keyframe the heading keeps its state A measure and size, and
     state B is a centred heading across a full-bleed band. Restate both. */
  #why.why--bg .grid.two h2,
  #why.why--bg .grid.two .display-m{ max-width:none; font-size:clamp(2.05rem,3.7vw,3.45rem); }
  #why.why--bg .lead{ font-size:calc(var(--body-size) * 1.12); }
}

/* ⚠ REMOVED IN V99: A V91 SCRIM OVERRIDE SAT HERE, AT THE END OF THE FILE,
   AND IT WAS SILENTLY BEATING V98's.

   It was a left-weighted gradient (.84 down the left, .42 at the right),
   written when this band was a fixed layout with the copy ranged left over
   the photograph. Same selector as V98's, same specificity, later in the
   file — so it won, and the scrim documented forty lines above was not the
   one on screen. Nothing looked wrong, which is the problem: a left-weighted
   scrim under CENTRED copy is darkness in the wrong half of the band, and it
   is darkness this client has asked four separate times to have less of.

   The V98 gradient stands alone now. Re-measured after removal — the numbers
   are in the project doc for v2.94.0. The old note's warning still holds and
   is worth keeping: the PARAGRAPH is the constraint on this band, not the
   heading, because it is normal-size body copy against a 4.5 bar while the
   heading passes on the large-text rule. Do not lighten this scrim without
   re-measuring the paragraph across the whole travel. */

/* ════════════════════════════════════════════════════════════════════════
   V106 · EVERY HEADING ON THE SITE RUNS ON THE HERO'S CLOCK
   ════════════════════════════════════════════════════════════════════════
   Bianca, 15 Sep, pointing at the homepage hero headline: "can we use the
   transition effect of the main heading on the screenshot and apply to all
   headings on the entire website".

   THE MOVE WAS ALREADY THE HERO'S. THE CLOCK WAS NOT.
   `heroUp` and `mmhHeadWipe` are the same keyframe, translateY(103%) to 0.
   The hero runs it on a 1.1s ease-out timer with each line 120ms behind the
   one above. Every other heading ran it on `animation-timeline: view()`, which
   is linear, has no easing, and advances only as fast as the wheel turns. Same
   move, different feel, and the scroll-linked one stutters because it is
   welded to the scroll.

   These rules sit OUTSIDE the `@supports (animation-timeline: view())` block
   the old system lives in, and being later in the file they win. That is not
   incidental: it is how the same rules reach the browsers the old system
   silently skipped.

   ⚠ THE HIDDEN STATE IS BEHIND JS AND MUST STAY THERE. Both selectors below
   key off attributes only mmh.js ever writes: `data-mmh-lines` when a heading
   is split, `data-mmh-wipe` when it could not be. No script, no attribute,
   nothing hidden. A heading invisible because a script did not run is the
   worst failure this system can have, and this is the third time this file
   has had to say so.

   `clip`, never `hidden`: `hidden` makes an element a scroll container, which
   is what broke the older system in two separate ways. Nothing here reads a
   scroll timeline any more, but the rule stands for the next person.
   ════════════════════════════════════════════════════════════════════════ */

/* ── Split headings: one clipped box per rendered line ───────────────────── */
[data-mmh-lines] .mmh-ls-line{
  display:block; overflow:clip;
  padding-bottom:.12em; margin-bottom:-.07em;
}
[data-mmh-lines] .mmh-ls-line > span{
  display:block;
  transform:translateY(103%);
  /* Switches off the view()-driven animation from the older block above. */
  animation:none;
}
[data-mmh-lines].lines-in .mmh-ls-line > span{
  animation:heroUp 1.1s var(--ease-out) forwards;
}
/* The hero's own stagger, to the millisecond: .18s then 120ms a line. */
[data-mmh-lines].lines-in .mmh-ls-line:nth-child(1) > span{ animation-delay:.18s; }
[data-mmh-lines].lines-in .mmh-ls-line:nth-child(2) > span{ animation-delay:.30s; }
[data-mmh-lines].lines-in .mmh-ls-line:nth-child(3) > span{ animation-delay:.42s; }
[data-mmh-lines].lines-in .mmh-ls-line:nth-child(4) > span{ animation-delay:.54s; }
/* The hero never goes past four. A five-line heading holds the last step
   rather than drifting a second and a half behind its own first line. */
[data-mmh-lines].lines-in .mmh-ls-line:nth-child(n+5) > span{ animation-delay:.66s; }

/* ── Headings the splitter could not break up ─────────────────────────────
   Six across the theme carry a styled run inside them (a coloured <em>), and
   tearing that out of its own element to word-split it would lose the colour.
   They take the same move on the block instead of per line, on the same clock.
   No mask on these, which is how the fallback has always been: the element
   translating is also the element that would have to clip it. */
[data-mmh-wipe]{
  display:block;
  transform-origin:center bottom;
  transform:translateY(103%);
  animation:none;
}
[data-mmh-wipe].lines-in{
  animation:mmhHeadWipe 1.1s var(--ease-out) forwards;
  animation-delay:.18s;
}

/* Reduced motion never reaches here: mmh.js returns before splitting anything,
   so neither attribute is ever written. This is belt and braces for a stale
   DOM, and it restates the transform because `animation:none` alone would
   leave a heading parked at 103% and invisible. */
@media (prefers-reduced-motion: reduce){
  [data-mmh-lines] .mmh-ls-line{ overflow:visible; }
  [data-mmh-lines] .mmh-ls-line > span,
  [data-mmh-wipe]{ transform:none !important; animation:none !important; }
}

/* ---------- V117 · PLANS & DESIGN — the three equal cards ----------
   Note (K) on every model PDF: "Split this section into 3. Exterior Design,
   Floor Plan, 3D Floor Plan."

   EQUAL is the word the client used and it is the whole point, so the media box
   is a fixed 4/3 for every card whatever the source crop's own shape is, and the
   drawing is CONTAINED inside it rather than cropped to fill. A floor plan with
   its edge cut off is not a floor plan.

   The ground is white. These are fine black line drawings and the section sits
   on the dark page; on their own background they are close to invisible.

   --n2 narrows the grid instead of stretching two cards across three columns.
   Eleven of the fourteen models have no 3D floor plan yet, so two is the common
   case until those are supplied. */
.plan-trio{ display:grid; grid-template-columns:repeat(3,1fr); gap:clamp(16px,2vw,28px); }
.plan-trio--n2{ grid-template-columns:repeat(2,1fr); max-width:860px; }
.plan-trio--n1{ grid-template-columns:1fr; max-width:520px; }
/* ⚠ V173 — CARD 107, PART B. ONE PLAN CARD SIZE ON EVERY MODEL PAGE.
 *
 * Max: "the images in the Plans & Design sections are not consistent in size
 * between the different model pages... so the presentation looks uniform
 * throughout the site."
 *
 * MEASURED FIRST, AND THE OBVIOUS ANSWER WAS WRONG. The suspect was the card:
 * --n3 gives 427x320, --n2 gives 416x312. Eleven pixels. Nobody is reporting
 * an 11px difference, and had I "fixed" the card I would have shipped a change
 * that altered nothing the client can see.
 *
 * What differs is the GRID. --n2 caps at 860px inside a 1336px wrap, so on a
 * two-card page the whole Plans & Design block is a 476px-narrower island than
 * the full-bleed three-column block next door. That is the difference being
 * reported, and it lines up exactly with the four pages Max named: the two he
 * called smaller are --n2, the two he called larger are --n3.
 *
 * The second offender is the extra-plans row underneath. It takes its own
 * --nN from its own count, so Lodge Classic 1 Bed ends on a --n1 card at
 * 520x390 — 25 per cent wider than every other plan card on the site, and the
 * real outlier in the measurements.
 *
 * So both model grids become one three-column grid at full wrap width. Every
 * plan card on every model page is now the same box, main row and extra row
 * alike, whether the page has two cards or three. A two-card page fills from
 * the left and leaves the third cell empty, which is what makes it match the
 * three-card pages instead of re-centring into its own narrower block.
 *
 * ⚠ SCOPED TO A MODIFIER, NOT TO --n2 ITSELF. --n2's 860px cap still belongs
 * to the pages that ask for it, and the configurator pair deliberately opts
 * out of it through --full (see V169 below). Widening --n2 globally would
 * reach the townhouse plans and Custom Configurations, neither of which is in
 * this card. The class is added in model-body.php only.
 */
.plan-trio--model{ grid-template-columns:repeat(3,1fr); max-width:none; }
/* Three columns need the width to earn them. Measured across all 42 plan cards:
   at 1440 the card is 427 wide, at 1024 it is 303 and at 900 it is 266 — and a
   floor plan at 266px is a grey smudge you have to open the lightbox to read.
   Two columns below 1100 put it back to ~460. Consistency is across PAGES at a
   given width, which this keeps: every model page steps down together. */
@media (max-width:1100px){
  .plan-trio--model{ grid-template-columns:repeat(2,1fr); }
}
.plan-trio .plan-card{ margin:0; }
.plan-trio .plan-card-media{
  display:block; width:100%; aspect-ratio:4/3; background:#fff;
  border:1px solid var(--line-gold); padding:clamp(10px,1.4vw,18px);
  /* It is a <button> so the lightbox and the keyboard can both reach it, which
     means the browser's own button styling has to be neutralised here. */
  cursor:zoom-in; font:inherit; color:inherit; text-align:inherit;
  transition:border-color .35s var(--ease);
}
.plan-trio .plan-card-media:hover{ border-color:var(--gold); }
.plan-trio .plan-card-media:focus-visible{ outline:2px solid var(--gold); outline-offset:3px; }
.plan-trio .plan-card-media img{ width:100%; height:100%; object-fit:contain; display:block; }
/* V118 — the card foot, matched to the client's example on page 25 of the
   Lodge Classic PDF: a numbered badge, a centred label, a short gold rule under
   it, and the description beneath. The badge sits ON the join between the image
   and the caption, as it does in the example. */
.plan-trio .plan-card figcaption{
  display:block; text-align:center; position:relative;
  margin-top:0; padding-top:1.9rem;
}
/* A card with no number badge does not need the room the badge sits in. */
.plan-trio .plan-card figcaption:not(:has(.plan-card-num)){ padding-top:1.1rem; }
.plan-trio .plan-card-num{
  position:absolute; top:-18px; left:50%; transform:translateX(-50%);
  width:36px; height:36px; border-radius:999px;
  display:flex; align-items:center; justify-content:center;
  background:var(--ink-2); border:1px solid var(--gold);
  font-family:var(--mono); font-size:.66rem; letter-spacing:.06em; color:var(--gold-glow);
}
.plan-trio .plan-card-label{
  display:block; font-family:var(--mono); font-size:.66rem; letter-spacing:.16em;
  text-transform:uppercase; color:var(--gold-deep);
}
.plan-trio .plan-card-label::after{
  content:""; display:block; width:26px; height:1px; background:var(--gold);
  opacity:.8; margin:.6rem auto 0;
}
.plan-trio .plan-card-note{
  display:block; margin:.7rem auto 0; max-width:30ch;
  font-size:.86rem; line-height:1.6; color:var(--muted); letter-spacing:0;
  text-transform:none; font-family:var(--body);
}
/* ⚠ V169 — A ONE-COLUMN STACK OF plan-cards, FOR A CARD THAT IS ALREADY HALF
   THE PAGE. Custom Configurations puts two drawings inside a `.cfg-card`, and
   `.cfg-grid` is two columns from 900px up. Side by side inside that, each
   drawing measures about 300px, and a floor plan at 300px is unreadable — the
   room labels and door swings are the whole reason the card exists.

   No max-width and no auto margins, unlike --n1: this stack fills the card it
   sits in rather than centring inside it. */
.plan-trio--stack{ grid-template-columns:1fr; gap:clamp(18px,2.4vw,30px); }
/* ⚠ AND IT NEEDS THE CARD'S OWN INSET, which the treatment it replaced did
   not. `.cfg-shot` ran the artwork edge to edge inside `.cfg-card` — right to
   the border, because a dark plate over a photo wants the full bleed. A
   plan-card is a WHITE box, and taken to the same edges it stops reading as a
   card floating on the ground (which is what the Lodge trio does, and what she
   pointed at) and starts reading as a white panel bolted into a dark frame.
   Caught on the rendered page, not in the markup.

   The inline padding matches `.cfg-body` below it, so the drawings, the
   heading and the button all line up on one edge. */
.cfg-card .plan-trio--stack{ padding:clamp(20px,2.4vw,30px) clamp(20px,2.4vw,30px) 0; }

/* ⚠ V169 — NO 860px CAP ON THE CONFIGURATOR PAIR. `--n2` caps at 860px, which
   suits the Lodge's portrait plans: two tall drawings side by side do not want
   the whole page. The configurator's two are a landscape render and a
   configuration SHEET whose module dimensions are set in about 6px type. The
   cap put each card at 416px, against the 660px the old `.config-grid` gave
   them — measured — and at 416px the sheet's figures are unreadable without
   opening the lightbox. Drawings that carry dimensions get the full width. */
.plan-trio--full{ max-width:none; }
/* ⚠ V171 — THE SUBGRID IS GONE, AND SO IS THE REASON FOR IT.
   V169 gave these cards a per-image aspect-ratio so the wide drawings would
   not letterbox. That made the two media boxes different heights, which
   knocked the numbered badges out of line, which needed subgrid to fix, which
   put a 28px row gap between the box and its caption — so the badge floated
   24px BELOW the card instead of straddling its edge. Three mechanisms, all
   downstream of the first decision, and the first decision was wrong.

   `.plan-card-media` is WHITE and the artwork sits on white, so `contain`
   letterboxing inside the Lodge's 4/3 box is INVISIBLE — there is no band to
   avoid. Measured: a 1.46:1 render in a 654px-wide 4/3 box still draws 618px
   across, exactly as it did at its own ratio. Nothing is lost, every card is
   the same height again, the badges align by themselves and sit ON the join
   exactly as they do on the Lodge pages.

   Bianca, 24 Sep: "the style of plans here is very different to the plan
   style from lodge-classic-two-bedroom". Measured live: badge 24px below the
   box here, overlapping it by 18px there. Same component, two different
   cards. */

@media (max-width:760px){
  .plan-trio,.plan-trio--n2{ grid-template-columns:1fr; max-width:520px; margin-inline:auto; }
  /* --stack is already one column and is inside a card that has its own
     width; the shared rule above must not put a 520px cap on it. */
  .plan-trio--stack{ max-width:none; margin-inline:0; }
}

/* ════════════════════════════════════════════════════════════════════════════
   V119 · SITE-WIDE 3D & CINEMATIC EFFECTS
   Client card "Site-Wide 3D & Cinematic Effects", sections 2 to 6.
   Section 1 (the Ken Burns hero drift) shipped in V89 and is above, at .kb.
   Section 7 (the About Us team photograph) is held: the photograph is still
   pending and the card says to complete that part last.

   The card's direction is "subtle, smooth, premium, architectural, cinematic
   — not strong, flashy or gimmicky", so every number here is at the quiet end
   of the range it asks for, and every effect is bought with `transform` and
   `opacity` alone. Nothing in this block animates a box metric, a filter or a
   shadow on a moving element, because those are what cost frames, and the
   card's own list of things not to interfere with starts with page speed.

   Every effect here is off under prefers-reduced-motion, and every cursor
   effect is off on coarse pointers. The tilt engine in mmh.js does not even
   bind in those two cases; the rules below are the belt to that braces, for
   the styling the engine does not own.
   ════════════════════════════════════════════════════════════════════════════ */

/* ── The shared tilt surface ──────────────────────────────────────────────
   mmh.js writes the rotation inline. This supplies the rest: the resting
   state, the return, and the flat fallback. `transform-style:preserve-3d` is
   what lets the parallax layers below sit at real depths inside the rotation
   rather than being flattened into it. */
[data-tilt]{
  transform-style:preserve-3d;
  transition:transform .55s var(--ease-out), border-color .5s var(--ease), box-shadow .5s var(--ease);
}
/* While the cursor is over it the element must follow the cursor, not trail
   half a second behind it. Short, linear, no easing: easing a value that is
   already being updated every frame reads as lag. */
[data-tilt].is-tilting{ transition:transform .1s linear; }

/* ── 4 + 5 · THE MODEL CARDS ──────────────────────────────────────────────
   Section 5: "Replace the current basic upward/lift hover effect on the home
   cards with this smoother 3D tilt." So the -4px lift goes, on any card that
   tilts. The gold border and the shadow stay — they are the existing card
   design, and the card says to keep it. */
.model-card[data-tilt]:hover{ transform:none; }
.model-card[data-tilt]{ will-change:auto; }

/* Section 4, the six homepage cards: "allow the image/card elements to sit at
   slightly different depths and respond subtly to cursor movement". The photo
   pushes toward the viewer and drifts WITH the cursor; the text block sits
   further back and drifts against it, by a third as much. That opposition is
   what reads as depth rather than as the whole card sliding.
   --tilt-x / --tilt-y are written by mmh.js, -1 to 1, and are 0 at rest, so
   the card is perfectly still until a cursor is on it. */
.model-card[data-tilt-parallax]{ --tilt-x:0; --tilt-y:0; }
.model-card[data-tilt-parallax] .model-media{
  transform:translate3d(calc(var(--tilt-x) * 7px), calc(var(--tilt-y) * 7px), 26px);
  transition:transform .55s var(--ease-out);
}
.model-card[data-tilt-parallax] .model-body{
  transform:translate3d(calc(var(--tilt-x) * -3px), calc(var(--tilt-y) * -3px), 12px);
  transition:transform .55s var(--ease-out);
}
.model-card[data-tilt-parallax].is-tilting .model-media,
.model-card[data-tilt-parallax].is-tilting .model-body{ transition:transform .1s linear; }
/* The media box already clips (overflow:hidden), so a layer pushed forward
   inside it cannot escape the card and cannot reach the page edge. Verified
   before writing this: .model-media is overflow:hidden at line ~1164. */

/* ── 2 · THE GALLERY FRAMES ───────────────────────────────────────────────
   The tilt sits on the existing frame button. Layout, aspect ratio, caption
   and the scheme filter are all untouched: the card says to keep them. */
.shot-frame[data-tilt]{ transform-origin:50% 50%; }

/* The townhouse gallery tiles had no enlarge control at all. This is the
   frame that gives them one, sized to the tile it fills so the existing
   .photo (position:absolute; inset:0) lands exactly where it already did. */
.gal-frame{
  position:absolute; inset:0; z-index:2;
  padding:0; margin:0; border:0; background:none;
  display:block; width:100%; height:100%;
  cursor:zoom-in;
}
.gal-frame:focus-visible{ outline:2px solid var(--gold); outline-offset:-3px; }

/* ── 2 · THE CINEMATIC EXPANSION ──────────────────────────────────────────
   While the image is in flight between the tile and the viewer, the FLIP in
   mmh.js owns its transform outright. The stylesheet's own open transition
   (transform:scale(.985) -> none) has to stand down for that moment or the
   two write to the same property and the flight stutters at its first frame. */
.mmh-imglb.is-flip .mmh-imglb-figure img{ transition:none; transform:none; }
/* The caption and the arrows belong to the viewer, not to the tile, so they
   fade with the backdrop rather than flying with the image. */
.mmh-imglb-figure figcaption,
.mmh-imglb-nav,
.mmh-imglb-x{ transition:opacity .3s var(--ease) .1s, background-color .25s var(--ease), border-color .25s var(--ease); }
.mmh-imglb:not(.is-open) .mmh-imglb-figure figcaption,
.mmh-imglb:not(.is-open) .mmh-imglb-nav,
.mmh-imglb:not(.is-open) .mmh-imglb-x{ opacity:0; }

/* ── 3 · THE MAGNIFIER ON TECHNICAL DRAWINGS ─────────────────────────────
   The loupe itself is the existing .mag-lens. All this adds is the cursor,
   so a plan announces that it can be inspected before anything is clicked.
   Hover-capable pointers only, because the loupe is a hover behaviour. */
@media (hover:hover) and (pointer:fine){
  .plan-card-media[data-mag]{ cursor:zoom-in; }
}

/* ── The cinematic image reveal ───────────────────────────────────────────
   Soft fade plus a slight depth entrance, 98% -> 100%, riding the existing
   .reveal observer in mmh.js rather than a second scroll listener. It is the
   movement the card describes in section 6 and the one section 7 asks for on
   the About Us team photograph when that photograph is supplied.

   .cine only CHANGES .reveal's resting transform, so an element that already
   reveals keeps its timing, its delay classes and its reduced-motion escape. */
.reveal.cine{
  opacity:0; transform:scale(.98);
  transition:opacity .85s var(--ease), transform 1.05s var(--ease-out);
}
.reveal.cine.in{ opacity:1; transform:none; }

/* ── 6 · OUR PROCESS ──────────────────────────────────────────────────────
   "Keep the existing interactive step/image functionality. Improve the
   transition between the large process images so it feels more cinematic/3D
   rather than simply switching images. Something like soft fade + slight
   depth/scale (approx. 98% -> 100%) is enough."

   Taken literally: the fade is the one that was already there, lengthened a
   little, with 98% -> 100% under it. The step logic, the captions and the
   scroll pinning are not touched. No tilt here — the card says none is
   needed. */
.proc-photo{
  transform:scale(.98);
  transition:opacity .72s var(--ease), transform .9s var(--ease-out);
}
.proc-photo.is-active{ transform:scale(1); }

/* ── The two switches the card asks for ──────────────────────────────────
   Coarse pointer: "Desktop cursor effects can use a clean/static or
   simplified treatment on mobile/tablet." Static, then. */
@media (hover:none), (pointer:coarse){
  [data-tilt]{ transform:none !important; transition:none; }
  .model-card[data-tilt-parallax] .model-media,
  .model-card[data-tilt-parallax] .model-body{ transform:none; transition:none; }
}
/* Reduced motion: everything in this block rests at its final state. Nothing
   is left mid-animation and no layout differs from anyone else's. */
@media (prefers-reduced-motion:reduce){
  [data-tilt],
  [data-tilt].is-tilting{ transform:none !important; transition:none; }
  .model-card[data-tilt-parallax] .model-media,
  .model-card[data-tilt-parallax] .model-body{ transform:none; transition:none; }
  .proc-photo{ transform:none; transition:opacity .01s linear; }
  .proc-photo.is-active{ transform:none; }
  .mmh-imglb-figure figcaption,
  .mmh-imglb-nav,
  .mmh-imglb-x{ transition:none; }
}

/* ════════════════════════════════════════════════════════════════════════════
   V120 · BIANCA'S 17 SEPTEMBER PASS
   Nine items. The four handled by editing rules in place are marked V120 where
   they sit: the text shadows (~line 5100), the Our Process alignment (~2619),
   the .proc track count (~1238) and the shadow backstop. The rest are here.
   ════════════════════════════════════════════════════════════════════════════ */

/* ── THE FINANCE CHART IS NO LONGER TALLER THAN THE VIEWPORT ──────────────
   "On Finance page, I think the graph it too big in terms of height that I
   need to scroll more to see all of the graph."

   Measured before changing it: 628 x 838 at 1440. The chart sits in
   `.split-media.tall`, which is 3/4 — a portrait frame built for photography,
   not for a chart whose content is a row of bars. At 838px it is taller than
   most laptop viewports, so the top of it scrolls away before the bottom
   arrives, which is exactly what she is describing.

   4/3 instead of 3/4, and capped at 62vh so it can never outgrow the screen on
   a short window. `.split` centres its columns, so the copy beside it stays
   level. Scoped to the finance chart by id, so no photograph on the site
   changes shape. */
#finance .split-media.tall{
  aspect-ratio:4/3;
  max-height:62vh;
}
/* preserveAspectRatio is "slice" on this SVG, so a wider, shorter frame crops
   the drawing rather than distorting it. The bars and the 5% are in the lower
   two thirds of the viewBox; this keeps the composition weighted to them. */
#finance .split-media.tall > svg{ width:100%; height:100%; display:block; }

/* ── THE CINEMATIC HERO DRIFT, ON EVERY PAGE ─────────────────────────────
   "The site wide effects and transitions should also apply to all pages."

   This supersedes the client card's "Pages to Keep Clean" list, which held the
   drift off Contact Perth, Contact Melbourne and FAQ. Newest instruction wins.

   Twelve page templates carry a `.page-hero-band` hero and none of them had
   the class: About, Configurator, Contact Perth, Contact Melbourne, FAQ, How
   It's Done, Privacy, Our Process, SMRT Built, Terms, Townhouses and What Sets
   Us Apart. Adding the drift by SELECTOR rather than by editing twelve files
   means a thirteenth page built tomorrow gets it for nothing, which is what
   "site wide" has to mean to stay true.

   `.page-hero-band` is overflow:hidden — checked before writing this, same as
   at V89 — so the 104% scale is clipped and cannot push the page sideways. */
.page-hero-band > .photo,
.cib-hero > .photo{
  transform-origin:50% 50%;
  animation:mmh-kb 14s cubic-bezier(.22,.61,.36,1) both;
}
@media (prefers-reduced-motion:reduce){
  .page-hero-band > .photo,
  .cib-hero > .photo{ animation:none; transform:scale(1.04); }
}

/* ── THE HOVER, REWORKED FOR A PREMIUM READ ──────────────────────────────
   "The hover effect on modules tilting looks weird, any effect to improve
   more that looks premium?"

   The reasoning is in mmh.js above the tilt engine. In short: rotating a card
   that contains TYPE shears the type, and that is what reads as cheap. The
   rotation drops from 3 degrees to 1.2, and the depth is carried instead by a
   photograph that moves inside a frame that already clips, a specular sheen
   that tracks the cursor, and a long, slow lift.

   Every value here is on transform, opacity or a compositor-friendly filter.
   Nothing animates a box metric. */

/* The sheen. One pseudo-element, one radial gradient, positioned from the
   --mx / --my the engine publishes. It sits above the photograph and below the
   captions and chips, which all carry z-index 3. */
.model-media,
.shot-frame,
.gal-frame{ --mx:50%; --my:50%; }

.model-card[data-tilt] .model-media::after,
.shot-frame[data-tilt]::after,
.gal-frame[data-tilt]::after{
  /* ⚠ `content` CARRIES !important ON PURPOSE. Line ~1624 kills both pseudo
     elements on every .shot-frame with `content:none !important`, which is a
     deliberate reset of an older frame treatment. Without matching it here the
     sheen simply never renders on the galleries — and it renders on the cards,
     so it would have looked like it worked. */
  content:"" !important; position:absolute; inset:0; z-index:2; pointer-events:none;
  background:radial-gradient(38% 46% at var(--mx) var(--my),
    rgba(255,255,255,.17) 0%, rgba(255,255,255,.06) 42%, rgba(255,255,255,0) 72%);
  opacity:0; transition:opacity .55s var(--ease);
  mix-blend-mode:soft-light;
}
.model-card[data-tilt].is-tilting .model-media::after,
.shot-frame[data-tilt].is-tilting::after,
.gal-frame[data-tilt].is-tilting::after{ opacity:1; transition:opacity .3s var(--ease); }

/* The photograph moves, the type does not. The frame already clips, so the
   image can be oversized and drift without anything escaping the card. */
.model-card[data-tilt] .model-media > picture > img,
.model-card[data-tilt] .model-media > img,
.shot-frame[data-tilt] > img,
.gal-frame[data-tilt] > img{
  transform:scale(1) translate3d(0,0,0);
  transition:transform 1s var(--ease-out);
  will-change:auto;
}
.model-card[data-tilt].is-tilting .model-media > picture > img,
.model-card[data-tilt].is-tilting .model-media > img,
.shot-frame[data-tilt].is-tilting > img,
.gal-frame[data-tilt].is-tilting > img{
  transform:scale(1.045)
    translate3d(calc(var(--tilt-x) * -9px), calc(var(--tilt-y) * -9px), 0);
  transition:transform .45s var(--ease-out);
}

/* The lift. Slower and softer than the old 4px hop, and it comes back on the
   card rather than on the rotation, so it survives the smaller tilt. */
.model-card[data-tilt]{
  transition:transform .7s var(--ease-out), border-color .6s var(--ease), box-shadow .7s var(--ease);
}
.model-card[data-tilt].is-tilting{
  border-color:var(--line-gold);
  box-shadow:0 44px 80px -46px rgba(0,0,0,.8);
}

/* The parallax depths come down with the rotation. At 1.2 degrees a 26px push
   toward the viewer is more than the perspective can carry without the layer
   looking detached from its own card. */
.model-card[data-tilt-parallax] .model-media{
  transform:translate3d(calc(var(--tilt-x) * 4px), calc(var(--tilt-y) * 4px), 14px);
}
.model-card[data-tilt-parallax] .model-body{
  transform:translate3d(calc(var(--tilt-x) * -2px), calc(var(--tilt-y) * -2px), 7px);
}

@media (hover:none), (pointer:coarse){
  .model-card[data-tilt] .model-media::after,
  .shot-frame[data-tilt]::after,
  .gal-frame[data-tilt]::after{ display:none; }
  .model-card[data-tilt] .model-media > picture > img,
  .model-card[data-tilt] .model-media > img,
  .shot-frame[data-tilt] > img,
  .gal-frame[data-tilt] > img{ transform:none; transition:none; }
}
@media (prefers-reduced-motion:reduce){
  .model-card[data-tilt] .model-media::after,
  .shot-frame[data-tilt]::after,
  .gal-frame[data-tilt]::after{ display:none; }
  .model-card[data-tilt] .model-media > picture > img,
  .model-card[data-tilt] .model-media > img,
  .shot-frame[data-tilt] > img,
  .gal-frame[data-tilt] > img{ transform:none !important; transition:none; }
}

/* ── THE FAQ PAGE, RELAID ─────────────────────────────────────────────────
   "Any way we can improve the FAQ page design layout?"

   What was wrong, measured rather than felt: 27 questions in 8 categories as
   one column 2,586px tall, each question set on a 1,336px line, and no way to
   reach a category except scrolling past every category above it. Long line
   lengths and no wayfinding.

   Three changes, no copy touched:
     1. A sticky category index down the left. Eight categories, always
        visible, marking the one being read.
     2. The questions come back to a readable measure beside it.
     3. The rows get a proper open state so the page shows you where you are.

   The index is built by mmh.js from the template's own <h2>s. With JavaScript
   off this whole block collapses to the single column the page is today, which
   is why the grid is declared on .is-built rather than on .faq-layout. */
.faq-layout{ display:block; }
.faq-index{ display:none; }

@media (min-width:1000px){
  .faq-layout:has(.faq-index.is-built){
    display:grid;
    grid-template-columns:minmax(210px,258px) minmax(0,1fr);
    gap:clamp(36px,4.5vw,84px);
    align-items:start;
  }
  .faq-index.is-built{
    display:block;
    position:sticky;
    /* clears the fixed header, then a little air */
    /* The theme has no header-height token, so this is the measured height of
       the fixed header plus air, not a variable that would silently fall back. */
    top:104px;
  }
}

.faq-index-list{ list-style:none; margin:0; padding:0; border-left:1px solid var(--line); }
.faq-index-link{
  display:block; padding:.62rem 0 .62rem 1.05rem;
  margin-left:-1px; border-left:2px solid transparent;
  font-size:.86rem; line-height:1.35; color:var(--muted-2);
  text-decoration:none;
  transition:color .35s var(--ease), border-color .35s var(--ease);
}
.faq-index-link:hover{ color:var(--paper); border-left-color:var(--line-gold); }
.faq-index-link.is-current{ color:var(--gold); border-left-color:var(--gold); }

/* The questions were centred in a 1,336px wrap; inside the grid they take the
   column, and the accordion's own 820px cap becomes a floor rather than a
   centring device. */
.faq-body{ min-width:0; max-width:78ch; }
.faq-body .faq-accordion{ max-width:none; margin-inline:0; }
.faq-body .faq-cat + .faq-cat{ margin-top:clamp(40px,4vw,66px); }
.faq-body .faq-cat > h2{ scroll-margin-top:100px; }

/* The open row. It used to look identical to a closed one apart from the sign,
   so on a 27-row page you lose your place the moment you scroll. */
.faq-body .qa{ transition:background-color .4s var(--ease); }
.faq-body .qa[open]{
  background:color-mix(in srgb, var(--paper) 4%, transparent);
  box-shadow:inset 2px 0 0 var(--gold);
}
.faq-body .qa[open] > summary{ color:var(--paper); }
.faq-body .qa[open] > summary,
.faq-body .qa[open] .a{ padding-left:1.15rem; }
.faq-body .qa > summary,
.faq-body .qa .a{ transition:padding-left .4s var(--ease); }

@media (prefers-reduced-motion:reduce){
  .faq-index-link,
  .faq-body .qa,
  .faq-body .qa > summary,
  .faq-body .qa .a{ transition:none; }
}

/* ════════════════════════════════════════════════════════════════════════════
   V121 · THE HOVER, THIRD TIME — AND IT NO LONGER FOLLOWS THE CURSOR
   "I still don't like the hover style on the module, update it one more time.
   it looks and hovers weird."

   The reasoning is in mmh.js above the tilt engine. Short version: reducing
   the angle was treating the symptom. A card that rotates under the pointer is
   chasing it, so it never settles, and the eye tracks that the whole time.
   The card is now still. It changes state when you arrive and when you leave,
   and the only thing that follows the cursor is the light.

   Everything below overrides the V119/V120 rules above it rather than editing
   them, so the whole experiment is one block that can be lifted out.
   ════════════════════════════════════════════════════════════════════════════ */

/* The card itself: no rotation, no perspective, no per-frame writes. A lift
   and a shadow on a long ease. */
.model-card[data-tilt],
.model-card[data-tilt].is-tilting{
  transform:none;
  transition:transform .6s var(--ease-out), border-color .6s var(--ease), box-shadow .6s var(--ease);
}
.model-card[data-tilt].is-tilting{
  transform:translate3d(0,-6px,0);
  border-color:var(--line-gold);
  box-shadow:0 40px 74px -44px rgba(0,0,0,.82);
}

/* The layered parallax goes with it. The layers keep their resting depth so
   the card still has structure, but they no longer slide against the pointer:
   that was the same chasing problem one level down. */
.model-card[data-tilt-parallax] .model-media,
.model-card[data-tilt-parallax].is-tilting .model-media,
.model-card[data-tilt-parallax] .model-body,
.model-card[data-tilt-parallax].is-tilting .model-body{
  transform:none;
  transition:none;
}

/* The photograph. One slow scale, no drift — the drift was the card-chase in
   miniature. 1.06 over three quarters of a second reads as a slow push in. */
.model-card[data-tilt] .model-media > picture > img,
.model-card[data-tilt] .model-media > img,
.shot-frame[data-tilt] > img,
.gal-frame[data-tilt] > img{
  transform:none;
  transition:transform .8s var(--ease-out);
}
.model-card[data-tilt].is-tilting .model-media > picture > img,
.model-card[data-tilt].is-tilting .model-media > img,
.shot-frame[data-tilt].is-tilting > img,
.gal-frame[data-tilt].is-tilting > img{
  transform:scale(1.06);
  transition:transform .9s var(--ease-out);
}

/* The sheen, softened and widened. It is now carrying the effect on its own,
   so it should read as a sweep of light rather than as a hotspot: a wider
   gradient, lower peak, and it fades in rather than snapping. */
.model-card[data-tilt] .model-media::after,
.shot-frame[data-tilt]::after,
.gal-frame[data-tilt]::after{
  background:radial-gradient(60% 72% at var(--mx) var(--my),
    rgba(255,255,255,.13) 0%, rgba(255,255,255,.05) 46%, rgba(255,255,255,0) 78%);
  transition:opacity .6s var(--ease);
}
.model-card[data-tilt].is-tilting .model-media::after,
.shot-frame[data-tilt].is-tilting::after,
.gal-frame[data-tilt].is-tilting::after{ transition:opacity .45s var(--ease); }

/* The gallery frames get the same still treatment: they are photographs, so
   the scale alone is enough and the frame does not need to move at all. */
.shot-frame[data-tilt],
.shot-frame[data-tilt].is-tilting,
.gal-frame[data-tilt],
.gal-frame[data-tilt].is-tilting{ transform:none; }

@media (prefers-reduced-motion:reduce){
  .model-card[data-tilt].is-tilting{ transform:none; box-shadow:none; }
  .model-card[data-tilt].is-tilting .model-media > picture > img,
  .model-card[data-tilt].is-tilting .model-media > img,
  .shot-frame[data-tilt].is-tilting > img,
  .gal-frame[data-tilt].is-tilting > img{ transform:none !important; }
}

/* ════════════════════════════════════════════════════════════════════════════
   V124 · THE SMRT TOWNHOUSE POP-UP — card 96, 20 September
   Copy, structure and the reasoning are in tpl-home.php. This is the styling.

   The three colours are the CARD'S OWN hex values, described there as
   "approximately". They are declared as custom properties on this one element
   so they cannot leak anywhere else on the site, and so that switching to the
   site's own tokens is a three-line change if that is what is wanted. #101820
   in particular carries a blue cast this palette otherwise avoids.
   ════════════════════════════════════════════════════════════════════════════ */
.th-pop{
  --th-ground:#101820;   /* deep charcoal, the card's value */
  --th-warm:#F4EEE4;     /* warm white */
  --th-gold:#D3AE67;     /* muted gold */
}
.th-pop .mmh-modal-card{
  background:var(--th-ground);
  color:var(--th-warm);
  border:1px solid rgba(211,174,103,.38);   /* "fine gold border" */
  border-radius:16px;                        /* "softly rounded corners" */
  box-shadow:0 46px 96px -48px rgba(0,0,0,.9);
  /* "Allow scrolling within the popup when needed so all content can be
     reached" — a short window must never trap content below the fold. */
  max-height:92vh;
  overflow-y:auto;
}
.th-pop .mmh-modal-media{ border-radius:15px 15px 0 0; overflow:hidden; }

/* ════════════════════════════════════════════════════════════════════════════
   V131, REVERTED IN V132.1.

   V131 answered "make it a bit shorter to avoid the scrolling bar" by
   shrinking the banner from 16/7 to 16/3.6 and tightening every margin on the
   card, with two more steps for short windows. It worked, and it cost the
   photograph — the one thing the card asked to be shown at high resolution.

   V132 solved the same problem properly by going landscape on desktop, so the
   shrinking now earns nothing. Left in place it would only make the portrait
   card, which is what phones and small tablets see, diverge from the client's
   own mock-up: there the banner is a full 16/7 band across the top and the
   spacing is generous.

   So the whole V131 block is removed and the portrait layout is back to the
   mock-up as drawn. A short phone may scroll a little. The card allows for
   exactly that: "Allow scrolling within the popup when needed so all content
   can be reached."
   ════════════════════════════════════════════════════════════════════════════ */

.th-pop .eyebrow{ color:var(--th-gold); }
/* ⚠ `body.mmh` IS LOAD-BEARING HERE, NOT DECORATION.
   The site-wide heading register — `body.mmh :is(h1..h6):not(…)` — computes to
   (0,2,2) because `:not()` takes the specificity of its heaviest argument,
   `.trustindex-wrap *`. `.th-pop .mmh-modal-card h2` is only (0,2,1), so it
   LOST and the popup title rendered #FFFFFF while the card asks twice for warm
   white and this block defines --th-warm for exactly that purpose. The rule
   was here, it just never applied, which is why it read as done.

   Fixed by lifting this selector to (0,3,2) rather than adding the title to
   that :not() list. Adding it there would work for this element but would
   raise the register rule's specificity for EVERY OTHER heading on the site,
   against whatever else is currently overriding it. One popup is not worth
   that blast radius. */
body.mmh .th-pop .mmh-modal-card h2{ color:var(--th-warm); letter-spacing:-.01em; }
.th-pop .th-pop-gold{ color:var(--th-gold); }

/* "Immediately below the title, add YES, THIS IS MODULAR. Make this statement
   prominent in gold. The aim is for visitors to immediately realise that the
   contemporary two-storey townhouse shown is a modular home." Set in caps by
   the stylesheet rather than typed in caps, so a screen reader still reads it
   as a sentence. */
/* V128 — "Make this statement prominent in gold. The aim is for visitors to
   immediately realise that the contemporary two-storey townhouse shown is a
   modular home." (Card 96.)

   ⚠ IT WAS SMALLER THAN THE BODY COPY. At clamp(.78rem,…,.95rem) it computed
   to 15.2px against the description paragraph's 16px. Gold and letterspaced,
   but a line set smaller than the text beneath it is not the most prominent
   thing on the panel, and being the thing the visitor reads first is the
   entire stated purpose of the line.

   Now larger than the paragraph at every width, and second only to the title
   and the delivery figure, which is the order the card sets out.

   The letterspacing is REDUCED as the size goes up. .2em on a 21px uppercase
   line at 390px pushes "MODULAR." onto its own row and the statement stops
   reading as one statement. .14em holds it on two balanced lines on a phone
   and one line on a desktop. */
.th-pop .th-pop-claim{
  margin:.8rem 0 0;
  font-family:var(--mono);
  font-size:clamp(1.02rem,2.1vw,1.35rem);
  letter-spacing:.14em;
  text-transform:uppercase;
  color:var(--th-gold);
  font-weight:700;
}

/* The three features, evenly spaced, with the subtle vertical dividers the
   card asks for. Dividers are borders between items rather than extra
   elements, so they cannot end up orphaned at a wrap. */
.th-pop .th-pop-feats{
  list-style:none; margin:1.6rem 0 0; padding:0;
  display:grid; grid-template-columns:repeat(3,1fr);
}
.th-pop .th-pop-feats li{
  display:flex; flex-direction:column; align-items:center; gap:.3rem;
  padding:.2rem .6rem;
  border-left:1px solid rgba(244,238,228,.14);
}
.th-pop .th-pop-feats li:first-child{ border-left:0; }
.th-pop .th-pop-ic{ color:var(--th-gold); display:block; }
/* V128 — 30px, up from 26px, and centred in the column.
   The redrawn icons carry more internal detail than the shapes they replaced
   (slats on the garage door, corner ticks on the area mark), and at 26px the
   staircase in particular went faint against the gold. 30px is where all three
   hold their weight without competing with the 22px value beneath them.
   margin-inline:auto because the column is wider than the icon and a
   display:block SVG would otherwise sit hard left while the text centres. */
.th-pop .th-pop-ic svg{ width:30px; height:30px; display:block; margin-inline:auto; }
.th-pop .th-pop-feat-v{
  font-family:var(--display); font-size:clamp(1.05rem,2.2vw,1.4rem);
  font-weight:400; line-height:1.1; color:var(--th-warm);
}
.th-pop .th-pop-feat-l{
  font-family:var(--mono); font-size:.6rem; letter-spacing:.16em;
  text-transform:uppercase; color:rgba(244,238,228,.62); text-align:center;
}

.th-pop .mmh-modal-copy{ color:rgba(244,238,228,.82); line-height:1.75; }

/* "Make the timeframe large and gold, as shown." */
.th-pop .th-pop-time{ margin-top:1.6rem; display:grid; gap:.25rem; justify-items:center; }
/* V128 — the card writes this as "20–24 WEEKS". Every other display string it
   sets in capitals — the eyebrow, the claim, the feature labels, both button
   labels, the timeframe's own sub-label — renders in capitals here. This one
   was rendering "20–24 weeks" and was the only exception, for no reason.
   Uppercased in CSS rather than typed into the markup, so the text stays a
   normal readable sentence for a screen reader and for the client if they
   ever want it changed back. */
.th-pop .th-pop-time-v{
  font-family:var(--display); font-size:clamp(1.7rem,4vw,2.4rem);
  font-weight:360; line-height:1; color:var(--th-gold);
  text-transform:uppercase; letter-spacing:.01em;
}
.th-pop .th-pop-time-l{
  font-family:var(--mono); font-size:.62rem; letter-spacing:.16em;
  text-transform:uppercase; color:rgba(244,238,228,.62);
}

/* "Keep both buttons the same width, with consistent spacing." Equal columns
   rather than flex, so they match at every width instead of only when their
   labels happen to be a similar length. */
/* ⚠ MEASURED, NOT GUESSED. At 1440 the card is 560 wide with 46px of padding
   each side, so two equal columns come to 226px. The longest label,
   EXPLORE THE TOWNHOUSE, needs 247px and .btn is white-space:nowrap, so it
   was clipped at both ends. The card is widened rather than the type shrunk:
   the client asked for these labels in these words and they should not be set
   smaller than the rest of the site's buttons to fit a box. */
/* The base card sets `width:min(560px,100%)`, so a max-width here does
   nothing at all. It has to be `width`. Caught by measuring after the change
   rather than assuming it had worked: the claim colour fixed itself, the
   width silently did not. */
.th-pop .mmh-modal-card{ width:min(640px, 94vw); }
.th-pop .th-pop-actions{
  display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
  gap:.9rem; margin-top:1.9rem;
}
/* Below the width where two labels genuinely cannot sit side by side, they
   stack rather than clip. 640 is measured: 2 x 247 + gap + padding. */
@media (max-width:640px){
  .th-pop .th-pop-actions{ grid-template-columns:1fr; }
}
/* Measured at 1440 after widening the card: the longest label needed 267px
   in a 266px box. One pixel, which is exactly the kind of margin that clips
   on one browser and not another. The inline padding comes down inside this
   modal only, which buys 13px of headroom per button without touching the
   type size or the site's button component anywhere else. */
.th-pop .th-pop-actions .btn{
  width:100%; justify-content:center; text-align:center;
  padding-inline:14px;
}

/* "Ensure the layout adapts cleanly to smaller screens, the buttons are easy
   to tap." Below 560 the features stack to a row of three tight columns and
   the buttons go full width, one per line. */
@media (max-width:560px){
  .th-pop .th-pop-feats li{ padding:.2rem .3rem; }
  .th-pop .th-pop-feat-l{ font-size:.54rem; letter-spacing:.1em; }
  .th-pop .th-pop-actions{ grid-template-columns:1fr; }
}

/* ════════════════════════════════════════════════════════════════════════════
   V132 — LANDSCAPE, TWO COLUMNS.
   Client: "can we try a landscape style then 2 column so the image can still
   be seen clearly."

   V131 bought the height back by shrinking the banner, which solved the
   scrollbar and cost the photograph. This puts the photograph back at full
   height beside the text instead of above it: the card goes wide rather than
   tall, so the image gets more pixels than it ever had and the height problem
   disappears on its own.

   The markup is untouched. The card becomes a two-column grid, the media is
   placed in column one spanning every row, and the seven content blocks
   auto-place down column two in their existing document order. Nothing is
   reordered, duplicated or moved in tpl-home.php, so a screen reader still
   meets the content in the order the client wrote it.

   The image bleeds to three edges — top, left, bottom — using the card's own
   padding as a negative margin, the same trick the portrait banner already
   used on its three. `aspect-ratio:auto` and `height:100%` let it take
   whatever height the text column settles at, and `object-fit:cover` keeps
   the townhouse centred at any ratio.

   960px is the breakpoint, measured: the text column needs 430px for the two
   buttons side by side plus 46px of padding either side, and the image needs
   about 400px before it stops being worth showing.

   V132.1 — the image column widened at the client's request. The card goes
   1000 -> 1060 and the split goes 0.82fr / 1fr to 1.02fr / 1fr, so the extra
   width lands on the photograph rather than being shared. The text column
   holds its 430px floor, so neither button wraps. Card height 611px at
   1440x900, still well inside the 826px of room. Below that the card
   returns to the V131 portrait layout, unchanged, so phones and small
   tablets keep the stacked version that already fits them. */
/* The wrapper is invisible until the landscape layout needs it. */
.th-pop .th-pop-body{ display:contents; }

@media (min-width:960px){
  .th-pop .mmh-modal-card{
    width:min(1060px, 94vw);
    display:grid;
    grid-template-columns:minmax(400px, 1.02fr) minmax(430px, 1fr);
    align-items:stretch;
    text-align:left;
    padding-bottom:clamp(28px,3vw,40px);
  }
  .th-pop .th-pop-body{ display:block; grid-column:2; grid-row:1; }
  /* Column one. The photograph is the whole left side. */
  .th-pop .mmh-modal-media{
    grid-column:1; grid-row:1;
    align-self:stretch; aspect-ratio:auto; height:auto;
    margin:calc(-1 * clamp(28px,4vw,46px))
           clamp(26px,2.6vw,34px)
           calc(-1 * clamp(28px,3vw,40px))
           calc(-1 * clamp(28px,4vw,46px));
    border-radius:15px 0 0 15px;
  }
  .th-pop .mmh-modal-media img{ height:100%; object-fit:cover; object-position:center; }

  /* Column two. Everything left-aligned now that it sits in a column of its
     own rather than centred in a narrow card. */
  .th-pop .eyebrow{ justify-content:flex-start; margin-top:0; }
  .th-pop .mmh-modal-card h2{ margin-top:.55rem; }
  .th-pop .th-pop-claim{ margin-top:.5rem; }
  .th-pop .th-pop-feats{ margin-top:1.15rem; }
  .th-pop .mmh-modal-copy{ margin:1rem 0 0; max-width:none; line-height:1.7; }
  .th-pop .th-pop-time{ margin-top:1.2rem; justify-items:start; }
  .th-pop .th-pop-actions{ margin-top:1.35rem; }

  /* The feature row keeps its three columns and its dividers, but the items
     align left with the rest of the column instead of centring under a
     heading that is no longer above them. */
  .th-pop .th-pop-feats li{ align-items:flex-start; padding-inline:0 .9rem; }
  .th-pop .th-pop-feats li:not(:first-child){ padding-left:.9rem; }
  .th-pop .th-pop-ic svg{ margin-inline:0; }
  .th-pop .th-pop-feat-l{ text-align:left; }

  /* The close button sits over the photograph in this layout, so it needs a
     ground of its own to stay visible against a bright sky. */
  .th-pop .mmh-modal-x{
    background:rgba(16,24,32,.66);
    border-color:rgba(244,238,228,.3);
    backdrop-filter:blur(4px);
  }

  /* The short-window steps from V131 shrank the banner, which has no meaning
     once the image is a full-height column. They are cancelled here and the
     height is managed by the layout instead. */
  .th-pop .mmh-modal-media{ aspect-ratio:auto; }
}

/* ════════════════════════════════════════════════════════════════════════════
   V125 · THE HERO MEDIA GROUP — card 82, 17 September
   Two landscape video panels, a 3D call to action beneath them, and one
   control that collapses the lot. Markup and reasoning in tpl-home.php.
   ════════════════════════════════════════════════════════════════════════════ */

/* The group stacks: panels, then the CTA directly beneath, as the card asks.
   It keeps the position .hero-cards already held in the hero's grid, so the
   surrounding layout is untouched. */
/* ⚠ THE WIDTH GOES ON THE COLUMN, NOT ON THE GROUP. First attempt set it on
   .hero-media itself and nothing moved: `.hero-foot` is
   `grid-template-columns:1fr auto`, so the aside column is sized by its
   content — and `width:min(100%,...)` inside an auto column resolves 100% to
   that same content-derived width. Circular, so the panels stayed at 161px.
   Sizing .hero-aside breaks the circle. See the rule below the group. */
/* THE GROUP SETS ITS OWN WIDTH, and it has to. `.hero-foot` is
   `grid-template-columns:1fr auto`, so the aside column sizes to whatever is
   inside it — which left the two panels at 161px each, SMALLER than the
   single card they replaced. The card asks for them "slightly larger than
   those shown in the attached reference, while still leaving enough of the
   main hero/background visible", so the width is set here and capped: at 1440
   each panel comes out around 237px against the old card's 186. */
.hero-media{
  display:grid;
  width:100%;
  /* ⚠ AND A CAP, because below 1024 .hero-foot drops to one column and the
     aside becomes full width. Without this the panels went to 833px each at
     900 — the group swallowed the hero it is supposed to sit in front of.
     Measured at every breakpoint, not assumed from the desktop view. */
  max-width:min(100%, 520px);
  gap:clamp(10px,1vw,14px);
  transition:opacity .45s var(--ease), transform .45s var(--ease-out);
}
/* The aside column is what actually decides how wide the panels can be.
   At 1440 this gives each panel about 237px against the old card's 186 —
   "slightly larger", as asked, with the hero still clearly visible behind. */
.hero-aside:has(.hero-media){ min-width:clamp(280px, 34vw, 520px); }

/* "When collapsed, the videos and 3D CTA should move out of the way so the
   visitor can enjoy the full hero background." It moves out rather than
   merely fading, and is taken out of the tab order and the accessibility
   tree so a collapsed group cannot be reached by keyboard. */
/* ⚠ V134 — THE COLLAPSE TARGET IS NARROWER THAN THE GROUP.
   It used to hide .hero-media outright. The HIDE button now lives INSIDE that
   group, beside the 3D CTA, because Max asked for the two to form one
   horizontal row — so hiding the whole group would take the reopen control
   with it and strand the visitor. Only the two things the client named,
   "the videos and 3D CTA", are hidden. The row that holds the button keeps
   its place, and the button with it. */
.hero-media.is-collapsed .hero-cards,
.hero-media.is-collapsed .hero-3d-cta{
  opacity:0; transform:translateY(14px) scale(.985);
  pointer-events:none; visibility:hidden;
}
.hero-media.is-collapsed .hero-cards{
  /* The panels give their height back as well, otherwise a collapsed group
     leaves a hole the size of the videos and nothing is revealed. */
  height:0; margin:0; overflow:hidden;
}
.hero-media .hero-cards{ transition:opacity .45s var(--ease), transform .45s var(--ease-out); }
.hero-media .hero-3d-cta{ will-change:opacity, transform; }

/* ── V134 · THE CTA AND THE HIDE CONTROL, ONE ROW ────────────────────────
   "Please position the existing HIDE button directly beside the 3D
   walkthrough CTA ... The CTA and HIDE control should form one clean
   horizontal row on desktop." And: "Make the complete CTA slightly
   smaller/narrower so there is enough room for the HIDE button to sit beside
   it." The CTA takes 1fr and HIDE takes its own content width, so the CTA
   narrows by exactly the button plus the gap and not by a guessed amount. */
.hero-3d-row{
  display:grid; grid-template-columns:minmax(0,1fr) auto;
  gap:.55rem; align-items:stretch;
}

/* "The client would like the two video panels slightly larger than those
   shown in the attached reference, while still leaving enough of the main
   hero/background visible." Landscape 16/9 panels, side by side, sized off
   the viewport so they grow on a large screen without ever taking the hero. */
/* ⚠ THE BASE .hero-cards IS A FLEX ROW OF FIXED-WIDTH CARDS (see V97/V97.8
   above). That sizing was for one video beside one 3D card. This group is two
   equal landscape panels, so it becomes a two-column grid and the cards give
   up their fixed width. Overridden here rather than edited up there, because
   the old rule still documents why those numbers were chosen. */
.hero-media .hero-cards{
  display:grid; grid-template-columns:1fr 1fr;
  gap:clamp(10px,1.1vw,16px);
  align-items:start;
}
.hero-media .hero-card{ width:auto; min-width:0; flex:initial; }
.hero-card--video{
  margin:0; cursor:pointer;
  display:grid; gap:.45rem;
}
.hero-card-open{
  display:block; width:100%; padding:0; border:0; background:none;
  cursor:pointer; text-align:left;
}
.hero-card--video .hero-card-media{
  position:relative; display:block; width:100%;
  aspect-ratio:16/9;          /* landscape, per the card */
  overflow:hidden; border-radius:6px;
  border:1px solid var(--line-ui);
  background:var(--ink-2);
}
.hero-card--video .hero-card-media > video,
.hero-card--video .hero-card-media > .hero-card-poster,
.hero-card--video .hero-card-media > .hero-card-yt{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; display:block; border:0;
}
/* A YouTube embed will not fill a 16/9 box it does not know about unless it
   is scaled: the player letterboxes itself inside whatever it is given. */
.hero-card--video .hero-card-media > .hero-card-yt{
  width:calc(100% + 2px); height:calc(100% + 2px);
  left:-1px; top:-1px; pointer-events:none;
}

/* The sound mark. The panels play muted, so the visitor needs to be told
   that clicking gets them audio — otherwise a muted autoplaying video reads
   as decoration and nobody clicks it. */
.hero-card-sound{
  position:absolute; right:10px; bottom:10px; z-index:2;
  width:34px; height:34px; border-radius:50%;
  display:grid; place-items:center;
  background:color-mix(in srgb, var(--ink) 62%, transparent);
  border:1px solid var(--line-ui); color:var(--paper);
  backdrop-filter:blur(3px);
  transition:background-color .3s var(--ease), border-color .3s var(--ease), transform .35s var(--ease-out);
}
.hero-card-sound svg{ width:16px; height:16px; }
.hero-card-open:hover .hero-card-sound{
  background:var(--gold); border-color:var(--gold); color:var(--ink);
  transform:scale(1.06);
}
.hero-card-open:focus-visible{ outline:2px solid var(--gold-hi); outline-offset:3px; border-radius:7px; }

.hero-card-cap{
  font-family:var(--mono); font-size:.58rem; letter-spacing:.14em;
  text-transform:uppercase; color:var(--muted-2);
}

/* ── The 3D call to action ───────────────────────────────────────────────
   "Directly beneath the two videos, add a clear CTA: Explore the Townhouse
   in 3D", and "add a tasteful hover/visual cue such as Take a Virtual Tour"
   so it reads as an interactive walkthrough rather than a link.
   The cue is a second line that is there at rest and lifts into gold on
   hover, rather than a tooltip that appears from nowhere. */
/* V134 — "Add a noticeable gold border around the CTA." It was a 1px
   hairline at 42% opacity, which is the site's generic line colour and is
   not noticeable by any reading of the word. Now a solid 2px in the gold
   itself, with a soft outer glow so it holds its edge over a bright slide
   as well as a dark one. */
.hero-3d-cta{
  display:grid; grid-template-columns:auto 1fr auto; align-items:center;
  gap:.85rem; padding:.8rem 1rem;
  border:2px solid var(--gold); border-radius:6px;
  box-shadow:0 0 0 1px rgba(0,0,0,.28), 0 6px 22px -12px rgba(0,0,0,.7);
  background:color-mix(in srgb, var(--ink) 55%, transparent);
  backdrop-filter:blur(4px);
  color:var(--paper); text-decoration:none;
  transition:background-color .4s var(--ease), border-color .4s var(--ease), transform .4s var(--ease-out);
}
.hero-3d-cta:hover{
  background:color-mix(in srgb, var(--ink) 74%, transparent);
  border-color:var(--gold-hi);
  transform:translateY(-2px);
}
.hero-3d-cta:focus-visible{ outline:2px solid var(--gold-hi); outline-offset:3px; }
.hero-3d-mark{ width:26px; height:26px; color:var(--gold); display:grid; place-items:center; }
.hero-3d-mark svg{ width:100%; height:100%; }
.hero-3d-text{ display:grid; gap:.1rem; min-width:0; }
.hero-3d-main{ font-size:.92rem; font-weight:600; line-height:1.2; }
.hero-3d-cue{
  font-family:var(--mono); font-size:.56rem; letter-spacing:.16em;
  text-transform:uppercase; color:var(--muted-2);
  transition:color .35s var(--ease);
}
.hero-3d-cta:hover .hero-3d-cue{ color:var(--gold); }
.hero-3d-cta .arrow{ transition:transform .3s var(--ease); }
.hero-3d-cta:hover .arrow{ transform:translateX(4px); }

/* ── The collapse control ────────────────────────────────────────────────
   "a small, unobtrusive control". It sits under the group and stays put when
   the group goes, which is how the visitor gets it back. */
/* V134 — was `justify-self:end; margin-top:.55rem`, which put it on its own
   line 27px under the CTA. It is now a cell of .hero-3d-row, so it needs
   neither: it stretches to the CTA's height and sits hard against it. */
.hero-media-toggle{
  align-self:stretch;
  display:inline-flex; align-items:center; justify-content:center; gap:.45rem;
  padding:.42rem .7rem;
  border:1px solid var(--line-ui); border-radius:999px;
  background:color-mix(in srgb, var(--ink) 58%, transparent);
  backdrop-filter:blur(3px);
  color:var(--muted-2); cursor:pointer;
  font-family:var(--mono); font-size:.55rem; letter-spacing:.14em; text-transform:uppercase;
  transition:color .3s var(--ease), border-color .3s var(--ease), background-color .3s var(--ease);
}
.hero-media-toggle:hover{ color:var(--paper); border-color:var(--line-gold); }
.hero-media-toggle:focus-visible{ outline:2px solid var(--gold-hi); outline-offset:2px; }
.hero-media-toggle-ic{ width:13px; height:13px; display:grid; place-items:center; }
.hero-media-toggle-ic svg{ width:100%; height:100%; transition:transform .35s var(--ease-out); }
/* The minus becomes a plus by drawing the second stroke with a pseudo rule
   rather than swapping the icon, so there is no second SVG to keep in step. */
.hero-media-toggle.is-collapsed .hero-media-toggle-ic{ position:relative; }
.hero-media-toggle.is-collapsed .hero-media-toggle-ic::after{
  content:""; position:absolute; left:50%; top:1px; bottom:1px;
  width:1.7px; background:currentColor; transform:translateX(-50%); border-radius:2px;
}

@media (max-width:900px){
  /* The card asks for this to "work appropriately on mobile rather than
     simply scaling down the desktop arrangement". Two 16/9 panels side by
     side on a phone are too small to read, so they stack. */
  .hero-media .hero-cards{ grid-template-columns:1fr; }
  /* Stacked, one panel per row, so it can be a little wider than the pair. */
  .hero-media{ max-width:min(100%, 560px); }
  .hero-3d-main{ font-size:.86rem; }

  /* ⚠ V156 — THE SHOW VIDEOS PILL STOPS STRETCHING, AND THAT IS THE WHOLE
     BUG. Bianca, 24 Sep, with a screenshot of the blob and a reference pill
     beside it: "improve the button label paddings to be like the reference".

     Measured on live at 390 before the change: 121 × 93px. Its own padding
     and label only account for about 26px of that height. The other ~67px
     came from `align-self:stretch`.

     V134 gave it that stretch on purpose, so it would sit flush against the
     3D CTA beside it — correct on a desktop row where the two are a pair.
     On a phone the CTA wraps to two lines and the row grows to 93px, and the
     collapse hides that CTA with `visibility:hidden`, which leaves it
     occupying its full height while invisible. So the button stretched to
     match a neighbour nobody could see, and `border-radius:999px` on a
     121 × 93 box is an oval blob rather than a pill.

     Centred instead of stretched, it takes its own height from its own
     padding, which is what makes it read as a pill. min-height keeps the tap
     target at 44px — the padding alone lands about 36px, which is under the
     floor for a thumb.

     ⚠ DESKTOP IS UNTOUCHED. Above 900px the stretch still applies and the
     button still matches the CTA's height, exactly as V134 intended. */
  .hero-media-toggle{
    align-self:center;
    min-height:44px;
    padding:.62rem 1.15rem;
  }
}
@media (prefers-reduced-motion:reduce){
  .hero-media, .hero-3d-cta, .hero-card-sound,
  .hero-media-toggle, .hero-3d-cta .arrow{ transition:none; }
}

/* ════════════════════════════════════════════════════════════════════════════
   V126 — 21 SEPTEMBER. HOME PAGE UPDATES.DOCX, TRELLO CARD 82.

   Four changes. Each one traces to a specific yellow annotation in the
   client's document or to the menu screenshot on card 94.
   ════════════════════════════════════════════════════════════════════════════ */

/* ────────────────────────────────────────────────────────────────────────────
   1 · THE GOLD TICK BEFORE EVERY EYEBROW COMES OFF, SITE-WIDE

   remove.webp, card 94: a yellow box reading "remove from all" with its arrow
   on the short gold dash that sits to the left of the eyebrow label. Shown on
   the FAQ hero, and the note says all, so it is all of them.

   ⚠ THIS IS DELETED AT THE SOURCE RULE, NOT OVERRIDDEN LATER, and that matters
   here more than usual. The dash is drawn by `.eyebrow::before` and then
   RECOLOURED in four further places (.bg-ink, .bg-green, .company--bg, and the
   hero). An override appended at the end of the file would beat some of those
   on specificity and lose to others depending on the selector, so the dash
   would survive on exactly the sections with the most specific rules — the
   dark bands and the hero, which is to say the most visible ones.
   Setting `content:none` on the base rule removes the box entirely, so every
   later `background:` declaration has nothing to paint and no rule anywhere
   can bring it back.

   `.eyebrow.no-tick` is now redundant. It is deliberately LEFT IN PLACE rather
   than swept out of the markup: it is inert, it costs nothing, and if the
   client asks for the dash back on a subset of headings it is the hook that
   makes that a one-line change instead of a hunt.

   The gap is dropped with it. `display:inline-flex` plus `gap:.7em` was spacing
   the dash from the text; with the dash gone the gap became a phantom indent
   on the left of every eyebrow on the site. */
.eyebrow::before{ content:none; }
.eyebrow{ gap:0; }

/* ────────────────────────────────────────────────────────────────────────────
   2 · THE "VIEW →" LINK MOVES UP ONTO THE SPEC ROW

   Screenshot 5 of the document: a red arrow from "View →" pointing left at the
   spec line, labelled "in line".

   The foot was a separate row below a rule, carrying the warranty note on the
   left and the link on the right. The warranty note is gone (V124 on the
   homepage six, V126 in model-grid's catalogue branch), so on most cards the
   foot is now a rule with a single link floated right under it — a whole row
   of vertical space spent on one link, which is exactly what he is pointing at.

   So the link joins the spec line. The rule and the row go.

   ⚠ THE CARDS THAT STILL CARRY A NOTE KEEP THE OLD ARRANGEMENT. The Pool House
   has "Designed to your brief" and the priced catalogue cards have "From
   $X inc GST" — real content, not boilerplate, and it needs its own line.
   `:has()` picks those out so one rule covers both cases with no new class and
   no second code path. Where `:has()` is unsupported the card simply keeps the
   foot row it has today, which is the current design, not a broken one. */
@supports selector(:has(*)) {
  .model-body:not(:has(.card-note)) .foot{
    margin-top:0; padding-top:0; border-top:0;
  }
  .model-body:not(:has(.card-note)){
    /* The specs line and the link become one row. `gap:.5rem` on .model-body
       already separates the stacked children, so the row needs no margin of
       its own; `margin-top:auto` on the row is what still pins the pair to the
       bottom of a card that is taller than its content. */
    display:grid;
    grid-template-columns:1fr auto;
    grid-template-rows:auto 1fr;
    align-items:end;
  }
  .model-body:not(:has(.card-note)) .name{ grid-column:1 / -1; }
  .model-body:not(:has(.card-note)) .specs{ grid-column:1; align-self:end; }
  .model-body:not(:has(.card-note)) .foot{ grid-column:2; align-self:end; justify-content:flex-end; }
}

/* ────────────────────────────────────────────────────────────────────────────
   3 · THE COMPANY CARD WITH NO LOCKUP IN IT

   Screenshot 7: "remove" on the blue SMRT BUILT wordmark, "repalce" on the
   paragraph. Both applied in tpl-home.php; this is the layout that has to
   follow, because the card was a three-column grid built around the artwork —
   `auto 1fr auto`, logo then copy then button. Take the logo out and leave two
   paragraphs behind and the browser fills those three tracks with the three
   children it now has: first paragraph in the narrow auto column, second in
   the 1fr, button in the last. Two paragraphs of one statement, set at
   different widths, side by side.

   The two paragraphs are wrapped in `.partner-card-body` in the template and
   the grid drops to two tracks. A wrapper rather than `:has()` here, because
   this is structure the markup should state rather than something the
   stylesheet should infer, and it cannot silently regress on a browser without
   `:has()`. */
.partner-card--copyonly{ grid-template-columns:1fr auto; align-items:center; }
.partner-card-body{ display:flex; flex-direction:column; gap:1rem; max-width:78ch; }
.partner-card--copyonly .partner-card-copy{ max-width:none; }
@media (max-width:900px){
  .partner-card--copyonly{ grid-template-columns:1fr; }
}

/* ────────────────────────────────────────────────────────────────────────────
   V126 · 4 — THE CUSTOM CONFIGURATIONS PAGE (card 83, tasks 10-15)

   The approved layout is four cards, two by two. That is stated as a layout,
   so it is built as one: `repeat(2, minmax(0, 1fr))` from 900px up, one column
   below. Not auto-fit — auto-fit would give three across on a wide screen and
   quietly stop being the approved design on exactly the machines the client
   reviews it on.
   ──────────────────────────────────────────────────────────────────────────── */
.cfg-intro .sec-head{ max-width:none; }
.cfg-back{ margin-top:1.6rem; }

.cfg-grid{
  display:grid; grid-template-columns:1fr;
  gap:clamp(24px,3vw,44px);
}
@media (min-width:900px){
  .cfg-grid{ grid-template-columns:repeat(2, minmax(0,1fr)); }
}

.cfg-card{
  display:flex; flex-direction:column;
  background:var(--ink-2); border:1px solid var(--line-soft);
}
.cfg-card:hover{ border-color:var(--line-gold); }
.cfg-card{ transition:border-color .4s var(--ease); }

/* The two images stack inside the card. Each is a button into the lightbox,
   because the plans carry room labels and door swings that are unreadable at
   card width and are the entire reason the plan is on the page. */
.cfg-shots{ display:flex; flex-direction:column; }
.cfg-shot{
  position:relative; display:block; width:100%; padding:0; border:0; cursor:zoom-in;
  border-bottom:1px solid var(--line-soft);
  font:inherit; color:inherit; text-align:left;
}
/* ⚠ FIXED BOXES, AND THIS IS NOT COSMETIC.
   The four floor plans come off the PDF at wildly different proportions —
   2.12, 2.30, 2.75 and 1.47 to one — because each configuration is a different
   shape. Sized by their own aspect the four cards end up hundreds of pixels
   apart in height and the 2x2 grid stops reading as a set. A fixed box per
   image type puts all four on the same rhythm.

   ⚠ CONTAIN, NEVER COVER. These are technical drawings. `cover` would crop a
   room off the end of the 2.75:1 plan, and the whole footprint is the
   information the card exists to show. `contain` letterboxes instead, so every
   plan is complete at every width. The letterbox is filled with each artwork's
   OWN ground, so the padding is invisible rather than a grey band:
     · the plans are drawn on white;
     · the 3D views WERE supplied on the client's navy panel, sampled at
       #25366A from the file itself.

   ⚠ SUPERSEDED BY V166, AND THE REASONING ABOVE IS NOW WRONG. This note read
   "that navy is part of the artwork as supplied, not a border to trim". Max
   settled it the other way on 23 Sep: the navy comes out, matching the Lodge
   3D plans. It is not a crop into the artwork — the render is untouched and
   only the flat ground behind it is replaced — so card 84's objection to
   cropping supplied images does not apply. Both 3D grounds are white now. */
/* V137 — THE FIXED BOXES ARE GONE. The template now sets aspect-ratio inline,
   per image, from the file's own dimensions, so nothing is letterboxed and no
   ground shows. These two rules keep only the grounds, as a safety net for the
   moment between layout and decode, and for the fallback path where a v137
   crop is missing and the uncropped file is used instead. */
.cfg-shot--plan{ background:#FFFFFF; }
/* ⚠ #FFFFFF, not var(--paper). The site's paper is #F4F1EA, a warm off-white,
   and the plans are drawn on pure white. Letterboxing one with the other puts
   a visible band down each side of the drawing. The ground has to match the
   artwork exactly, not the palette. */
/* ⚠ V166 — WHITE, NOT #25366A. This ground existed to match the navy the
   supplied 3D renders were drawn on. Max, 23 Sep, asked for the Lodge
   treatment instead, where the navy is taken out of the artwork and the plan
   sits on white (V117, audited V141). With the navy gone from the files, a
   navy safety-net ground would be the only navy left on the card and would
   show as a band the moment an image is slow to decode. */
.cfg-shot--3d{ background:#FFFFFF; }
.cfg-shot img{
  display:block; width:100%; height:100%; object-fit:contain;
  transition:transform .6s var(--ease);
}
.cfg-shot:hover img{ transform:scale(1.02); }
.cfg-shot-cap{
  position:absolute; left:0; bottom:0;
  font-family:var(--mono); font-size:.62rem; letter-spacing:.14em; text-transform:uppercase;
  color:var(--paper); background:rgba(16,24,32,.78);
  padding:.45em .9em;
}
.cfg-shot:focus-visible{ outline:2px solid var(--gold); outline-offset:-2px; }

.cfg-body{ padding:clamp(20px,2.4vw,30px); display:flex; flex-direction:column; gap:.7rem; flex:1; }
.cfg-title{
  font-family:var(--display); font-weight:360; letter-spacing:-.01em;
  font-size:clamp(1.3rem,2vw,1.6rem); line-height:1.2; margin:0;
}
.cfg-copy{ margin:0; }
.cfg-summary{
  margin:0; font-family:var(--mono); font-size:.74rem; letter-spacing:.06em;
  color:var(--muted); padding-top:.7rem; border-top:1px solid var(--line-soft);
}
/* margin-top:auto pins the button to the bottom so all four cards line up
   however much copy each one carries. */
.cfg-cta{ margin-top:auto; align-self:flex-start; }

.cfg-alt-cta{ margin-top:1.8rem; }
.cfg-disclaimer{
  margin-top:1.8rem; font-size:.82rem; line-height:1.6; color:var(--muted-2);
  max-width:62ch; margin-inline:auto;
}

/* ────────────────────────────────────────────────────────────────────────────
   V126 · 5 — THE OUR DESIGNS HEAD BACKGROUND (card 83, Max 20 September)

   The photograph sits behind the collection head only. The filters and the
   grid keep the page ground, because the brief is explicit that nothing moves
   and only the presentation of that one band changes.

   The band is sized to the head rather than given a fixed height, so a longer
   heading at a narrow width cannot push the text off the artwork.
   ──────────────────────────────────────────────────────────────────────────── */
.section.has-head-bg{ position:relative; isolation:isolate; }
.mf-head-bg{
  position:absolute; inset:0 0 auto 0; z-index:-1;
  /* Tall enough to carry the head and the badge, and no taller: below that the
     page ground takes over and the filters sit on it as they do today. */
  height:clamp(320px, 42vw, 560px);
  overflow:hidden;
}
@supports (overflow:clip){ .mf-head-bg{ overflow:clip; } }
.mf-head-bg img{
  width:100%; height:100%; object-fit:cover; object-position:50% 58%;
  display:block;
}
/* Two jobs in one layer, both named in the brief: hold the white heading and
   the gold badge legible over a sunset band, and fade into the dark ground
   below instead of ending on a hard cut. */
.mf-head-scrim{
  position:absolute; inset:0; display:block;
  background:linear-gradient(180deg,
    rgba(16,24,32,.78) 0%,
    rgba(16,24,32,.62) 38%,
    rgba(16,24,32,.88) 78%,
    var(--ink) 100%);
}
@media (max-width:700px){
  /* Portrait viewports crop a 16:9 photograph hard. Pulling the focal point up
     keeps the house in frame rather than centring on foreground planting. */
  .mf-head-bg img{ object-position:50% 46%; }
}
@media (prefers-reduced-motion:no-preference){
  .section.has-head-bg .collection-head{ position:relative; }
}

/* V129 — the spec sheet's second line, under each inclusions group heading.
   Small mono caps so it reads as a label belonging to the heading above rather
   than as the first item of the list below, which is the one thing that would
   make it worse than leaving it out. */
.incl-sub{
  margin:.15rem 0 .55rem;
  font-family:var(--mono); font-size:.62rem; letter-spacing:.16em;
  text-transform:uppercase; color:var(--muted-2);
}

/* ═══════════════════════════════════════════════════════════════════════════
   V150 · HOMEPAGE FAQ — TWILIGHT BACKGROUND, PANNING, WITH A PAUSE CONTROL
   ═══════════════════════════════════════════════════════════════════════════
   Max, card 101, 24 Sep, with a concept screenshot and the photograph.

   The section keeps its wording, its questions, its gold accents and its
   two-column desktop grid. Everything below is the band around them.

   ⚠ ONLY THE PHOTOGRAPH MOVES. "The FAQ heading, questions, panels and
   buttons must remain stationary while only the background image moves."
   The animation is therefore on .faq-bg-photo alone and nothing else in the
   section is transformed. The content sits on z-index 2 above both layers.
   ═════════════════════════════════════════════════════════════════════════ */
.faq--bg{ position:relative; isolation:isolate; }
.faq--bg > .wrap{ position:relative; z-index:2; }

/* The frame the photograph pans inside. overflow:clip, never hidden — same
   rule as every other clipped box in this file: `hidden` makes an element a
   scroll container, and there are scroll-driven animations elsewhere on this
   page that resolve against the nearest one. */
.faq-bg{ position:absolute; inset:0; z-index:0; overflow:clip; }

/* ⚠ THE PHOTOGRAPH IS BIGGER THAN ITS FRAME ON PURPOSE. A pan moves it, and
   an element sized exactly to its box would drag a bare edge into view as
   soon as it moved. 112% with the origin centred leaves 6% of slack on every
   side, which is more than the 2.2% the keyframe ever travels. */
.faq-bg-photo{
  position:absolute; inset:0;
  /* ⚠ max-width:none IS LOAD-BEARING, AND ITS ABSENCE SHIPPED A VISIBLE SEAM.
     The theme sets a global img{max-width:100%}. With it, the 112% below was
     silently clamped back to 100%: measured 1440px wide inside a 1440px box,
     offset to -102px, so the photograph ended at x=1338 and the last hundred
     pixels of the band were bare section background — a hard vertical edge
     down the right-hand side at every width. Caught by measuring the rendered
     rect against the declared width, not by looking at it. */
  max-width:none;
  width:112%; height:112%;
  left:-6%; top:-6%;
  object-fit:cover; object-position:center;
  will-change:transform;
  animation:mmhFaqDrift 40s ease-in-out infinite alternate;
}

/* ⚠ `alternate` IS WHAT MAKES THE LOOP SEAMLESS, and it is the reason there
   is no second half to these keyframes. "The movement should start
   automatically and loop seamlessly." A forwards-only loop has to jump from
   its end state back to its start, which on a slow pan is a visible snap
   every cycle. Alternating plays the same move backwards instead, so the
   motion never cuts — it just changes direction, at the moment it is slowest
   because of the ease-in-out. 46s each way is slow enough that the direction
   change is not readable as one. */
/* ⚠ V155 — THE AMPLITUDE WAS TRIPLED BECAUSE THE FIRST SET WAS INVISIBLE.
   Bianca, 24 Sep: "nothing is happening with the motion?" She was right, and
   the animation was running the whole time. Measured on the live page at
   1440: the photograph moved 6.3px in 10 seconds — 0.63px/sec, which no one
   can see. "Slow" had been taken so literally that it became "still".

   ±3.2% of the element's 1613px width is ±52px, so a full traverse is ~103px
   in 40s, about 2.6px/sec. Visibly drifting, still unhurried.

   ⚠ THE TRAVEL MUST STAY INSIDE THE OVERSCALE OR AN EDGE APPEARS. The
   element is 112% wide at left:-6%, so there is 6% of the BOX (86px at 1440)
   of slack on each side. Travel is 52px against 86px of slack — inside it,
   with room to spare. Do not raise this past ~5% without raising the 112%
   with it, or the band will show its own background down one side at the
   ends of the cycle. The zoom only ever grows the image, so it cannot
   uncover an edge on its own. */
@keyframes mmhFaqDrift{
  from{ transform:translate3d(-3.2%, 1.4%, 0) scale(1.00); }
  to  { transform:translate3d( 3.2%, -1.4%, 0) scale(1.10); }
}

/* The dark overlay. A flat wash would kill the warm windows and the lit
   landscaping, which are the whole reason the client chose this photograph —
   "retaining the warm lighting and landscaping of the image". So it is a
   gradient: heaviest at the top where the heading sits over open sky, and
   lightest across the lower third where the house and the garden lights are.
   The flat floor underneath guarantees the contrast minimum everywhere. */
.faq-bg-scrim{
  position:absolute; inset:0; z-index:1; pointer-events:none;
  background:
    linear-gradient(180deg,
      rgba(1,9,22,.88) 0%,
      rgba(1,9,22,.78) 34%,
      rgba(1,9,22,.62) 66%,
      rgba(1,9,22,.72) 100%),
    rgba(1,9,22,.30);
}

/* ---- The panels ---------------------------------------------------------
   "Use slightly transparent charcoal panels over the moving background,
   following the supplied concept. Retain the existing white text and gold
   accents." So only the background and border of the existing .faq-top-item
   are restated; the grid, the padding, the type, the gold number and the
   hover lift all stay as they are.

   ⚠ backdrop-filter IS DELIBERATELY NOT USED. A blur behind ten panels over
   a continuously animating image is a full-section repaint every frame, and
   on a mid-range phone that is exactly where this section would start to
   stutter. The panel is opaque enough at .72 to hold the text on its own. */
.faq--bg .faq-top-item{
  background:rgba(16,24,32,.72);
  border-color:rgba(255,255,255,.14);
}
.faq--bg .faq-top-item:hover{
  background:rgba(16,24,32,.85);
  border-color:var(--line-gold);
}

/* ---- The pause control --------------------------------------------------
   Discreet, per the concept: a small outlined pill, bottom right of the
   band, out of the reading column. 44px minimum height because it is a real
   tap target on a phone. */
.faq-motion{
  position:absolute; right:clamp(12px,2.4vw,28px); bottom:clamp(12px,2.4vw,28px);
  z-index:3;
  display:inline-flex; align-items:center; justify-content:center;
  min-height:44px; padding:.55rem 1rem;
  font-family:var(--mono); font-size:.7rem; letter-spacing:.1em; text-transform:uppercase;
  color:var(--paper); background:rgba(16,24,32,.72);
  border:1px solid rgba(255,255,255,.22); border-radius:2px;
  cursor:pointer; transition:border-color .2s ease, background .2s ease;
}
.faq-motion:hover{ border-color:var(--line-gold); background:rgba(16,24,32,.88); }
.faq-motion:focus-visible{ outline:2px solid var(--gold); outline-offset:2px; }

/* The pause itself. One class on the section, so the state lives in one
   place and the control can be moved without touching this. */
.faq--bg.is-motion-paused .faq-bg-photo{ animation-play-state:paused; }

@media (max-width:720px){
  /* The band is taller on a phone (ten stacked rows), so the photograph has
     more height to cover and the pan reads as faster relative to the frame.
     Slower, and with less travel, to compensate. */
  .faq-bg-photo{ animation-duration:44s; }

  /* AND THE SCRIM EASES OFF, because the band is taller here, not because
     phones need less contrast. The desktop gradient reaches its heaviest
     value over roughly 670px; stretched down a 1020px band it holds that
     weight across most of the house, and the measured result was a section
     where the lit windows and the garden lighting had gone. The panels are
     what carry the text contrast, not the scrim, and they are unchanged — so
     this only affects the picture between them. */
  .faq-bg-scrim{
    background:
      linear-gradient(180deg,
        rgba(1,9,22,.84) 0%,
        rgba(1,9,22,.68) 30%,
        rgba(1,9,22,.52) 62%,
        rgba(1,9,22,.64) 100%),
      rgba(1,9,22,.24);
  }
  /* ⚠ ON A PHONE THE PAN IS MOSTLY VERTICAL, AND THAT IS FORCED BY THE BOX,
     not a style choice. Measured at 390: a copy of the desktop amplitude moved
     the image 2.1px in 10 seconds — invisible again, because a percentage
     translate resolves against the ELEMENT, and the element is only ~437px
     wide here against ~1140px tall. The same percentage buys five times less
     movement sideways than it does down.

     So the axes swap weight. Horizontal stays small because the slack does
     too (6% of 390 = 23px each side; 1.6% of 437 = 7px, comfortably inside).
     Vertical carries the movement: 3.5% of ~1142px is 40px against 61px of
     slack, so a full traverse is ~80px in 44s. */
  @keyframes mmhFaqDrift{
    from{ transform:translate3d(-1.6%, 3.5%, 0) scale(1.00); }
    to  { transform:translate3d( 1.6%, -3.5%, 0) scale(1.09); }
  }
  /* Out of the corner and onto its own line: at 390px a floating pill in the
     bottom right lands on top of the last question.

     ⚠ position:relative, NOT static, AND THAT IS THE WHOLE POINT OF THIS
     LINE. z-index has no effect on a statically positioned element, so the
     first version of this rule silently dropped the button out of the
     z-order: the scrim above it (z-index:1) painted straight over the top and
     the control was invisible on every phone. It still occupied space and
     still measured 133×44 in the layout, which is why it looked fine in the
     DOM and vanished in a screenshot. relative restores the stacking without
     taking it back out of the flow. */
  .faq-motion{
    position:relative; display:flex; width:max-content;
    margin:1.6rem auto 0;
  }
}

/* ⚠ REDUCED MOTION STOPS IT DEAD, AND THE CONTROL GOES WITH IT.
   "Respect the user's reduced-motion preference. Where reduced motion is
   enabled, the background should remain static." `animation:none` rather
   than a paused state, so nothing is left composited for no reason.

   The pause button is hidden too, and that is the point rather than an
   oversight: there is no motion left for it to pause, and a control that
   does nothing is worse than no control. The photograph, the scrim and the
   panels all remain, so the section looks the same — it simply holds still. */
@media (prefers-reduced-motion: reduce){
  .faq-bg-photo{ animation:none; transform:none; }
  .faq-motion{ display:none; }
}

/* ════════════════════════════════════════════════════════════════════════════
   V158 · THE WHY BAND RUNS THE DESKTOP TRANSITION AT EVERY WIDTH
   ════════════════════════════════════════════════════════════════════════════
   Bianca, 24 Sep: "on Why Master Modular Homes & Granny Flat section, the same
   transition on desktop should be the same on tablet and mobile."

   ⚠ THIS REPLACES V157, WHICH TOOK THE EFFECT OFF BELOW 901px. V157 was the
   wrong call and is withdrawn whole. The defect it found was real and is
   fixed properly here: captured from live at 390, the band rendered as a
   heading, a large empty void, then the photograph, because `whyLeadIn` holds
   the paragraph at `max-height:0` until 60% of the section's scroll range.

   The void was never caused by the animation existing. It was caused by the
   animation having nowhere to run. `height:305vh` on .why-dwell and the
   sticky `.why-dwell-in` were both inside `@media (min-width:901px)`, so below
   that the band had no dwell and no pin: the whole cross-fade had to resolve
   inside one section-height of ordinary scrolling. The paragraph's 60% mark
   arrived when the section was already leaving the screen, so in practice it
   never opened.

   Giving mobile and tablet the same dwell and the same pin gives the
   transition the same room it has on desktop, and the same six range values
   resolve against the same geometry. One choreography, three widths — which
   is what she asked for, and it removes the void as a consequence rather than
   by suppressing anything.

   ⚠ THE KEYFRAMES ARE NOT TOUCHED. The ≤900px `whyCopy` and `whyLeadIn` in
   the block above stay exactly as they are: they position the copy for a
   narrow column (a top offset to top:50%, 6% side margins) and that is still
   the right shape for a phone. Only the SCROLL GEOMETRY they run against
   changes here. (The state-A offset itself was 4% when this was written and
   is 13% now — see V161, which raised it because pinning the band turned that
   percentage into a fraction of the VIEWPORT and put the heading under the
   sticky header.)
   Do not "simplify" by deleting them — the desktop keyframes use a two-column
   left/right split that has nowhere to go at 390px.

   ⚠ V102's FREEZE IS NOT COMING BACK, and it is worth saying why. A pin stalls
   any `animation-timeline: view()` inside it, which is what cut this heading
   in half twice. The heading no longer uses view() at all — V106 moved every
   heading onto a timed `.lines-in` class, and V150 removed the scroll-driven
   re-fire on this one below 901px. Class-driven and time-based cannot stall
   against a pin.

   240vh rather than desktop's 305vh: the dwell is a multiple of the viewport,
   and a phone's viewport is tall relative to its width, so 305vh there is a
   great deal of scrolling for one band. 240 keeps the same shape with less
   thumb work. The range values are desktop's, unchanged — they are
   percentages of the dwell, so they scale with it. */
@media (max-width:900px){
  .why-dwell{
    height:240vh;
    --r-photo-a:32%;  --r-photo-b:57%;
    --r-scrim-a:49%;  --r-scrim-b:58%;
    --r-step-a:56.9%; --r-step-b:57.1%;
    --r-copy-a:42%;   --r-copy-b:67%;
  }
  .why-dwell-in{
    position:sticky; top:0;
    height:100vh;
    /* ⚠ NO overflow here, same as the desktop rule: a clipped element is a
       scroll container, and the view timeline on the wrapper would then
       resolve against the wrong box. */
  }
  #why.why--bg{ min-height:100vh; height:100%; }
}

/* ════════════════════════════════════════════════════════════════════════════
   V159 · THE HERO VIDEOS OPEN AND CLOSE SMOOTHLY ON MOBILE AND TABLET
   ════════════════════════════════════════════════════════════════════════════
   Bianca, 24 Sep: "can we add smooth transition for the intro and outro of the
   videos on mobile and tablet".

   ⚠ THE GROUP WAS ALREADY TRANSITIONING THE TWO PROPERTIES THAT COULD NOT
   CARRY IT. `.hero-media .hero-cards` transitions `opacity` and `transform`
   only. The collapse also sets `height:0`, `margin:0` and
   `visibility:hidden`, and NONE of those were in the transition list — so the
   panels faded while the box they sat in vanished in the same frame. The
   result reads as a snap with a fade painted over it, which is exactly the
   abrupt intro/outro she is describing.

   ⚠ height CANNOT BE TRANSITIONED FROM auto, which is why this uses
   max-height. The open state has no intrinsic height to animate from, so the
   collapse sets `height:auto` and drives `max-height` instead.

   ⚠ THE CEILING HAS TO SIT JUST ABOVE THE CONTENT, NOT FAR ABOVE IT. The
   first version used 1400px and the OPEN direction still read as a snap:
   measured at 390, the box was at 449 of its 450px within 80ms, because
   max-height was travelling to 1400 and passed the content's real height in
   the first sixth of the ramp. Everything after that was the box sitting
   still while only the fade ran, and the surrounding content had already
   been shoved down in one frame.

   520px is chosen against the tallest real case: two 16:9 panels stacked with
   their gap measure about 450px at 390 wide, roughly 478 at 414 and under 415
   at 320; the pair sits side by side, and shorter, on tablet. The box
   therefore reaches its natural height around 87% of the ramp at phone
   widths. If the cards ever grow past 520 they will be cut off when OPEN,
   which is a visible defect rather than a subtle one: raise this number, do
   not switch back to height.

   ⚠ max-height IS TIMED linear WHILE EVERYTHING ELSE KEEPS ease-out, and that
   is deliberate. ease-out front-loads: with it, the box covered 70% of its
   travel in the first quarter of the ramp, so at 390 it hit its full 450px
   by 150ms of a 550ms move and the remaining 400ms was a fade over a box
   that had already finished. Linear spreads the growth evenly across the
   duration, which is what makes the panels look like they open rather than
   appear. The fade and the slide still ease, so the motion itself is not
   mechanical.

   ⚠ visibility IS DELAYED, NOT TRANSITIONED. It is a discrete property: it
   flips at whichever end of the duration the delay puts it. Opening, it flips
   to visible at 0s so the panels are there for the whole move. Closing, it
   holds visible until the move finishes, so the group does not blink out
   halfway. Without this the fade is invisible on the way out.

   The 3D CTA gets the same treatment — it collapses alongside the panels and
   had no transition of its own at all, only `will-change`.

   Up to 1024px so it covers tablet landscape as well as phones, which is the
   width her screenshot was taken at. Desktop is unaffected: above 1024 the
   group does not start collapsed and this never runs. */
@media (max-width:1024px){
  .hero-media .hero-cards{
    max-height:520px;
    transition:
      opacity .5s var(--ease),
      transform .5s var(--ease-out),
      max-height .55s linear,
      margin .55s var(--ease-out),
      visibility 0s linear 0s;
  }
  .hero-media.is-collapsed .hero-cards{
    height:auto;
    max-height:0;
    transition:
      opacity .38s var(--ease),
      transform .38s var(--ease-out),
      max-height .5s linear,
      margin .5s var(--ease-out),
      visibility 0s linear .5s;
  }
  .hero-media .hero-3d-cta{
    transition:
      opacity .45s var(--ease),
      transform .45s var(--ease-out),
      visibility 0s linear 0s;
  }
  .hero-media.is-collapsed .hero-3d-cta{
    transition:
      opacity .35s var(--ease),
      transform .35s var(--ease-out),
      visibility 0s linear .45s;
  }
}

/* Someone who asked for less motion gets the state change with no travel. */
@media (prefers-reduced-motion:reduce){
  .hero-media .hero-cards,
  .hero-media.is-collapsed .hero-cards,
  .hero-media .hero-3d-cta,
  .hero-media.is-collapsed .hero-3d-cta{ transition:none; }
}

/* ════════════════════════════════════════════════════════════════════════════
   V162 · THE TABLET HERO: THE MEDIA GROUP COMES OUT OF THE FLOW
   ════════════════════════════════════════════════════════════════════════════
   Bianca, 24 Sep, with an annotated shot: "the height shouldn't be extended
   after showing the video ... Move the show video button on the lower right
   and move a bit downwards the titles as well", plus "the hide video is now
   missing and also no spacings".

   ⚠ MEASURED FIRST, TOGGLING THE GROUP OPEN AND READING THE HERO'S HEIGHT:

       768 x 1024   942px -> 1059px   +117
       820 x 1180   960px -> 1095px   +135
       900 x 1200   960px -> 1106px   +146
      1024 x  768   707px ->  707px      0   (stable)

   The hero grows by roughly a panel's height every time the videos are shown,
   and shrinks again when they are hidden. At 1024 it does not, which is the
   clue: `@media (max-width:1199px)` collapses `.hero-foot` to ONE column and
   gives `.hero-aside` `order:2`, so from there down the media group is a
   block in normal flow underneath the headline. Anything it does to its own
   height, the hero inherits. Above that it is a grid column beside the
   headline and cannot push the hero at all.

   That same stacking is why the control drifts inward: measured 180px from
   the hero's right edge at 768 and 308px at 900, because it is riding the
   left-aligned stack rather than the hero's right edge.

   And it is why "the hide video is now missing". Expanding pushes the whole
   group DOWN by the height it just gained — at 768 the control lands at y=957
   inside a viewport 1024 tall, so on a shorter tablet, or once the page has
   scrolled at all, the button the visitor needs in order to undo what they
   just did has left the screen.

   One cause, three symptoms. Taking the group out of flow fixes all three:
   the hero cannot inherit a height it no longer contains, the group can be
   pinned to the right edge, and the control stays where it was put.

   ⚠ THE GROUP IS PINNED TO .hero-foot, NOT .hero. `.hero-foot` already
   carries `position:relative` and `z-index:2`, and the slider controls are
   centred over it; anchoring to the same box keeps the two on one baseline
   instead of leaving them to drift apart as the hero's height changes.

   ⚠ NOT APPLIED AT 1024 AND ABOVE. From 1200 up the group is a grid column
   and must stay one. Between 901 and 1199 the hero is already stable (the
   videos are not collapsed by default there — see V150's 900px breakpoint) so
   this is scoped to 900 and below, which is where the defect lives and where
   the collapse-on-load actually runs. */
@media (max-width:900px){
  .hero-foot{
    /* The titles move down, as asked. The old value bottomed out at 150px,
       which on a tall tablet left the headline sitting high with a large
       empty band beneath it. */
    padding-top:clamp(180px,32vh,300px);
  }

  /* ⚠ `:has(.hero-media)` IS IN THIS SELECTOR TO MATCH SPECIFICITY, NOT
     because the condition is needed. The desktop floor is written as
     `.hero-aside:has(.hero-media){ min-width:clamp(280px,34vw,520px) }`,
     which is (0,2,0) — `:has()` contributes its argument's specificity. A
     plain `.hero-aside` here is (0,1,0) and LOSES, even inside a media query
     and even later in the file. Measured while it was losing: width computed
     to 280px with min-width 280px, and the panels still sat on the CTA. */
  .hero-aside:has(.hero-media){
    position:absolute;
    /* ⚠ var(--edge), NOT 0. An absolutely positioned child resolves its
       offsets against the containing block's PADDING BOX, so `right:0` sits
       flush with the outer edge and ignores .hero-foot's own gutter entirely
       — measured 0px of inset while the headline above it sat on 27.6px at
       768 and 32.4px at 900. .hero-foot carries the `wrap` class, whose
       padding-inline IS var(--edge), so reusing the token puts the control on
       exactly the same line as the headline and the slider counter instead of
       against the glass. */
    right:var(--edge);
    /* Level with the slider controls row, which is the last thing in the
       flow. Its own bottom padding is the foot's, so matching that puts the
       two on the same baseline — her red box sits exactly there. */
    bottom:0;
    z-index:4;
    align-items:flex-end;
    /* ⚠ THE WIDTH IS WHAT KEEPS THE PANELS OFF THE HEADLINE, and it is the
       only lever that works. Out of flow and bottom-anchored, the panels grow
       UPWARD past the CTA row, so vertical spacing cannot separate them —
       only horizontal can. Measured, the CTA row's right edge sits at 534 at
       768 and 538 at 900, near enough fixed because the two buttons size to
       their labels. 26vw lands the panel column's left edge at 540 and 638
       respectively, clear of it at both, and the 300px cap stops the panels
       growing back over the headline on a wide tablet.

       A first attempt at 380px put the panels' left edge at 360 and they
       covered the headline and the BOOK A SHOWROOM VISIT button outright.
       Do not raise this without re-measuring that right edge.

       ⚠ 30vw/260px, NOT 26vw/300px. The narrower figure cleared the CTA at
       every width but left only ~107px for the walkthrough label once HIDE
       took its share of the row, which is what forced the label to break
       badly. 260px gives the CTA the two-line wrap her reference shows and
       still clears the CTA row at 820 and above; at 768 it runs about 24px
       into the end of BOOK A SHOWROOM VISIT, and only while the videos are
       open. Flagged to Bianca rather than silently traded away. */
    width:min(30vw, 260px);
    /* ⚠ AND A HARD CAP AGAINST THE CTA ROW, because a vw figure cannot know
       where that row ends. The two hero buttons size to their labels, so
       their right edge is near enough fixed at ~535px whatever the viewport
       does; 100vw minus that, minus the gutter, is the widest this column can
       be without landing on them. Measured before the cap: 700 and 768 both
       overlapped while 820 and 900 were clear, which is exactly the shape of
       a percentage outrunning a fixed obstacle. */
    max-width:calc(100vw - var(--edge) - 545px);
    /* ⚠ THE min-width HAS TO BE UNSET OR THE WIDTH ABOVE DOES NOTHING.
       `.hero-aside:has(.hero-media){ min-width:clamp(280px,34vw,520px) }`
       floors the column at 280px for the desktop two-column layout. Measured
       with the width already set: computed width 280px, min-width 280px — the
       panels were still landing on the CTA at 768 and the width looked like
       it had been ignored. min-width beats width, always. */
    min-width:0;
    /* Out of flow, so the slider row underneath keeps its full width and the
       group cannot push it, or the hero, anywhere. */
    pointer-events:none;
  }
  /* The group is inert as a box but every control inside it is not — the
     pointer-events pair is what lets the slider row stay clickable across its
     whole width while the panels float above its right-hand end. */
  .hero-aside:has(.hero-media) > *{ pointer-events:auto; }

  .hero-media{ width:100%; max-width:100%; }

  /* The panels grow UPWARD from the control rather than downward, so opening
     them never moves the button that closes them. */
  .hero-media .hero-cards{ margin-bottom:.7rem; }

  /* The slider row keeps clear of the pinned group. Without this the 01/08
     counter and the panels share the same strip at the narrow end. */
  .hero-slider-ui{ padding-right:clamp(0px,12vw,150px); }

  /* ⚠ THE 3D CTA AND THE CONTROL STAY SIDE BY SIDE, per Bianca's reference
     shot of the opened state (24 Sep): panels stacked, then one row with the
     walkthrough CTA and HIDE beside it, then the slider counter underneath.

     V162 briefly stacked them. That was wrong against the reference, and it
     was solving a real problem the wrong way: at the 200px group width the
     CTA's label broke to "Take a Virtual / Walkthrou / Now". The fix is to
     make the row fit, not to change its shape — the type steps down and the
     control drops its label to an icon, which is what buys the CTA the width
     it needs to hold two lines as her shot shows.

     ⚠ THE WORD ON THE CONTROL STAYS. A first pass here clipped the label to
     an icon to buy width; her reference shows "HIDE" spelled out beside the
     CTA, so the width comes from the group instead. */
  /* ⚠ V163 — THE ROW BREAKS OUT OF THE PANEL COLUMN INSTEAD OF SHRINKING TO
     FIT IT. Bianca, 24 Sep: "It looks cramped. It should be as clean as
     mobile." Measured at 768 with the videos open, that was exactly right:
     the CTA had 120px to work with, so "Take a Virtual Walkthrough Now" broke
     to FOUR lines at .74rem and the sub-line to three.

     The column is narrow for one reason only — the PANELS have to clear the
     hero buttons, whose right edge sits at 534/536/538 across 768/820/900.
     The CTA row sits BELOW those buttons, on the slider counter's line, and
     that strip is empty: the counter ends at 388/390/392 and nothing occupies
     the ~150px between it and the panel column. The row was being squeezed by
     a constraint that does not apply to it.

     So the panels keep the narrow column and the row alone steps left out of
     it, into space that was already free. Measured after: 2 lines at 768, 2
     at 820, 1 at 900, no overlap with the buttons, the counter or each other,
     no horizontal overflow, and the hero height still 942 unchanged at every
     pinned width. The type goes back to near its normal size because the row
     no longer has to earn its width by shedding it. */
  .hero-media .hero-3d-row{
    --mmh-3d-out: clamp(120px, 17vw, 200px);
    width:calc(100% + var(--mmh-3d-out));
    margin-left:calc(var(--mmh-3d-out) * -1);
  }
  .hero-3d-row{ grid-template-columns:minmax(0,1fr) auto; gap:.6rem; align-items:stretch; }
  .hero-3d-main{ font-size:.86rem; line-height:1.25; }
  /* ⚠ THE LETTER-SPACING IS THE REAL COST ON THE SUB-LINE, not the font size.
     "EXPLORE THE TOWNHOUSE IN 3D" is 27 characters of uppercase mono at
     .14em tracking — measured, that broke to FOUR lines in the ~100px the
     row leaves it, against the two her reference shows. Dropping the
     tracking to .05em buys back roughly a quarter of the line's width, which
     is what gets it onto two lines without touching the group's width and
     reintroducing the overlap with the CTA row. */
  /* V163 — .52rem/.05em was the cost of the 120px column. With the row out of
     that column the sub-line holds one or two lines at a readable size. */
  .hero-3d-cue{ font-size:.56rem; letter-spacing:.1em; line-height:1.3; }
  .hero-3d-cta{ padding-inline:.95rem; }
  .hero-3d-mark{ flex:0 0 auto; }
  .hero-3d-mark svg{ width:22px; height:22px; }
  .hero-media-toggle{ padding-inline:.9rem; white-space:nowrap; }
}

/* ⚠ THE GROUP RETURNS TO THE FLOW AT 640, NOT 520, AND THE NUMBER IS NOT
   ARBITRARY. `@media (max-width:640px)` sets `.hero-actions{ width:100% }`,
   so from 640 down the two hero CTAs span the whole column and there is no
   right-hand strip left for a pinned group to occupy — anything floated over
   that edge lands on a button.

   Caught by measurement after the group was widened to 260px: at 640 the
   panel column ran 425–617 against a CTA row ending at 529, a 104px overlap
   that the earlier 200px width had been just narrow enough to avoid.

   ⚠ THE THRESHOLD IS 719, NOT 640. Below about 720 the cap above leaves the
   column under ~150px — narrower than one readable panel — so there is no
   width at which a pinned group both clears the buttons and is worth looking
   at. It goes back into the flow instead.

   Below it the group simply sits last in the flow. The hero's height changing
   there is the lesser problem: a 260px panel column pinned over a 390px
   screen would cover the controls entirely. */
/* ⚠ V163 — THE THRESHOLD MOVES AGAIN, 719 -> 767, AND MEASUREMENT MOVED IT.
   With the CTA row broken out, 768 and up are clean, but 720 was not: the cap
   `calc(100vw - var(--edge) - 545px)` leaves the panel column ~149px there,
   the row's 17vw step-out is only 122px, and the row landed ON the hero
   buttons — measured rowOverBtn:true with the label back to four lines.

   720-767 therefore goes in flow, the same as a phone, which is the layout
   Bianca asked these widths to read like. The hero grows ~170px there when
   the videos open; that is the accepted trade below the pinned band, and the
   band where the height must not move (768-900) is untouched. */
/* ⚠ V164 — THE PINNED GROUP IS GONE. TABLET IS THE PHONE LAYOUT NOW.
   Bianca, 24 Sep, with a phone screenshot beside the tablet one: "it should be
   fullwidth too like the 2nd image."

   That retires the whole right-hand pinned column between 768 and 900 — the
   panels, the CTA row, the counter all go back into the flow at full width,
   exactly as they do on a phone. Everything the block above negotiates
   (min-width:0, the 545px cap, the 17vw step-out, the pointer-events pair)
   exists only to make a pinned column work and is simply overridden here.
   Those rules are left in place rather than deleted because they are still
   live for the desktop two-column layout above 900.

   ⚠ THE HERO NOW GROWS ON TABLET, AND THAT IS THE ACCEPTED TRADE.
   Measured with the videos open, with the 180px top padding kept:
   942 -> 1205 at 768, 1206 at 820, 1213 at 900.
   Her earlier instruction was that the height must not extend at these widths
   (V162), and pinning the group was how that was met. Full width and a fixed
   height cannot both be true: a full-width panel is in the flow, and anything
   in the flow adds its height to the hero. The newest instruction wins, and
   the phone has behaved this way all along.

   Measured after, videos open, at 768 / 820 / 900:
     panels    one column, 713 / 761 / 835 wide, on the page gutter
     CTA row   full width, label one line, sub-line one line
     HIDE      present, >=44px, beside the CTA
     overflow-x  none at any width

   ⚠ .hero-foot KEEPS ITS 180px TOP PADDING HERE. The lowered titles she asked
   for in V162 are unrelated to where the media group sits, so that value is
   deliberately NOT overridden until 767, where the phone's own figure takes
   over. */
@media (max-width:900px){
  .hero-aside:has(.hero-media){ position:static; width:auto; min-width:0; max-width:100%; align-items:flex-start; pointer-events:auto; }
  .hero-slider-ui{ padding-right:0; }
  /* Back in flow the row has the full column, so the step-out is cancelled. */
  .hero-media .hero-3d-row{ width:100%; margin-left:0; }
}

@media (max-width:767px){
  .hero-foot{ padding-top:clamp(120px,24vh,200px); }
}

/* ⚠ V165 — TWO REAL DEFECTS, PLUS THE STACK ITSELF WAS THE WRONG SHAPE HERE.
   Bianca, 24 Sep, screenshot of the opened state at tablet: "There should be
   spaces and buttons and lower details are visible."

   DEFECT ONE — A 45% TAP-SCALE. `.hero-card--video:hover{ transform:scale(1.449) }`
   is the desktop expand gesture. The small-lift override for touch was scoped
   `@media (max-width:720px)`, so from 721 up it was live — and on a tablet
   hover fires on TAP. Measured at 768 with the panels full width: the second
   card computed matrix(1.449) and rendered 1033px wide inside a 713px column,
   which is the panel bleeding over its neighbour in her shot. The lift stays,
   the scale goes, up to 900. Above that it is a real pointer and the gesture
   is deliberate, so it is left alone.

   DEFECT TWO — THE CLIP CEILING WAS BELOW THE CONTENT. `.hero-cards` carries
   `max-height:520px` from V159's open/close ramp, and the rule for that
   ceiling is that it sits JUST ABOVE real content. Full width, real content
   measured 858px at 768 and 996px at 900. `overflow` is visible, so nothing
   was hidden — the panels simply painted straight over the CTA row and the
   counter while the layout below them only moved 520px. That is "the lower
   details are not visible".

   AND THE STACK — measured, a full-width 16:9 panel is 424px tall at 768 and
   493px at 900. Two of them plus the CTA row and the counter is 1561px of
   hero against a 1024px screen, so the controls could not share a screen with
   the panels at any ceiling. Side by side is the full-width shape that fits:
   the PAIR spans gutter to gutter, which is what full width means on a
   viewport this wide, and the phone keeps stacking because at 390 there is no
   room for two.

   Measured after, videos open:
     768   panels 347x218 each, 28 -> 740, gap 18, row at 833, counter 925-953
     820   panels 371x232 each, 30 -> 790, gap 18, row at 833, counter 925-953
     900   panels 409x253 each, 32 -> 868, gap 18, row at 843, counter 935-963
   No clipping, no spill past the gutter, no horizontal overflow, and the whole
   opened state sits inside a 1024px screen at every width.

   ⚠ AND THE HERO STOPS MOVING AGAIN. 942 -> 942 at 768 and 820, 942 -> 953 at
   900. V164 traded that away because a stacked full-width panel has to grow
   the hero. Side by side gives back both halves of what she asked for. */
@media (max-width:900px){
  .hero-card--video:hover{
    transform:translateY(-3px);
    box-shadow:0 14px 34px rgba(0,0,0,.42);
  }
}
@media (min-width:768px) and (max-width:900px){
  .hero-media .hero-cards{
    grid-template-columns:1fr 1fr;
    /* The "spaces" she asked for: 10px read as two panels touching once they
       were this wide. */
    gap:18px;
    /* Just above the 253px the taller pair measures at 900, per the V159 rule
       that this ceiling tracks real content. */
    max-height:460px;
    margin-bottom:1.15rem;
  }
}

/* ════════════════════════════════════════════════════════════════════════════
   V178 · CARD 103 — THE SINGLE-POST LAYOUT AND ITS SIDEBAR
   Bianca, 25 Sep: "Can we prepare a nice layout for the single blogs? Like
   have a prepared sidebar ... Make sure it's match to the other pages like
   fixing the alignments, the content width and so on."

   THE ALIGNMENT COMPLAINT, AND WHAT IT ACTUALLY WAS. The article was a single
   760px column centred inside the 1336px .wrap every other internal page fills.
   Nothing was misaligned in the CSS sense — it was centred correctly — but a
   narrow strip of text floating in a wide page does not look like the rest of
   the site, and that is what reads as wrong. Giving the spare width a job
   fixes it: reading column plus sidebar, filling the wrap the way the model
   and content pages do.

   The 760px measure itself is UNCHANGED and that is deliberate. It is roughly
   90 characters at this size, which is what long prose wants; widening the
   text to fill 1336px would be the opposite of a fix.
   ════════════════════════════════════════════════════════════════════════════ */
.post-layout{
  display:grid;
  /* The sidebar is capped rather than taking every spare pixel. Measured
     with minmax(0,1fr) it came out 519px at 1440, which is not a sidebar, it
     is a second column of equal weight competing with the article. 340 holds
     a contents line on one or two lines and keeps the reading column as the
     obvious primary. justify-content centres the pair in the wrap. */
  grid-template-columns:minmax(0,1fr) minmax(0,340px);
  gap:clamp(32px,3.2vw,68px);
  /* ⚠ V179 — STRETCH, NOT START, AND THE STICKY DEPENDS ON IT. With
     align-items:start the sidebar column is only as tall as its own cards, and
     position:sticky can only travel inside its containing block — so the
     sidebar stuck for about a screen and then scrolled away for the rest of
     the article. Measured: 4000px down the page its top sat at -2056px, i.e.
     long gone. Letting the column stretch to the row height gives the sticky
     the full length of the article to travel in. */
  align-items:stretch;
  /* ⚠ V181 — THE ARTICLE COLUMN NOW TAKES THE SPARE WIDTH. Bianca, 25 Sep:
     "shouldn't be the content auto adjust on the right side since it still has
     space?"

     She was right and V180 only half-answered her. Pushing the columns to the
     track's edges fixed WHERE the article starts, but it left the spare width
     sitting between them as dead gutter, and the gutter grows with the screen:
     measured 236px at 1440, 452px at 1672, 522px at 1920, while the text stayed
     at 760 throughout. On a wide monitor that is a narrow ribbon of text with
     half a screen of nothing beside it.

     The article column is now flexible and takes what is left after the
     sidebar and a fixed gutter, so the space goes into the reading column
     instead of between the two.

     ⚠ AND THE TYPE SCALES WITH IT, which is the part that makes this safe.
     A 900px column at the old 1.06rem is about 105 characters a line, well past
     what is comfortable, and "fill the space" would have quietly traded
     alignment for readability. The article font-size now scales with the
     viewport, so the wider column carries larger text and the measure stays in
     the 90s rather than climbing. See .post-content below.

     The layout is capped so the gutter cannot run away again on a very large
     display. 1340px is chosen to sit just above the 1336px track at 1440, so
     the cap does nothing at common widths and the article still begins on the
     wrap's own left edge there — which is what V180 was asked for. */
  max-width:1340px;
  margin-inline:auto;
  /* ⚠ V180 — SPACE-BETWEEN, NOT CENTRE. Bianca: "the blog content width is not
     following to the other's pages width."

     Measured at 1440: every other page runs its content from x=52 to x=1388,
     the wrap's own edges. This grid filled that track correctly but centred
     760 + 340 of columns inside it, leaving 89px of dead space at each end —
     so the article began at x=141 and the sidebar ended at x=1299. Nothing
     lined up with the page above or below it.

     Pushing the two columns to the track's edges puts the article's first
     character at x=52, exactly where About Us, Can I Build and the model pages
     start theirs, and the sidebar's right edge at x=1388.

     The 760px reading measure is deliberately NOT widened to close the gutter.
     Filling the track with text would make the measure about 105 characters,
     which is a readability regression dressed up as an alignment fix. A wide
     gutter between an article and its sidebar is ordinary editorial layout;
     text running the full width of a 1440px screen is not. */
  justify-content:space-between;
}
/* .post-content and .post-foot both carry `margin-inline:auto` and a 760px cap
   from the rules further up this file, written when the article was alone in
   the wrap. Inside the grid column the cap is the column, so the auto margins
   would only push the text off-centre against its own sidebar. */
.post-layout .post-content,
.post-layout .post-foot{ max-width:none; margin-inline:0; }

.post-side{ position:relative; height:100%; }
/* Sticky, so the contents list stays reachable through a long article. The
   offset clears the fixed header using the same value the rest of the site
   uses for scroll-padding, not a number guessed here. */
/* ⚠ V179 — STICKY WITH A VIEWPORT CAP. Bianca: "is it possible that the right
   column/sidebar is fixed as you scroll". It was already sticky, but sticky
   alone only works while the column is shorter than the viewport, and the
   sidebar has since gained a categories card and four article rows with
   thumbnails. Past that height the top of it scrolls away and never comes
   back, which reads as the sticky having failed.

   Capping it to the space between the header and the bottom of the screen and
   letting it scroll inside itself keeps the whole thing reachable at any
   height. The top offset is the same value the site uses for
   scroll-padding-top, so it clears the fixed header rather than guessing.

   ⚠ THIS MAKES THE SIDEBAR A SCROLL CONTAINER, which is exactly the hazard the
   mmhHeadWipe note warns about: a scroll container freezes any `view()`
   timeline inside it, and the side cards contain <p> and a .btn, both of which
   the site's scroll entrances would otherwise match. Frozen mid-entrance means
   permanently invisible. `.post-side *` is excluded from all three of those
   rules for that reason — the exclusion is load-bearing, not tidiness. */
.post-side-inner{
  position:sticky; top:clamp(96px,11vh,124px);
  max-height:calc(100vh - clamp(96px,11vh,124px) - 1.5rem);
  overflow-y:auto; overscroll-behavior:contain;
  display:flex; flex-direction:column; gap:clamp(14px,1.6vw,20px);
  /* Room for the scrollbar so it never sits on a card's border. */
  padding-right:4px;
  scrollbar-width:thin;
  scrollbar-color:var(--line) transparent;
}
.post-side-inner::-webkit-scrollbar{ width:6px; }
.post-side-inner::-webkit-scrollbar-track{ background:transparent; }
.post-side-inner::-webkit-scrollbar-thumb{ background:var(--line); border-radius:3px; }
.side-card{
  background:var(--paper-2,#131D26);
  border:1px solid var(--line-soft);
  border-radius:12px;
  padding:clamp(16px,1.6vw,22px);
}
.side-title{
  margin:0 0 .85rem;
  font-family:var(--mono); font-size:.68rem; letter-spacing:.18em;
  text-transform:uppercase; color:var(--gold-deep);
}

/* ── On this page ─────────────────────────────────────────────────────────
   Numbered, because the list mirrors the order of the article and the number
   is the cheapest way to say so. counter() rather than a real <ol> marker so
   the number can be gold and hang in its own column without the text wrapping
   underneath it. */
.side-toc-list{
  list-style:none; margin:0; padding:0;
  counter-reset:mmh-toc;
  display:flex; flex-direction:column; gap:.55rem;
}
.side-toc-list li{ counter-increment:mmh-toc; margin:0; }
.side-toc-list a{
  display:grid; grid-template-columns:1.7em 1fr; gap:.15rem;
  font-size:.92rem; line-height:1.4;
  color:var(--muted-dk); text-decoration:none;
  transition:color .25s var(--ease);
}
.side-toc-list a::before{
  content:counter(mmh-toc,decimal-leading-zero);
  font-family:var(--mono); font-size:.68rem; letter-spacing:.06em;
  color:var(--gold-deep); padding-top:.22em;
}
.side-toc-list a:hover,
.side-toc-list a:focus-visible{ color:var(--paper); }

/* ── More articles ───────────────────────────────────────────────────────── */
.side-more-list{ list-style:none; margin:0; padding:0; }
.side-more-list li + li{ margin-top:.2rem; border-top:1px solid var(--line-soft); }
.side-more-list a{
  display:block; padding:.7rem 0; text-decoration:none;
  transition:color .25s var(--ease);
}
.side-more-list li:first-child a{ padding-top:0; }
.side-more-title{
  display:block; font-size:.95rem; line-height:1.35; color:var(--paper);
}
.side-more-date{
  display:block; margin-top:.3rem;
  font-family:var(--mono); font-size:.66rem; letter-spacing:.12em;
  text-transform:uppercase; color:var(--muted-2);
}
.side-more-list a:hover .side-more-title,
.side-more-list a:focus-visible .side-more-title{ color:var(--gold-hi); }

/* ── The call to action ──────────────────────────────────────────────────── */
.side-cta .btn{ width:100%; justify-content:center; }
.side-cta-alt{ display:inline-block; margin-top:.9rem; font-size:.9rem; }

/* ── Narrow ──────────────────────────────────────────────────────────────
   One column, and the sticky comes off with it: a sidebar that sticks while
   it is stacked underneath the article would pin itself over the footer. The
   reading column keeps its own centring back, since it is alone again. */
@media (max-width:1100px){
  .post-layout{ grid-template-columns:minmax(0,1fr); }
  .post-layout .post-content,
  .post-layout .post-foot{ max-width:760px; margin-inline:auto; }
  .post-side-inner{
    position:static; max-height:none; overflow:visible; padding-right:0;
  }
  .post-side{ max-width:760px; margin-inline:auto; width:100%; }
}

/* V178 — a bare classic-editor table scrolls inside its own box instead of
   pushing the page sideways. See the wrapper note in functions.php for why
   this is a wrapper and not `display:block` on the table. */
.post-content .post-table-scroll{ overflow-x:auto; margin:2rem 0; }
.post-content .post-table-scroll > table{ margin:0; min-width:min(100%,520px); }

/* ── V179 · Categories, and article rows with their featured image ───────── */
.side-cat-list{
  list-style:none; margin:0; padding:0;
  display:flex; flex-wrap:wrap; gap:.4rem;
}
.side-cat{
  display:inline-flex; align-items:center; gap:.4rem;
  padding:.34rem .6rem;
  border:1px solid var(--line); border-radius:999px;
  background:rgba(255,255,255,.04);
  font-size:.8rem; line-height:1; color:var(--muted-dk);
  text-decoration:none;
  transition:color .25s var(--ease), border-color .25s var(--ease);
}
.side-cat:hover,
.side-cat:focus-visible{ color:var(--paper); border-color:var(--gold-deep); }
.side-cat-n{
  font-family:var(--mono); font-size:.62rem; letter-spacing:.06em;
  color:var(--gold-deep);
}

/* The thumbnail is a fixed box and the picture fills it, so rows stay the same
   height whatever shape the client's featured images happen to be — the same
   reasoning as the gallery tiles on the model pages. */
.side-more-list a{ display:grid; grid-template-columns:72px 1fr; gap:.7rem; align-items:start; }
.side-more-thumb{
  display:block; width:72px; aspect-ratio:4/3; overflow:hidden;
  border-radius:8px; background:var(--ink-2);
  color:var(--muted-2);
}
.side-more-thumb img{
  width:100%; height:100%; object-fit:cover; display:block;
  border-radius:0;
}
.side-more-thumb svg{
  width:24px; height:24px; margin:calc((54px - 24px) / 2) auto; display:block;
}
.side-more-text{ min-width:0; }
.side-more-list .side-more-title{ font-size:.9rem; }

/* ── V181 · the current section in the contents list ─────────────────────
   Gold text, gold number and a gold bar in the number's own column, so the
   mark reads at a glance without the row moving — no padding or border that
   would shift the text sideways as the reader scrolls. */
.side-toc-list a{ position:relative; }
.side-toc-list a.is-current{ color:var(--paper); }
.side-toc-list a.is-current::before{ color:var(--gold-hi); }
.side-toc-list a.is-current::after{
  content:""; position:absolute; left:-.75rem; top:.35em; bottom:.35em;
  width:2px; border-radius:1px; background:var(--gold);
}
@media (prefers-reduced-motion: no-preference){
  .side-toc-list a{ transition:color .25s var(--ease); }
}
