/* =============================================================================
   refresh.css, the 2026-08 feedback pass.

   Sits after responsive.css and site.css so it wins ties. Everything here is an
   override of the design export, for the same reason responsive.css exists: the
   bundle is re-exported wholesale (see `mix site.import`), so a hand edit to
   index.html would not survive. Anything that needs *markup* rather than style
   is a patch in lib/mix/tasks/site.import.ex; anything that is only paint or
   motion lives here.

   Contents
     1  Typography rhythm
     2  Corners (square) + shadows (flat)
     3  Section reveal (3b: editorial motion)
     4  Cursor
     5  Top stripe (socials | phone | CTA)
     6  Sticky Enquire, chat + WhatsApp stack
     7  Footer
     8  Trusted Partners, framed article images, Channel Partner cards
     9  The body scroll container
    10  About "Our Journey": the card deck
    11  Intro
    12  Inner-page top padding
    13  Page transition curtain
   ========================================================================== */

/* --- 1. Typography rhythm --------------------------------------------------
   The export runs body copy at 1.7-1.8 and hangs headings straight off the
   paragraph below them, so a page reads as one grey slab. Tighten the leading,
   open the gap under a headline so it separates from its body, and leave list
   items alone, bullets and pointers were explicitly kept as they are. */
body p,
body [role="paragraph"] {
  line-height: 1.58 !important;
}

/* the export sets leading inline on the copy blocks too, not only on <p> */
body p[style*="line-height: 1.8"],
body p[style*="line-height:1.8"],
body p[style*="line-height: 1.75"],
body p[style*="line-height:1.75"],
body p[style*="line-height: 1.7"],
body p[style*="line-height:1.7"] {
  line-height: 1.58 !important;
}

body h1,
body h2 {
  line-height: 1.16 !important;
}

body h3 {
  line-height: 1.24 !important;
}

/* Headline -> body. `+ p` only: a heading followed by a wrapper div keeps the
   layout's own gap, and overriding those would move whole sections. */
body h1 + p,
body h2 + p,
body h3 + p {
  /* 24px until 2026-08-18. Where the heading and its paragraph are flex
     children the layout already puts a gap between them, so the margin landed
     on top of it and the sentence floated away from its headline. 8px reads as
     "these belong together" in both cases. */
  margin-top: 8px !important;
}

/* Within a run of prose the paragraphs sit closer than the headline gap. */
body p + p {
  margin-top: 0.72em !important;
}

/* Bullets and pointers keep the export's spacing, stated in the feedback. */
body li,
body li p {
  line-height: inherit !important;
  margin-top: revert !important;
}

/* No widows: never leave a sentence's last word alone on its own line.
   `text-wrap: pretty` is the browser's own fix, it looks ahead at the last few
   lines and pulls a word down rather than stranding one, at no runtime cost and
   with no markup. Headings get `balance`, which evens every line instead, so a
   two-line headline breaks near the middle rather than leaving one word under a
   full line.

   Where it is unsupported (Safari, older Firefox) the text simply wraps as it
   does today; nothing breaks, the widow just comes back. */
body p,
body li,
body blockquote,
body figcaption,
body .fm-md {
  text-wrap: pretty;
}

body h1,
body h2,
body h3,
body h4 {
  text-wrap: balance;
}

/* CMS-authored article bodies (F-Desk, News) render as markdown inside .fm-md.
   Same rhythm the hand-written articles use: a wide gap above a heading, a
   narrow one below, so a section reads as a group instead of one even column
   (2026-08-18). */
body .fm-md > :where(h1, h2, h3, h4) {
  margin: 58px 0 8px !important;
  font-family: var(--font-display);
  letter-spacing: 0.06em;
}

body .fm-md > :where(h1, h2, h3, h4):first-child {
  margin-top: 0 !important;
}

body .fm-md > p {
  margin: 18px 0 0 !important;
}

body .fm-md > p:first-child {
  margin-top: 0 !important;
}

/* Milestone timeline: un-pin on phones (mirrors the vv-values block above).
   The scroll-jacked 100vh stage (refresh.js sets [data-ms-track] height and
   translates [data-ms-stage]) has room for only the year on a narrow screen, the two-column card collapses to a ~160px sliver and its reveal state never
   fires, so every milestone but the year is invisible. On a phone the stage
   goes back into normal flow and each year stacks and shows in full: image over
   text, both revealed. !important beats refresh.js's inline (non-important)
   height/transform writes, so the pin is genuinely released, not fought each
   scroll tick. */
@media (max-width: 860px) {
  [data-ms-track] {
    height: auto !important;
  }

  [data-ms-stage] {
    position: static !important;
    height: auto !important;
    transform: none !important;
    display: block !important;
    padding: 48px 20px 8px !important;
  }

  [data-ms-slide] {
    position: static !important;
    inset: auto !important;
    height: auto !important;
    opacity: 1 !important;
    transform: none !important;
    margin-top: 40px;
  }

  /* Stack the photograph over the copy instead of a 42% column that starves it. */
  [data-ms-card] {
    grid-template-columns: 1fr !important;
    height: auto !important;
  }

  [data-ms-shot] {
    min-height: 200px !important;
  }

  /* The reveal never runs for the pinned slides, so their content is parked at
     opacity:0 forever on mobile, show it. Covers both the ms-* nodes and any
     data-reveal* children the runtime tags inside them. */
  [data-ms-slide] [data-ms-shot],
  [data-ms-slide] [data-ms-title],
  [data-ms-slide] [data-ms-tag],
  [data-ms-slide] [data-ms-text],
  [data-ms-slide] .fm-in,
  [data-ms-slide] [data-reveal],
  [data-ms-slide] [data-reveal-child],
  [data-ms-slide] [data-reveal-img],
  [data-ms-slide] [data-reveal-text],
  [data-ms-slide] .fm-mask-i {
    opacity: 1 !important;
    transform: none !important;
  }
}

/* --- 2. Corners and shadows -------------------------------------------------
   Square corners, flat surfaces. The client's direction, and it supersedes the
   earlier "CTA buttons: rectangular with rounded corners" note.

   Interaction transitions are NOT part of this any more, they were removed
   with the corners and then asked for back the next day. See the note at the
   foot of this section.

   Blanket rather than rule-by-rule, because the radii come from three places, inline styles in the export, the folded classes in site.css, and the design
   system's own `--radius-*` tokens, and picking them off individually leaves
   one behind on the next export.

   Circles survive. A disc is not a rounded corner: the social icons, the chat
   and WhatsApp launchers and the numbered step medallions are round objects,
   and squaring them would read as a bug rather than a decision. The inline ones
   are matched on their style attribute; the two folded classes and our own are
   named. */
:root {
  --radius-pill: 0 !important;
  --radius-sm: 0 !important;
  --radius-md: 0 !important;
  --radius-lg: 0 !important;
  /* Flat, not floated: every design-system card reads its lift from these two
     tokens (site.css classes and ~30 inline styles), so emptying them makes
     the whole site 2D at once, asked for on 2026-08-09, pointing at the
     project cards and partner tiles. Functional shadows this file sets by
     hand (the pinned deck's separator, the chat launcher) are hardcoded and
     unaffected. The partner tiles' own 3D look is baked into their PNGs and
     is handled by the flattened copies in partner_list/. */
  --shadow-card: none !important;
  --shadow-float: none !important;
}

body,
body *,
body *::before,
body *::after {
  border-radius: 0 !important;
}

body [style*="border-radius:50%"],
body [style*="border-radius: 50%"],
body .fm-s-76db03,
body .fm-s-cdf2cb,
body #fm-whatsapp,
body [data-top-socials] a {
  border-radius: 50% !important;
}

/* Social icon hover: the disc fills with burgundy and the anchor sets
   `color: var(--white)` for it, but the icon carries its own `color` in
   site.css (.fm-s-778ea8), which stops that inheriting, so the glyph stayed
   dark grey on the burgundy disc (2026-08-18). Keyed on the disc, so every
   icon in one, footer, Contact, top stripe, behaves the same. */
body .fm-s-76db03:hover svg,
body [data-top-socials] a:hover svg {
  color: var(--white) !important;
}

/* Facebook and LinkedIn are drawn solid, not as outlines -------------------
   Asked for 2026-09-03: on the grey disc the inside of these read grey,
   because the export draws every social mark as a 1.6px stroke with
   `fill:none`. Instagram and YouTube are outline marks and are right that way;
   an f and an "in" are silhouettes, and hollow they look like a grey logo on a
   grey disc rather than a white one.

   Both paths already trace their own silhouette, so filling them is the whole
   fix: no new artwork, and `currentColor` keeps them white at rest and white
   on the burgundy hover. Keyed on the anchor's aria-label, which is on both
   copies of the row, the top stripe and the Contact/footer discs. */
body a[aria-label="Facebook"] svg,
body a[aria-label="LinkedIn"] svg {
  fill: currentColor;
  stroke: none;
}

/* WhatsApp is the one that is not a silhouette: it is a bubble with a handset
   inside it, and the mark reads as a bubble. So the bubble keeps its outline
   and only the handset fills, which is the part that was grey inside. Filling
   both turned the whole disc white and swallowed the handset (2026-09-03). */
body a[aria-label="WhatsApp"] svg path:nth-of-type(2) {
  fill: currentColor;
}

/* Outline buttons: a hover that states both colours ------------------------
   The design system's outline variant changes only `background` on hover and
   leaves `color: var(--burgundy)` from its resting style, so whatever fill
   lands there, the label is burgundy on it. Reported on "View All Projects"
   (2026-08-19): the button goes solid burgundy and the type disappears.

   Both halves are set here, so the pair can never drift again: burgundy fill,
   white type, the same hover the hand-written CTAs use (Download PDF, the
   unit-finder chips). Mouse only: `@media (hover: none)` in responsive.css
   flattens this instead, because on a phone :hover latches after a tap. */
@media (hover: hover) {
  body a[style*="border: 1px solid var(--burgundy)"]:hover,
  body button[style*="border: 1px solid var(--burgundy)"]:hover,
  body a[style*="border:1px solid var(--burgundy)"]:hover,
  body button[style*="border:1px solid var(--burgundy)"]:hover {
    background: var(--burgundy) !important;
    border-color: var(--burgundy) !important;
    color: var(--white) !important;
  }
}

/* The Enquire call to action is square like everything else (2026-08-11): the
   6px it used to carry read as soft next to the flat stripe it sits in. Kept as
   an explicit rule so the exemption cannot creep back in on the next export. */
body [data-cursor="gold"] > div > a {
  border-radius: 0 !important;
}

/* Interaction transitions are back ON (2026-08-09: "there is no smooth
   transitions in the website"). A blanket `transition: none` used to live here
   under an earlier "no interaction motion" note; it made every hover, focus,
   open and close snap, which is most of why the site read as not-smooth. The
   export's own .35s eases on links, buttons and cards now apply again. */

/* --- 3. Section reveal + scroll focus --------------------------------------
   The cinematic pass (2026-08-08), re-tuned for smoothness on 2026-08-09:
   every move is transform + opacity only, nothing here can force a repaint, and the durations came down, because a slow effect on a busy frame is what
   reads as lag. Four moves, one per kind of thing, one expo curve:

     * bands sweep in from alternating sides, left, then right, so the page
       assembles like cuts in a title sequence rather than drifting up
     * headings rise and settle from a touch oversized
     * images settle into their frames out of a slight zoom (Ken Burns)
     * grid tiles rise and settle from 96% scale, cascading

   refresh.js tags everything and flips .fm-in on entry. The attribute is added
   by script, never by markup: with JS off nothing is tagged, nothing hidden. */
[data-reveal] {
  opacity: 0 !important;
  transform: translateY(12px);
  transition:
    opacity 0.85s cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 0.85s cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* the sweep: bands alternate sides (refresh.js writes l/r), arriving on a
   shallow diagonal. html clips the x-overflow, so nothing widens the page */
[data-reveal="l"] {
  transform: translate(-44px, 20px);
}

[data-reveal="r"] {
  transform: translate(44px, 20px);
}

[data-reveal].fm-in {
  opacity: 1 !important;
  transform: none;
}

/* On a phone the diagonal has nowhere to come from. A full-width band offset
   44px is 44px of page, and `html { overflow-x: hidden }` does not save it, the document still measures wider than the viewport, and iOS Safari scrolls
   it. Every audited page reported sideways scroll from exactly this. The bands
   arrive straight up instead; there is no room for a sideways entrance at
   390px, and nothing is lost by dropping it. */
@media (max-width: 720px) {
  [data-reveal="l"],
  [data-reveal="r"] {
    transform: translateY(12px);
  }
}

/* Grid tiles rise into place behind their band, one after another, and back out
   on the way up: refresh.js takes .fm-in off again once a tile is fully below
   the fold, so the cascade plays in both directions.

   Scrubbed like the copy, and written straight onto the card: see the note by
   SCRUBBED in refresh.js for why it is opacity and transform rather than a
   custom property. Three earlier attempts went through a custom property and
   each one cost the About page most of its frame rate.

   What makes a row read one card at a time is --fm-o, each card's place in it,
   subtracted from the band's scroll progress. It is a position and not a delay,
   which is the point: a delay only runs forwards, and these have to come apart
   on the way back up as well.

   28px, further than the 12 it used to travel: next to masked copy rising a
   whole line, a 12px drift did not read as the same gesture. The scale came off
   with it, a scaled card re-rasterises its text every frame. */
[data-reveal-child] {
  opacity: 0 !important;
  transform: translateY(28px);
  transition:
    opacity 0.85s cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 0.85s cubic-bezier(0.22, 0.61, 0.36, 1);
  transition-delay: var(--fm-stagger, 0ms);
}

[data-reveal-child].fm-in {
  opacity: 1 !important;
  transform: none;
}

/* Copy is scrubbed, not played: refresh.js writes --fm-p, how far this element
   has arrived given where it sits in the viewport, and the arrival is read
   straight off it. No transition, deliberately: a transition on a value that is
   already updated per frame lags the scroll it belongs to, which is the same
   reason the parallax drift below carries none either.

   --fm-o offsets the element by its place in the section's reading order, so the
   eyebrow leads, the heading follows, the paragraph under it comes last. /0.7
   means each one is fully arrived by 70% of its own window, leaving the rest of
   the travel to the copy behind it. */
[data-reveal-text],
[data-reveal-kick] {
  --fm-tp: clamp(0, calc((var(--fm-p, 0) - var(--fm-o, 0)) / 0.7), 1);
  opacity: var(--fm-tp) !important;
  transform: translateY(calc((1 - var(--fm-tp)) * 26px));
}

/* Images: the Ken Burns settle. The tagged element is the picture itself,
   inside the export's own overflow:hidden frame, so a zoomed start spills
   nowhere, it settles into the frame as it fades in. transform + opacity
   only: unlike the clip-path aperture this replaces, it never repaints,
   which is the difference between gliding and stuttering on a page with
   twelve of them. */
[data-reveal-img] {
  opacity: 0 !important;
  transform: scale(1.12);
  transition:
    opacity 0.9s cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 1.3s cubic-bezier(0.22, 0.61, 0.36, 1);
}

[data-reveal-img].fm-in {
  opacity: 1 !important;
  transform: none;
}

/* The eyebrow above a heading is scrubbed with it, in the rule above: it leads
   the heading by its --fm-o and takes no performance of its own, it is the plate
   the heading arrives on. */

/* The scrubbed depth-zoom that lived here is gone with the aperture: the
   played settle above covers it, and one zoom per image is enough. */

/* Feature photos in a tall frame are *uncovered* instead of faded (2026-08-11,
   after Belgrade Arbor): the frame carries a curtain in the page's own
   off-white which retracts to the right edge. scaleX rather than a slide, so
   the curtain can never leave the frame and no `overflow:hidden` is assumed, and, like everything else here, it is one composited property. */
[data-reveal-curtain]::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background: var(--off-white, #F7F4EF);
  transform: scaleX(1);
  transform-origin: 100% 50%;
  transition: transform 1.05s cubic-bezier(0.22, 0.61, 0.36, 1);
}

[data-reveal-curtain].fm-in::after {
  transform: scaleX(0);
}

/* The photo drifts inside that frame as the page moves, refresh.js writes
   --fm-py. The standing 1.12 scale is what buys the travel: at inset:0 the
   layer is exactly its frame, and 34px of movement would otherwise open a
   34px gap. No transition: the value is already updated per frame, and easing
   it would lag the scroll it belongs to. */
[data-parallax] {
  transform: scale(1.12) translateY(var(--fm-py, 0px));
  will-change: transform;
  /* No easing on the drift, and !important because the value it has to beat is
     an inline one (2026-09-02).
     
     The photo frames carry `transition: transform .7s` inline, from the pass
     that gave every image the same hover zoom. --fm-py is rewritten every frame,
     so that transition restarted every frame and never arrived: the computed
     transform sat at identity and the picture juddered instead of drifting.
     Reported as the Leading the Vision band getting stuck.
     
     Nothing is lost by removing it here. These layers carry no hover transform
     of their own, so the only thing the transition was easing was the drift,
     which is already smooth by virtue of being per frame. */
  transition: none !important;
}

/* Headings are uncovered rather than faded: each word starts a full line below
   its place and rises into a box that clips it. The mask is the whole trick, and
   it is why this reads as editorial rather than as a fade.

   `overflow: clip` with a clip margin, falling back to `hidden`, so a descender
   ("g", "y") is not shaved off a settled word while the one still to come, a
   whole line-height away, stays hidden.

   `vertical-align: top` because a clipped inline-block takes its baseline from
   its bottom edge rather than from its text: left on `baseline` every word would
   sit a third of an em high in its line box.

   The travel is a percentage of the word's own height, never a fixed px, so a
   resize cannot leave anything clipped or short. ponytail: the offsets are baked
   from the line grouping at split time, so a resize that re-wraps the text can
   ripple in a slightly odd order until the next re-render re-splits it. */
[data-reveal-words] .fm-mask {
  display: inline-block;
  vertical-align: top;
  overflow: hidden;
  overflow: clip;
  overflow-clip-margin: 0.18em;
}

/* Two offsets, added rather than one chosen: --fm-o is where the heading sits in
   its section's reading order, --fm-lo is where the word sits inside the
   heading (its line, then its place along that line). Taking only the second
   would start every heading at the same instant however far down the page it
   was; taking only the first would land the whole statement at once. */
[data-reveal-words] .fm-mask-i {
  /* 0.42 is what each word takes to travel, and it is the last term of a budget
     that has to add to 1, or the last word of a heading would still be rising
     when the scrub has nothing left to give: 0.13 reading order (--fm-o) + 0.45
     spread across the heading (--fm-lo) + 0.42 = 1. */
  --fm-wp: clamp(0, calc((var(--fm-p, 0) - var(--fm-o, 0) - var(--fm-lo, 0)) / 0.42), 1);
  display: inline-block;
  opacity: var(--fm-wp);
  transform: translateY(calc((1 - var(--fm-wp)) * 110%));
}

/* The header stack gets out of the way on the way down and returns on the
   smallest scroll up (refresh.js toggles the body class past 80% of the first
   screen). Transform only, the stack stays fixed, so nothing below it moves. */
[data-fm-nav] {
  transition: transform 0.5s cubic-bezier(0.22, 0.61, 0.36, 1);
}

body.fm-nav-off [data-fm-nav] {
  transform: translateY(-100%);
}

/* The hero never sits still: a slow push in and back out, so the first screen
   is alive before the reader has scrolled at all. 24s and 4%, at that rate no
   single frame reads as movement, only the sense that the shot is breathing.
   Safe to scale: the section is `overflow:hidden` at 100vh. */
@media (prefers-reduced-motion: no-preference) {
  section[data-screen-label="Home hero"] video {
    animation: fm-hero-breathe 24s cubic-bezier(0.4, 0, 0.2, 1) infinite alternate;
  }

  @keyframes fm-hero-breathe {
    from {
      transform: scale(1);
    }
    to {
      transform: scale(1.04);
    }
  }
}

/* Scroll focus. `scroll-snap-type: y proximity` was the first attempt at the
   "lock the viewport to the active section" request and it had to come out:
   measured, it was the single worst thing on the page, a 1033ms frame during
   scroll against 233ms with it disabled, and it is what made the page feel like
   it was catching. Snap points on sections taller than the viewport fight every
   scroll gesture inside them, and the browser re-runs snap selection on each
   one.

   `scroll-padding-top` stays. It costs nothing and it is what stops the fixed
   header swallowing an in-page anchor target (the hero's promotions link).

   `scroll-behavior: auto` overrides the export's `html{scroll-behavior:smooth}`:
   the inertial glide in refresh.js is the easing now, and browser smoothing on
   top of it made every glide write animate on its own, which both doubled the
   float and tripped the glide's external-jump guard one frame in. */
html {
  scroll-padding-top: 150px;
  scroll-behavior: auto !important;
}

@media (prefers-reduced-motion: reduce) {
  [data-reveal],
  [data-reveal-child],
  [data-reveal-text],
  [data-reveal-kick],
  [data-reveal-words] .fm-mask-i {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    transition: none !important;
  }

  [data-reveal-img] {
    clip-path: none !important;
    opacity: 1 !important;
    transition: none !important;
  }
}

/* --- 4. Cursor -------------------------------------------------------------
   The Fairmount mark in silver with a gold star over the page, the ordinary
   cursor over anything you can use.

   Both variants are silver (2026-08-12). The first export shipped a cherry
   mark for the page and a gold one for the top stripe, and the colour change
   as the pointer crossed into the stripe read as a glitch, so they were
   unified by hand. The design has since caught up: the current export ships
   the silver mark itself, in two gradients that differ only in how bright the
   metal reads, the lighter one over the page, the darker over the stripe.

   The export puts its mark on `body, body a, body button, body [role="button"]`
   and a gold variant on the top stripe, so the branded cursor sat on the
   links and buttons too, and nothing on the page said "you can click this".
   The mark stays on the page at large (it is inherited from body, which is left
   alone); every interactive thing is handed back to the system cursor.

   !important throughout, because the export injects those rules from a <style>
   the runtime writes after this sheet, and a plain declaration would lose the
   tie no matter how specific.

   The silver star trail that used to follow the pointer was removed at the
   client's request, it lived in this section and in a sparkles() block in
   refresh.js, along with the MutationObserver exclusion that existed only to
   stop it re-running the tagging pass on every mouse move. */

/* The mark itself: the current export's two cursor SVGs byte for byte
   (16e259e3… over the page, 7889f1b9… over the gold stripe), at their 6,6
   hotspot, brushed-metal gradients on the swash and the star.

   Data URIs rather than edits to the bundle SVGs, because the bundle is
   re-unpacked wholesale on every design import. */
body {
  cursor: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'32'%20height%3D'32'%20viewBox%3D'55%20230%20195%20250'%3E%3Cdefs%3E%3ClinearGradient%20id%3D'fmSil'%20x1%3D'0'%20y1%3D'0'%20x2%3D'1'%20y2%3D'1'%3E%3Cstop%20offset%3D'0'%20stop-color%3D'%23FFFFFF'%3E%3C%2Fstop%3E%3Cstop%20offset%3D'.22'%20stop-color%3D'%23E6E8EA'%3E%3C%2Fstop%3E%3Cstop%20offset%3D'.5'%20stop-color%3D'%23B9BDC2'%3E%3C%2Fstop%3E%3Cstop%20offset%3D'.72'%20stop-color%3D'%238E949A'%3E%3C%2Fstop%3E%3Cstop%20offset%3D'1'%20stop-color%3D'%23D5D8DC'%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3ClinearGradient%20id%3D'fmSilB'%20x1%3D'0'%20y1%3D'1'%20x2%3D'1'%20y2%3D'0'%3E%3Cstop%20offset%3D'0'%20stop-color%3D'%239AA0A6'%3E%3C%2Fstop%3E%3Cstop%20offset%3D'.38'%20stop-color%3D'%23DFE2E5'%3E%3C%2Fstop%3E%3Cstop%20offset%3D'.62'%20stop-color%3D'%23A7ADB3'%3E%3C%2Fstop%3E%3Cstop%20offset%3D'1'%20stop-color%3D'%23EFF1F3'%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cg%3E%0A%3Cpolygon%20fill%3D'url(%23fmSil)'%20points%3D'199.35%20388.57%20193.99%20403.61%20176.73%20403.64%20191.08%20412.57%20185.43%20428.74%20199.49%20418.93%20213.18%20428.59%20207.92%20412.5%20222.57%20403.7%20204.79%20403.65%20199.35%20388.57'%3E%3C%2Fpolygon%3E%0A%3Cpath%20fill%3D'url(%23fmSilB)'%20d%3D'M93.98%2C458.88s13.87-12.11%2C40.01-16.58c0%2C0%2C9.72%2C1.28-18.49%2C21.84-39.19%2C27.79-65.57-10.92-21.68-30.92%2C0%2C0%2C20.72-9.72%2C56.9-17.06%2C0%2C0%2C15.78-4.94%2C19.29%2C4.46%2C0%2C0%2C3.98%2C11.32-13.71%2C13.87%2C0%2C0-56.95%2C8.02-62.32%2C24.39'%3E%3C%2Fpath%3E%0A%3Cpath%20fill%3D'url(%23fmSilB)'%20d%3D'M128.26%2C411.79c.18.85.35%2C1.7.53%2C2.55%2C0%2C0%2C.73%2C4.82%2C7.34%2C3.25%2C0%2C0%2C6.08-1.15%2C8.7-3.77%2C0%2C0%2C2.62-2.51-.73-13.52-16.08-46.29-32.33-92.23%2C19.38-122.42%2C0%2C0%2C15.03-9.3%2C33.17-8.67%2C0%2C0%2C18.13-.73%2C11.42%2C18.66%2C0%2C0-3.22%2C8.96-13.12%2C19.58-10.6%2C11.37%2C3.42%2C24.38%2C12.38%2C18.97%2C9.4-5.67%2C16.21-12.19%2C22.22-29.01%2C20.25-56.59-49.26-69.09-95.76-23.5-50.93%2C48.16-16.04%2C87.49-5.54%2C137.89'%3E%3C%2Fpath%3E%0A%3C%2Fg%3E%3C%2Fsvg%3E") 6 6, auto !important;
}

[data-cursor="gold"] {
  cursor: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'32'%20height%3D'32'%20viewBox%3D'55%20230%20195%20250'%3E%3Cdefs%3E%3ClinearGradient%20id%3D'fmSil'%20x1%3D'0'%20y1%3D'0'%20x2%3D'1'%20y2%3D'1'%3E%3Cstop%20offset%3D'0'%20stop-color%3D'%23E9EBED'%3E%3C%2Fstop%3E%3Cstop%20offset%3D'.22'%20stop-color%3D'%23E6E8EA'%3E%3C%2Fstop%3E%3Cstop%20offset%3D'.5'%20stop-color%3D'%23B9BDC2'%3E%3C%2Fstop%3E%3Cstop%20offset%3D'.72'%20stop-color%3D'%238E949A'%3E%3C%2Fstop%3E%3Cstop%20offset%3D'1'%20stop-color%3D'%23D5D8DC'%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3ClinearGradient%20id%3D'fmSilB'%20x1%3D'0'%20y1%3D'1'%20x2%3D'1'%20y2%3D'0'%3E%3Cstop%20offset%3D'0'%20stop-color%3D'%239AA0A6'%3E%3C%2Fstop%3E%3Cstop%20offset%3D'.38'%20stop-color%3D'%23BFC4C9'%3E%3C%2Fstop%3E%3Cstop%20offset%3D'.62'%20stop-color%3D'%23A7ADB3'%3E%3C%2Fstop%3E%3Cstop%20offset%3D'1'%20stop-color%3D'%23C6CACE'%3E%3C%2Fstop%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cg%3E%0A%3Cpolygon%20fill%3D'url(%23fmSil)'%20points%3D'199.35%20388.57%20193.99%20403.61%20176.73%20403.64%20191.08%20412.57%20185.43%20428.74%20199.49%20418.93%20213.18%20428.59%20207.92%20412.5%20222.57%20403.7%20204.79%20403.65%20199.35%20388.57'%3E%3C%2Fpolygon%3E%0A%3Cpath%20fill%3D'url(%23fmSilB)'%20d%3D'M93.98%2C458.88s13.87-12.11%2C40.01-16.58c0%2C0%2C9.72%2C1.28-18.49%2C21.84-39.19%2C27.79-65.57-10.92-21.68-30.92%2C0%2C0%2C20.72-9.72%2C56.9-17.06%2C0%2C0%2C15.78-4.94%2C19.29%2C4.46%2C0%2C0%2C3.98%2C11.32-13.71%2C13.87%2C0%2C0-56.95%2C8.02-62.32%2C24.39'%3E%3C%2Fpath%3E%0A%3Cpath%20fill%3D'url(%23fmSilB)'%20d%3D'M128.26%2C411.79c.18.85.35%2C1.7.53%2C2.55%2C0%2C0%2C.73%2C4.82%2C7.34%2C3.25%2C0%2C0%2C6.08-1.15%2C8.7-3.77%2C0%2C0%2C2.62-2.51-.73-13.52-16.08-46.29-32.33-92.23%2C19.38-122.42%2C0%2C0%2C15.03-9.3%2C33.17-8.67%2C0%2C0%2C18.13-.73%2C11.42%2C18.66%2C0%2C0-3.22%2C8.96-13.12%2C19.58-10.6%2C11.37%2C3.42%2C24.38%2C12.38%2C18.97%2C9.4-5.67%2C16.21-12.19%2C22.22-29.01%2C20.25-56.59-49.26-69.09-95.76-23.5-50.93%2C48.16-16.04%2C87.49-5.54%2C137.89'%3E%3C%2Fpath%3E%0A%3C%2Fg%3E%3C%2Fsvg%3E") 6 6, auto !important;
}

/* A click target shows the ARROW, not the hand (asked for 2026-08-19). `default`
   rather than a data-URI copy of an arrow: it is the platform's own pointer, so
   it is pixel-correct on every OS and DPI, costs nothing to download, and cannot
   drift from what the user expects a cursor to look like.
   The page's own swash cursor still applies everywhere that is not clickable. */
body a,
body button,
body [role="button"],
body [sc-camel-on-click],
body [style*="cursor:pointer"],
body [style*="cursor: pointer"] {
  cursor: default !important;
}

/* Text entry is not a click target: it keeps the I-beam, or the field stops
   looking editable. `auto` rather than `text` so a disabled or readonly control
   still resolves to whatever the browser thinks is right for it. */
body input,
body textarea,
body select,
body [contenteditable] {
  cursor: auto !important;
}

body :disabled {
  cursor: not-allowed !important;
}

/* --- 5. Top stripe ---------------------------------------------------------
   After the "top stripe" patch in mix site.import the grid is
   socials | phone | Enquire. The mobile stack in responsive.css was written for
   phone | email | CTA, so both spans are re-placed here: phone takes the first
   line beside the CTA, the socials drop underneath. */
body [data-top-socials] a {
  /* Same disc as the footer's social row (2026-08-29): a soft grey fill and a
     white glyph — just kept at the top stripe's own size rather than the
     footer's 52px, and its icon stays the top stripe's own 15px too. */
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border-color: transparent;
  background: rgba(0, 0, 0, 0.2);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--white);
  transition:
    background 0.35s cubic-bezier(0.25, 0.1, 0.25, 1),
    color 0.35s cubic-bezier(0.25, 0.1, 0.25, 1),
    border-color 0.35s cubic-bezier(0.25, 0.1, 0.25, 1);
}

body [data-top-socials] a:hover {
  border-color: var(--burgundy);
  background: var(--burgundy);
}

body [data-top-socials] svg {
  width: 15px;
  height: 15px;
  color: var(--white);
}

@media (max-width: 860px) {
  /* phone, first line (it is span 2 now that the socials lead the row) */
  body [data-cursor="gold"] > div > span:nth-of-type(2) {
    grid-area: 1 / 1 !important;
    justify-content: flex-start !important;
    justify-self: start !important;
    font-size: 14px !important;
  }

  /* socials, second line */
  body [data-cursor="gold"] > div > span:nth-of-type(1) {
    grid-area: 2 / 1 !important;
    justify-self: start !important;
  }

  /* The two lines had nothing between them. The grid's `gap` is one value for
     rows and columns, and on a phone it resolves to the bottom of its clamp,
     which is the right amount between columns and too little once the phone
     and the socials are stacked. Rows get their own, columns are left alone
     (2026-09-04). */
  body [data-cursor="gold"] > div {
    row-gap: 10px !important;
  }

  body [data-top-socials] a {
    width: 26px;
    height: 26px;
  }

  body [data-top-socials] svg {
    width: 14px;
    height: 14px;
  }
}

/* --- 4a. Contact cards ------------------------------------------------------
   The gold-ringed icon badge above each heading goes (2026-08-18): three of them
   in a row drew more attention than the details underneath, which are the point
   of the card. The heading says "Visit Us" / "Call Us" / "Email Us", the badge
   was saying it a second time in pictures.

   With the badge gone the cards keep 40px of padding for a line or two of text,
   which left the Call card mostly empty (they stretch to the tallest). Tighter
   padding and a smaller gap; the address card still sets the height, and it now
   sets a shorter one. */
body .fm-s-25aee1 > .fm-s-cdf2cb {
  display: none !important;
}

body .fm-s-25aee1 {
  padding: 28px 26px !important;
  gap: 10px !important;
  /* Top-aligned, not centred (2026-08-18): the cards stretch to the tallest, so
     centring each one's content put "Visit Us", "Call Us" and "Email Us" on
     three different lines. */
  justify-content: flex-start !important;
}

/* --- 4c. The Enquire tab ----------------------------------------------------
   The design's tab is 14px at .24em tracking with 24px of padding either side,
   which runs about 200px down the right edge and reads as a third piece of
   furniture beside the two launcher discs. Trimmed to a marker rather than a
   banner (2026-08-17); responsive.css already had its own, smaller figures for
   phones and those still win below 720px. */
body [data-edge-tab] {
  font-size: 11px !important;
  letter-spacing: 0.16em !important;
  padding: 15px 8px !important;
  border-radius: 8px 0 0 8px !important;
}

/* --- 5a. The nav's underline ------------------------------------------------
   The export drew two 2px burgundy bars: one under the current page, always,
   and another wiped in on hover. Beside a logo carrying its own hairline the
   nav was the louder of the two, and the point was for the mark to be what the
   eye finds first.

   Both are gone (2026-08-14). Nothing is underlined at rest, the current page
   is marked by its label being burgundy while the rest are grey, and the hover
   draws the logo's hairline instead of a bar. The static border is off in the
   nav mapping (`underline` in index.html); the rest is here. */
header nav a::after {
  /* The export's 2px bar, redrawn as the hairline under the logo: 1px, fading
     out at both ends rather than stopping dead. It is the only mark the nav
     makes, and only while the pointer is on a link, the export animates it in
     from scaleX(0), which this keeps. */
  height: 1px !important;
  bottom: -6px !important;
  background: linear-gradient(
    90deg,
    transparent,
    var(--burgundy),
    transparent
  ) !important;
  transform-origin: center !important;
}

/* The hover bar is drawn twice over: the export's `::after` above, and a
   `border-bottom` the runtime writes out from the link's `style-hover`. Only
   killing the first left the second, which is why the line kept coming back.
   The border stays in the box at `transparent` so nothing shifts by 2px as the
   pointer crosses. The selector is heavier than it looks because the runtime's
   own rule is `.scpN:hover { ... !important }`, a plain `nav a:hover` ties on
   !important and loses on specificity, so this adds the attribute the links all
   carry to outweigh it. */
body header nav a[style]:hover,
body header nav a[style]:focus {
  border-bottom-color: transparent !important;
}

/* --- 5b. The header logo stands alone ---------------------------------------
   There was a hairline under the header mark, the same flourish the footer
   logo sits on, in cherry rather than gold. Removed on 2026-08-14: with the
   nav's own underlines gone, it was the only rule left up there and it read as
   a stray border under the logo rather than as part of it.

   The footer logo keeps its rule; that one sits in a block of its own with room
   around it, which is what the flourish was drawn for. */
body nav div:has(> [data-logo-link])::after {
  content: none;
}

/* --- 6. Sticky Enquire, chat + WhatsApp stack ------------------------------
   The edge tab lives outside every page block, so it is already on all pages, it just has to out-rank the chat stack and stay put while sections snap. */
body [data-edge-tab] {
  position: fixed !important;
  z-index: 190 !important;
  /* Square, matching the Enquire button in the top stripe. */
  border-radius: 0 !important;
}

/* Nexprop's launcher is a bare 68px disc with no class of its own; it is
   matched on the inlined data-URI icon (the same hook responsive.css uses).
   Feedback: smaller, white mark on cherry red, pulsing in white/light grey. */
body > button:has(> img[src^="data:image/svg"]) {
  width: 52px !important;
  height: 52px !important;
  background: var(--burgundy) !important;
  /* NOT !important, deliberately. An !important declaration outranks a CSS
     animation, so marking the resting shadow important silently cancelled the
     pulse below, the keyframes ran and were overruled every frame. Without it
     the animation wins (animations outrank normal and inline declarations, so
     the widget's own inline shadow loses too) and the ring shows. */
  box-shadow: 0 10px 26px rgba(44, 44, 44, 0.28);
  animation: fm-chat-pulse 2.6s cubic-bezier(0.25, 0.1, 0.25, 1) infinite;
}

/* The launcher's icon is a data-URI SVG whose artboard is a full-bleed cherry
   square with the mark inset, so the image is the disc, not a badge sitting on
   it. At 30px inside a 52px button, 22px of that disc was plain button colour
   and the mark itself was tiny. Filling the button scales the mark by ~1.7x
   without touching the asset; the button already clips to a circle. */
body > button:has(> img[src^="data:image/svg"]) > img {
  width: 100% !important;
  height: 100% !important;
  object-fit: cover !important;
}

/* Two rings, half a beat apart, so it reads as a wave leaving the button rather
   than a single blink. Light grey rather than white: white is invisible against
   the off-white pages, which is where the launcher sits most of the time, and
   grey still reads against the dark hero. */
@keyframes fm-chat-pulse {
  0% {
    box-shadow:
      0 10px 26px rgba(44, 44, 44, 0.28),
      0 0 0 0 rgba(176, 178, 188, 0.85),
      0 0 0 0 rgba(176, 178, 188, 0.5);
  }
  55% {
    box-shadow:
      0 10px 26px rgba(44, 44, 44, 0.28),
      0 0 0 13px rgba(176, 178, 188, 0),
      0 0 0 7px rgba(176, 178, 188, 0.28);
  }
  100% {
    box-shadow:
      0 10px 26px rgba(44, 44, 44, 0.28),
      0 0 0 13px rgba(176, 178, 188, 0),
      0 0 0 22px rgba(176, 178, 188, 0);
  }
}

@media (prefers-reduced-motion: reduce) {
  body > button:has(> img[src^="data:image/svg"]) {
    animation: none;
  }
}

/* The corner stack, bottom up: chat launcher (24px), WhatsApp (88px), then the
   widget's greeting (152px) above both. The widget writes `bottom` and `right`
   inline, so moving its greeting takes !important.

   WhatsApp sits below the widget's own z-indices on purpose, when the chat
   panel opens it should cover this corner, not be punched through by a floating
   disc. It is injected by refresh.js so it survives a re-export. */
/* The whole corner, both discs and the caption above them, rides
   --fm-corner-lift, which refresh.js sets to however far the colophon reaches
   into this corner. The caption was left out at first and collided with the
   discs as they rose past it, which showed up on a phone, where it sits closest
   to them (2026-08-19). `translate` rather than `transform`: the widget
   animates its own transform on hover, and these two must not fight. */
#fm-whatsapp,
body > button[data-fm-chat-greeting],
body > button[aria-label="Open chat"]:not([data-fm-chat-greeting]),
body > button[aria-label="Close chat"]:not([data-fm-chat-greeting]) {
  translate: 0 calc(-1 * var(--fm-corner-lift, 0px));
  transition: translate 240ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
#fm-whatsapp {
  position: fixed;
  right: 24px;
  /* 170px left a 55px hole over the caption (2026-08-18). The caption is a
     stable 44px one-liner whose top sits 116px off the floor, so 128px puts the
     disc just above it. The caption moved out of the column later the same day
     (see its own rule below), so the two discs sit together again: the chat
     launcher owns 24-76px, this clears it by 8. */
  bottom: 84px;
  z-index: 999996;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: #25d366;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 10px 26px rgba(44, 44, 44, 0.28);
  transition: transform 0.35s cubic-bezier(0.25, 0.1, 0.25, 1);
}

#fm-whatsapp:hover {
  transform: translateY(-4px);
}

#fm-whatsapp svg {
  width: 29px;
  height: 29px;
  fill: #fff;
}

/* The widget's greeting. Both of its body-level buttons carry
   aria-label="Open chat", so they have to be told apart. This used to key on
   `:not(:has(img))`, the launcher held an icon, the message did not. That broke
   the moment chat-launcher.js landed on dev: it swaps the launcher's <img> for a
   masked <span>, so the launcher matched too and every rule below would have
   flattened it into text. chat-launcher.js stamps the message
   `data-fm-chat-greeting`, which is the hook it means rather than one inferred
   from what happens to be inside. It moves above both launchers, loses its white card,
   and arrives on a drift-in with a gold rule drawing itself in from the right, the box container the feedback asked to drop, replaced with motion. */
body > button[data-fm-chat-greeting] {
  /* Beside the launchers, not above them. Floating over the page it read as
     stray text that happened to land there; sitting level with the two discs
     and tied to them by a rule, it reads as their label. */
  /* Above the pair, one column down the right edge (2026-08-19). Beside them it
     needed ~200px of gutter it does not have on a narrow desktop, so it read as
     floating over the page. The discs own 24-136px; 144 clears the top one by
     8, the same air that separates them from each other. */
  right: 24px !important;
  bottom: 144px !important;
  /* Narrow on purpose. At 316px the prompt ran as two long lines back across
     the page and collided with whatever was beside it, on the F-Desk that was
     an article's body copy, with neither readable. At this width it breaks into
     four short right-aligned lines that stay inside the corner
     (2026-08-14). */
  max-width: 178px !important;
  padding: 12px 4px 12px 18px !important;
  border-radius: 0 !important;
  box-shadow: none !important;
  text-align: right !important;
  position: relative;

  /* Type on the page and nothing else: no panel, no border, no backdrop-filter.
     Asked for on 2026-08-12, after two versions that both added something, a tinted gradient over a `blur(7px)`, which read as a patch of the video
     having been rubbed out, and then a flat off-white label, which read as a box.

     With nothing behind the words, contrast has to come from the words, and no
     single colour works, because this is fixed and passes over the hero picture
     and over white page sections in one scroll. So the colour follows what is
     behind it: this is the default, for the plain sections below the fold, where
     burgundy on off-white needs no help at all. The hero case is the rule after
     this one. Everything stays on screen the whole way down, as it did before. */
  background: none !important;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
  border: 0 !important;
  text-shadow: none;
  transition: color 320ms linear, text-shadow 320ms linear;

  animation: fm-greeting-in 0.7s cubic-bezier(0.22, 0.61, 0.36, 1) 0.9s both;
}

/* Over the hero. Off-white on a soft dark shadow, because burgundy holds on
   neither a dark video frame nor a pale, busy one, and a dark shadow, unlike
   the white halo tried before it, separates the type from light clutter too.
   refresh.js adds .fm-hail-on while the first screen is in view. */
body.fm-hail-on > button[data-fm-chat-greeting] {
  text-shadow:
    0 1px 2px rgba(0, 0, 0, 0.72),
    0 0 10px rgba(0, 0, 0, 0.55),
    0 0 26px rgba(0, 0, 0, 0.4);
}

/* The rule is vertical now and sits on the launcher side, so the caption points
   at the buttons instead of underlining itself. */
body > button[data-fm-chat-greeting]::after {
  content: "";
  position: absolute;
  right: 0;
  top: 16%;
  bottom: 16%;
  width: 1px;
  background: linear-gradient(180deg, transparent, var(--gold), transparent);
  transform: scaleY(0);
  animation: fm-greeting-rule 0.8s cubic-bezier(0.22, 0.61, 0.36, 1) 1.5s both;
}

/* The widget renders two lines: its name, then the prompt. "FAIRMOUNT BUILDERS"
   goes (2026-08-14), the logo is already in the header and the footer, and in
   this corner it was one more thing competing for the same glance. Hidden here
   rather than blanked in the loader's `data-greeting-title`, because the widget
   needs a title to render the greeting at all. */
body > button[data-fm-chat-greeting] > div:first-child {
  display: none !important;
}

body > button[data-fm-chat-greeting] > div:last-child {
  font-family: var(--font-display) !important;
  font-size: 15px !important;
  line-height: 1.3 !important;
  letter-spacing: 0.02em !important;
  /* Burgundy below the fold, where the page behind it is plain and pale. */
  color: var(--burgundy) !important;
  transition: color 320ms linear;
}

/* ...and off-white over the hero picture, where burgundy disappears. Only this
   line changes: the eyebrow above it is gold, which carries on both. */
body.fm-hail-on > button[data-fm-chat-greeting] > div:last-child {
  color: var(--white, #fff) !important;
}

@keyframes fm-greeting-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes fm-greeting-rule {
  to {
    transform: scaleY(1);
  }
}

/* Below ~720px the caption cannot sit beside the launchers: it needs about
   356px of right-hand room and would end up over the page content it is
   supposed to sit clear of. It goes back above the stack there, keeping the
   tint and the fade, with the rule turned horizontal to match.

   The narrow tier also tightens the stack rather than growing it taller: at
   full launcher size the widget's own "Schedule a site visit" action was
   pushed under the edge tab and could not be clicked. */
@media (max-width: 720px) {
  /* The caption goes on a phone (2026-08-19). There is no room for it: the
     Enquire tab owns this edge, the two discs own the corner under it, and
     wherever it was put it landed on one of them or on the copy. The discs
     say what it said.

     Kept as `display: none` on the button itself rather than removed in the
     loader, because the widget needs its greeting to exist to render at all. */
  body > button[data-fm-chat-greeting] {
    display: none !important;
  }

  body > button[data-fm-chat-greeting]::after {
    top: auto;
    bottom: 0;
    left: 22px;
    right: 0;
    width: auto;
    height: 1px;
    background: linear-gradient(90deg, transparent, var(--gold));
    transform: scaleX(0);
    transform-origin: right;
    animation-name: fm-greeting-rule-h;
  }

  body > button[data-fm-chat-greeting] > div:last-child {
    font-size: 14px !important;
  }
}

@keyframes fm-greeting-rule-h {
  to {
    transform: scaleX(1);
  }
}

/* chat-launcher.css draws the greeting as a white speech bubble with a tail,
   to match the Mugdha widget. This site's greeting is type on the page with no
   box, so the tail has nothing to hang off, and that sheet loads after this
   one, so saying so takes a rule of its own rather than a tidier ordering.

   The bubble also parked at bottom:88px right:24px, which is where the WhatsApp
   launcher lives: it covered the disc completely, and the button looked gone.
   The caption sits beside the discs instead, so the stack is clear again. */
body > button[data-fm-chat-greeting]::after {
  display: none !important;
}

/* The give-way at the foot of the page: over "Powered by nexprop.ai" the two
   sets of words landed on each other and neither could be read. refresh.js adds
   .fm-hail-hide once the colophon reaches the caption's own band. Both launcher
   discs stay, so the chat is never out of reach. */
body.fm-hail-hide > button[data-fm-chat-greeting] {
  opacity: 0 !important;
  transform: translateY(8px) !important;
  pointer-events: none;
}

@media (max-width: 480px) {
  #fm-whatsapp {
    right: 18px;
    bottom: 96px;
  }
}

/* "Built on Purpose" cards: the three bands inside a card are subgrid rows, so
   they inherited the 32px gap that separates the cards themselves to 60px under
   the photo and 40px under the lead line. Spacing is the card's own padding
   now, and the same in every card. */
body div[style*="grid-template-rows:subgrid"],
body div[style*="grid-template-rows: subgrid"] {
  row-gap: 0 !important;
}

body div[style*="padding:clamp(28px,2.4vw,38px) clamp(28px,2.4vw,38px) 8px"],
body div[style*="padding: clamp(28px, 2.4vw, 38px) clamp(28px, 2.4vw, 38px) 8px"] {
  padding: 30px clamp(28px, 2.4vw, 38px) 14px !important;
}

body div[style*="grid-template-rows:subgrid"] h3,
body div[style*="grid-template-rows: subgrid"] h3 {
  margin-bottom: 10px !important;
}

/* Partner "How It Works" steps: each step heading sat 24px off its own
   sentence (the h3 + p rule in section 1 wins over the export's 10px) and each
   step carried 52px of tail padding before the next one. */
body div[style*="padding-bottom:clamp(36px,4vw,52px)"],
body div[style*="padding-bottom: clamp(36px, 4vw, 52px)"] {
  padding-bottom: 30px !important;
}

body div[style*="padding-bottom:clamp(36px,4vw,52px)"] h3,
body div[style*="padding-bottom: clamp(36px, 4vw, 52px)"] h3 {
  margin: 2px 0 8px !important;
}

/* --- 7. Footer -------------------------------------------------------------
   Everything a notch tighter, a gold rule under the logo, and every heading in
   dark grey, burgundy is the logo's alone down here (asked for 2026-08-17,
   restated 2026-08-18 against the deployed page). */
body footer {
  padding-top: 64px !important;
  padding-bottom: 56px !important;
}

body footer > div:first-child {
  margin-bottom: 0 !important;
  padding-bottom: 22px !important;
  position: relative;
}

/* The gold hairline under the footer mark goes (2026-08-18), same call as the
   one under the header logo. The logo stands on its own. */
body footer > div:first-child::after {
  content: none;
}

body footer > div:nth-child(2) {
  margin-top: 34px !important;
}

/* One line rhythm for every stack in the footer, links, address, phones,
   emails. The export gives each column its own gap *and* its own line-height,
   so a wrapped address line and the line under it sat on different beats. */
body footer [style*="flex-direction:column"],
body footer [style*="flex-direction: column"] {
  gap: 0 !important;
  line-height: 1.6 !important;
}

/* The three column headings share a class; "Follow Us" and "Email Us" are
   styled inline in the export, so their own margins are the hook. */
body footer .fm-s-57af4e,
body footer [style*="margin:56px 0 18px"],
body footer [style*="margin: 56px 0px 18px"],
body footer [style*="margin:32px 0 18px"],
body footer [style*="margin: 32px 0px 18px"] {
  color: var(--gray-dark) !important;
}

/* Each heading sits close to the lines it introduces, the export leaves
   18-22px under them, which reads as a break rather than a label. */
body footer .fm-s-57af4e {
  margin-bottom: 8px !important;
}

body footer [style*="margin:56px 0 18px"],
body footer [style*="margin: 56px 0px 18px"] {
  margin: 28px 0 8px !important;
}

body footer [style*="margin:32px 0 18px"],
body footer [style*="margin: 32px 0px 18px"] {
  margin: 22px 0 8px !important;
}

/* --- 8. Section-specific paint --------------------------------------------- */

/* Trusted Partners: each logo sits on a card again (2026-08-10). Flat, not the
   3D tile the design shipped, white face, one hairline border, square corners,
   no shadow, in keeping with section 2. The logos themselves are the flattened
   PNGs in partner_list/, so the card is drawn here rather than baked into the
   artwork, and an editor uploading a plain logo gets the same card for free.

   The marquee tile is the only 280x150 box in the export (it appears twice, the
   row is duplicated so the loop is seamless), so its own dimensions are the
   hook. */
body div[style*="width:280px"][style*="height:150px"],
body div[style*="width: 280px"][style*="height: 150px"] {
  box-shadow: none !important;
  border: 1px solid var(--light-gray) !important;
  background: var(--white) !important;
}

/* The size is desktop-only. responsive.css sizes these tiles from the viewport
   below 861px (30vw, so three fit a phone), and this file loads after it, so at
   equal specificity these fixed pixels were winning on a phone too, and two
   210px cards ran off a 390px screen (2026-08-19). The flat-card paint above
   stays global; only the measurements are scoped.

   Smaller cards, asked for 2026-08-10 to 280x150 down to 210x112, the same
   proportion, with the gap and inner padding scaled to match so the row still
   reads as evenly spaced rather than crowded. */
@media (min-width: 861px) {
  body div[style*="width:280px"][style*="height:150px"],
  body div[style*="width: 280px"][style*="height: 150px"] {
    width: 210px !important;
    height: 112px !important;
    padding: 16px !important;
    margin-right: 18px !important;
  }
}

/* The logo inside is a fixed 420x223 background box, larger than the card it
   sits in, so it has to be told to fit rather than left to overflow. `contain`
   is already on it, so filling the card scales the artwork down with it. */
body div[style*="width:280px"][style*="height:150px"] > div[role="img"],
body div[style*="width: 280px"][style*="height: 150px"] > div[role="img"] {
  width: 100% !important;
  height: 100% !important;
}

/* Signature Developments: the project photo gets the same inset white frame the
   "Built on Purpose" cards have (2026-08-10, "how the photos are having a
   border, i want this projects in the homepage like that only").

   Those cards get it from the `div:has(> [data-img-reveal])` rule below, which
   this card misses because its image wrapper is a <span>, not a <div>, hence
   the full-bleed photo. `.fm-s-719fbd` is that wrapper (aspect-ratio 4/3,
   overflow hidden) and is unique to this card in the export.

   Padding on the wrapper does NOT do it, which is why the photo was still
   running edge to edge. The image is `position:absolute; inset:0`, and an
   absolutely positioned box resolves against its containing block's *padding
   box*, so `inset:0` fills the padding as well and the gutter never appears.
   The inset has to be set on the image itself.

   14px on three sides, matching the framed images in the rule below; the bottom
   is left flush because the white plaque laps over it. */
body .fm-s-719fbd {
  background: var(--white) !important;
}

body .fm-s-719fbd > [data-img-reveal] {
  inset: 14px 14px 0 !important;
  border: 1px solid var(--light-gray) !important;
}

/* The plaque: the logo, name and location ride in a white block that overlaps
   the bottom of the photo, the same device the project detail page uses for its
   identity card (2026-08-10, "i need this card like this"). That one is
   `margin-top:-96px` against a full-bleed hero; scaled to a ~370px card it is
   -52px, which laps the photo by about a third of the plaque.

   It lifts itself out of the copy block's 24px top padding, so the negative
   margin is measured from the photo's edge, and z-index puts it over the image's
   own stacking context. Square and flat, like everything else, on the detail
   page this block carries a radius and a float shadow, and sections 2's blankets
   take both off here. */
body .fm-plaque {
  /* Trimmed 2026-08-10 ("that project logo card is big, reduce it somewhat"):
     the padding, the mark and the name's reserved height all came down, so the
     overlap follows them to keep the same two-thirds-on-the-photo proportion. */
  /* Full width, not inset. The 14px either side left two slivers of the
     photograph showing under the plaque, which read as a misaligned card rather
     than as a panel laid over the picture, and it put the name hard against the
     photo's edge with nothing between them (2026-08-13, "where is the space"). */
  /* The copy block around it is padded 24px, so the plaque was inset by that
     much and two slivers of the photograph showed underneath, it read as a
     misaligned card rather than a panel laid over the picture, and put the name
     hard against the photo's edge (2026-08-13, "where is the space"). The
     negative side margins cancel the parent's padding so it spans the card. */
  margin: -78px -24px 0 !important;
  padding: 12px 18px 4px !important;
  /* The plaque has to be at least as tall as it is pulled up, or the strip of
     photograph it was meant to cover shows below it as a hairline straight
     through the location (2026-08-18). It only bit on cards whose name fits one
     line: a logo (52px) or a two-line name clears 78px on its own, a single line
     comes to about 61px. A floor, not a fixed height, so taller plaques still
     grow. */
  min-height: 78px !important;
  box-sizing: border-box;
  position: relative;
  z-index: 2;
  /* the copy block is `align-items:center`, under which a block child shrinks to
     its content, the plaque came out as a narrow tab around the logo instead of
     a card. Stretch puts it across the photo, inset by its own margin. */
  align-self: stretch;
}

/* The copy block's own top padding would add to the plaque's overlap, so it
   goes, but only on a card that HAS a plaque. The cross-sell cards under
   "Explore Our Residential Communities" use the same card class with no plaque
   in them, and this took their 26px away too: the name sat flush against the
   bottom of the photograph with nothing between them (2026-08-18). */
body .fm-s-7334c5:has(.fm-plaque) > span:last-child {
  padding-top: 0 !important;
}

/* The tagline sat in ~26px of air on each side: the plaque's 10px bottom
   padding, the copy block's 6px flex gap and the link's own 6px top padding,
   all stacked (2026-08-18). One small gap does the job. */
body .fm-s-7334c5 > span:last-child {
  gap: 2px !important;
}


/* Inside the plaque only, the same name span on the Projects page keeps the
   design's sizing. The mark and logo come down from 26/44px, and the name stops
   reserving two lines of height, which was most of the plaque's bulk. */
body .fm-plaque > svg {
  width: 20px !important;
}

/* A fixed height, not a cap: most project logos are small files and rendered at
   their own size, well under the 32px ceiling, so they read as thumbnails on
   the plaque (2026-08-18). `object-fit` only matters for a logo wide enough to
   hit the max-width, it letterboxes rather than stretches. */
/* One fixed box every uploaded logo is fitted into (2026-08-18). A cap alone
   left small files at their own size and wide files letterboxed down to a
   sliver; a fixed 240x52 box with `contain` means each logo grows until it hits
   one edge of the box, so they all read at the same weight, whatever was
   uploaded and whatever its proportions. No cropping, no stretching, the only
   thing CSS can't fix is whitespace baked into the image itself. */
body .fm-plaque > img {
  width: 100% !important;
  max-width: 240px !important;
  height: 52px !important;
  max-height: none !important;
  object-fit: contain !important;
}

/* One line, and the same lettering on every card (2026-08-19: "the title should
   be in single line only, and everything should follow same font, not one
   different and another different").

   Both halves were the same cause: the name is whatever the admin typed. The
   display face is a small-caps design, so "Skyline Residences" came out as small
   caps while "KOKAPET ONE" came out as full caps, two different-looking titles
   from one rule. `text-transform: uppercase` settles it at the CSS end, so the
   cards match however the name is entered.

   The size comes down from 26px to 23 because that is what fits the longest name
   in one line inside the plaque ("FAIRMOUNT FORTUNE ONE", 21 characters, in a
   ~342px plaque at 1440). Following the viewport, so the fit holds as the card
   narrows. Ellipsis is the backstop for a name longer than any of today's, one
   line is the instruction, and a wrap is what it must not do.

   `display` is deliberately left alone: the export writes `display:{{ nameShow
   }}` inline and sets it to none on a card that shows a logo instead, forcing
   it to block here would put an empty name back on every one of those. */
body .fm-plaque > span[style*="min-height:2.3em"],
body .fm-plaque > span[style*="min-height: 2.3em"] {
  min-height: 0 !important;
  font-size: clamp(16px, 1.55vw, 23px) !important;
  letter-spacing: 0.08em !important;
  text-transform: uppercase !important;
  white-space: nowrap !important;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 2px;
}

/* The cross-sell cards have no plaque, so nothing covers the strip -----------
   Reported 2026-08-19 against "Explore Our Residential Communities": an empty
   white box sits between the photograph and the name.

   That strip is the difference between the 17/16 frame and the 4/3 picture
   inside it, shown whole and anchored to the top, on a project card the plaque
   laps over exactly that gap, which is what the taller frame was bought for.
   These cards (and the residential cross-sell on Projects) have no plaque, so
   the frame is simply the shape of the picture: 4/3, which is what every project
   render is.

   `:not(:has(.fm-plaque))` rather than a new class, for the same reason the
   padding rule above uses it, the two card variants share `fm-s-7334c5` and
   differ only by whether a plaque is in them. */
body .fm-s-7334c5:not(:has(.fm-plaque)) .fm-s-719fbd {
  aspect-ratio: 4 / 3 !important;
}

/* ...and that card's name gets the one-line, uppercase treatment the plaque name
   has, it is the same title in the same card, and "Shree Ram Garden 2" was
   wrapping. First child of the copy block, which on a plaque card is the plaque
   itself, so the two never cross. */
body .fm-s-7334c5:not(:has(.fm-plaque)) > span:last-child > span:first-child {
  font-size: clamp(16px, 1.55vw, 23px) !important;
  letter-spacing: 0.08em !important;
  text-transform: uppercase !important;
  white-space: nowrap !important;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

/* ...and the asset class under it reads as part of the same stack. It was the
   one line on the card left in mixed case, which this face renders as small
   caps, beside two tracked uppercase lines it looked like a different font
   altogether. Same treatment, one step down in size: name, location, class,
   link now step through one scale. Keyed on the card class, so both the home
   and the Projects variant are covered. */
body .fm-s-7334c5 [class*="whitespace-pre-line"] {
  font-size: 14px !important;
  letter-spacing: 0.14em !important;
  text-transform: uppercase !important;
}

/* The tagline's one-line reserve (min-h-[1.8em], what keeps every card's link
   on the same baseline) and its leading are utilities on the span itself now, see the card patches at the foot of lib/mix/tasks/site.import.ex. */

/* News & Media and F-Desk: images framed, not full bleed. The reveal wrapper is
   the image itself, so the frame goes on its parent. */
body [data-img-reveal] {
  border-radius: 6px;
}

body [data-img-reveal]:not(:only-child),
body div.fm-p-img {
  padding: 14px 14px 0;
  background: var(--white);
}

body div.fm-p-img > [data-img-reveal] {
  border: 1px solid var(--light-gray);
  box-shadow: 0 1px 0 rgba(44, 44, 44, 0.04);
}

/* the blog lead story frames on all four sides, it sits beside the copy, not
   above it, so there is no card padding underneath to borrow */
body div.fm-p-img[style*="min-height:340px"],
body div.fm-p-img[style*="min-height: 340px"] {
  padding: 14px !important;
}

/* About: Vision and Mission lose their icon medallions. The 65x65 disc is the
   only one of its size in the export (How It Works on the Channel Partner page
   uses 63x65 and keeps its icons), so hiding its column and collapsing the
   96px track needs no markup change. */
body div.fm-p-disc {
  display: none !important;
}

body div.fm-p-grid96 {
  grid-template-columns: 1fr !important;
  gap: 0 !important;
}

/* Channel Partner "Why Partner With Us" cards: the Fairmount mark comes off the
   tile, and the headings drop from cherry red to grey. Both inline styles are
   unique to that card in the export. */
body img[style*="width:22px; height:auto; margin-bottom:18px"],
body img[style*="width: 22px; height: auto; margin-bottom: 18px"] {
  display: none !important;
}

body div[style*="font-size:20px; letter-spacing:.08em; color:var(--burgundy); margin-bottom:12px"],
body div[style*="font-size: 20px; letter-spacing: 0.08em"][style*="margin-bottom: 12px"] {
  color: var(--gray-dark) !important;
}

/* --- 9. The body scroll container ------------------------------------------
   `overflow-x: hidden` with `overflow-y: visible` computes the y axis to
   `auto`, which turns <body> into a scroll container that never scrolls, and
   any position:sticky inside it then has no scrollport to stick to. The export
   ships `html,body{overflow-x:hidden}`, so body has to be walked back
   explicitly; on the root element the same declaration propagates to the
   viewport instead and clips sideways overflow without the side effect.

   This was a mobile-only rule in responsive.css, which is exactly why the
   pinned milestone deck worked on a phone and did nothing on desktop. */
html {
  overflow-x: hidden;
}

body {
  overflow-x: visible !important;
}

/* --- 11. Intro (removed) ----------------------------------------------------
   The site used to open on a curtain: the Fairmount mark flying out of the
   distance, two panels parting to reveal the hero. Taken out on 2026-08-14, the client did not want it. The page paints straight into the hero now.

   The curtain over *navigation* is a different thing and is still there
   (section 13): that one covers a screen swap the SPA would otherwise do
   instantly, which is a transition rather than a toll gate at the front door. */

/* --- 12. Inner-page top padding --------------------------------------------
   Every page but Home opens with a top padding sized to clear a fixed header:
   240px on the standard pages, 150-170px on the three detail screens. That was
   right when the export shipped, because the export's header is
   `position:fixed` and every section started at y=0 underneath it.

   It has not been right since 2026-07-28, when the header became
   `position:sticky` so the hero media would start below the navbar rather than
   behind it (see "Header must not sit on top of the hero" in responsive.css).
   A sticky header keeps its space in normal flow, so the clearance is no longer
   clearing anything, it is a screen of blank off-white above every headline.

   One fluid value for all of them: 40px on a phone (what the old ≤1100px rule
   used) rising to 80px on a desktop, which is about the air the 240px was
   meant to leave under a 145px header. Bottom padding is untouched.

   The selectors are wide because the same padding arrives four ways, a
   generated class, an inline style on a <section>, an inline style on a screen
   wrapper <div>, and a lone padding-top, and inline styles need !important to
   override. Both spellings throughout: the dc-runtime re-serialises style
   attributes through the CSSOM, which normalises them with spaces. */
body .fm-s-255d50,
body .fm-s-804702,
body section[style*="padding: 240px"],
body section[style*="padding:240px"],
body div[style*="padding: 240px"],
body div[style*="padding:240px"],
body div[style*="padding: 150px"],
body div[style*="padding:150px"],
body div[style*="padding: 170px"],
body div[style*="padding:170px"],
body div[style*="padding-top: 150px"],
body div[style*="padding-top:150px"] {
  padding-top: clamp(40px, 5vw, 80px) !important;
}

/* --- 13. Page transition curtain, removed 2026-08-18 ------------------------
   The five-slat wipe that covered every navigation is gone; page changes are an
   instant swap again. refresh.js still hooks the navigation write, but only to
   stop a smooth-scroll glide running into the runtime's scroll-to-top. */

/* --- 14. Craft: hover, magnetism, depth --------------------------------------
   Ported from the design project (claude.ai/design, "Fairmount Website.dc.html"),
   which names these "Signature 3: hover craft", "Magnetic CTAs" and "Depth
   layer: cards respond in 3D". Selectors are the design's own, so they bind to
   the export's markup without a patch.

   Two deliberate departures from the source:

     * no hover shadow. The design lifts a card with `box-shadow:var(--shadow-float)`;
       the client asked for flat surfaces on 2026-08-09 and section 2 empties
       those tokens, so the lift is carried by movement and the gold rule alone.
     * transform and opacity only. Nothing here can force a repaint, the
       smoothness pass on 2026-08-09 is the reason that rule exists.

   Everything is pointer-fine and reduced-motion aware: on touch there is no
   hover to craft, and a reader who asked for less motion gets none. */
@media (hover: hover) and (pointer: fine) {
  /* Cards: a gold hairline draws across the top edge and the frame lifts. The
     match is on the export's inline `box-shadow:var(--shadow-card)`, that text
     is still in the markup even though the token is now empty, which is what
     makes it a reliable hook for "this is a card". */
  [data-craft-card] {
    position: relative;
    transition: transform 520ms cubic-bezier(0.22, 0.61, 0.36, 1);
  }

  [data-craft-card]::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: 2px;
    z-index: 2;
    background: var(--gold);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform 620ms cubic-bezier(0.22, 0.61, 0.36, 1);
  }

  [data-craft-card]:hover::after {
    transform: scaleX(1);
  }

  [data-craft-card]:hover {
    transform: translateY(-6px);
  }

  /* The design also breathes the photograph on hover (scale 1.055). Not ported:
     that layer already carries [data-parallax], whose transform is its scale
     plus the scroll drift, and a hover rule on the same element replaces the
     whole shorthand, the picture would shrink and stop drifting the moment the
     pointer touched it. One element, one treatment. The card's lift and the
     gold rule carry the hover on their own. */

  /* Solid burgundy buttons fill with burgundy-deep from the left rather than
     swapping colour. Outline and light buttons keep their own treatment, so
     their text never loses contrast mid-transition. */
  [data-pill-solid] {
    position: relative;
    overflow: hidden;
    isolation: isolate;
  }

  [data-pill-solid]::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: var(--burgundy-deep, #6d0027);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform 480ms cubic-bezier(0.22, 0.61, 0.36, 1);
  }

  [data-pill-solid]:hover::before {
    transform: scaleX(1);
  }

  /* The nav underline retreats to the right on exit instead of snapping back. */
  header nav a:not(:hover)::after {
    transform-origin: right;
  }

  /* Magnetism and depth. The transform itself is written by refresh.js; these
     rules only declare how it settles when the pointer leaves. */
  /* , not : several of these already carry a transform
     for their own positioning (the edge tab is translateY(-50%)), and writing
     transform from script would throw that away. The independent property
     composes with it instead. */
  [data-mag] {
    will-change: translate;
    transition: translate 420ms cubic-bezier(0.22, 0.61, 0.36, 1);
  }

  [data-tilt-wrap] {
    perspective: 1200px;
  }

  [data-tilt] {
    transform-style: preserve-3d;
    transition: transform 480ms cubic-bezier(0.22, 0.61, 0.36, 1);
  }
}

@media (prefers-reduced-motion: reduce) {
  [data-mag],
  [data-tilt] {
    translate: none !important;
    transform: none !important;
    transition: none !important;
  }
}

/* --- 15. Craft, second pass -------------------------------------------------
   Two more effects from the same design project, recovered from the offline
   export where the stylesheet ships inside the script payload rather than in
   <style>: "Leadership portraits" and the scroll cue under the first screen.

   The rest of that file's motion is not portable and was left alone on purpose:
   the rotating hero, the pinned horizontal gallery, the scroll-expanding frame,
   the marquee band, the full-screen panel stack and the cinematic chapter layer
   are all layout, and the brief was animations and effects only. The design's
   own intro and burgundy page sweep are also skipped, this build already has
   an intro and a page curtain the client signed off on, and running two of
   either would read as a stutter, not a flourish. */

/* Leadership portraits. The export gives no class to hold on to, so the card is
   found by shape in refresh.js: a zoomable photo, its frame, and a name block.
   The design's gold rule and rotating star are dropped, there are no such
   elements in our markup, and inventing them would be content, not motion. */
@media (hover: hover) and (pointer: fine) {
  [data-leader] {
    transition: transform 700ms cubic-bezier(0.25, 0.1, 0.25, 1);
  }

  [data-leader]:hover {
    transform: translateY(-5px);
  }

  /* `scale`, not `transform`: the photo may already be a parallax layer, whose
     transform is its standing 1.12 plus the scroll drift. The independent
     property multiplies into that instead of replacing it. */
  [data-lead-img] {
    transition: scale 700ms cubic-bezier(0.25, 0.1, 0.25, 1);
  }

  [data-leader]:hover [data-lead-img] {
    scale: 1.05;
  }

  /* The burgundy veil the design faded up over the portrait on hover was taken
     out on 2026-09-02, asked for: over a black-and-white photograph a 42%
     burgundy wash reads as the picture turning pink rather than as a highlight,
     and it was the first thing anyone noticed about the band. The portrait stays
     black and white; the lift and the 1.05 zoom carry the hover on their own.

     [data-lead-frame] keeps `position: relative` because the frame still
     positions the picture inside it. */
  [data-lead-frame] {
    position: relative;
  }

  /* The picture pushes in with the card. This is the part of the Signature
     Developments hover that the "Built on Purpose" and "Vision, Mission & Ethos"
     cards were missing: the export writes `style-hover="transform:scale(1.07)"`
     directly onto the photo inside a *link* card, and those two sections are
     plain divs, so the runtime never applied it. Same curve and same 1.07 as the
     export uses, so the two read as one effect.

     Parallax layers are excluded. That element's transform is already its
     standing 1.12 scale plus the per-frame scroll drift, and a hover rule on the
     same property replaces the whole shorthand, the photo would shrink and stop
     drifting the moment the pointer touched it. Those cards get the zoom from the
     export's own style-hover anyway, so nothing is lost. */
  [data-craft-card] [data-img-reveal]:not([data-parallax]) {
    transition: transform 1.4s cubic-bezier(0.25, 0.1, 0.25, 1);
  }

  [data-craft-card]:hover [data-img-reveal]:not([data-parallax]) {
    transform: scale(1.07);
  }
}

/* The design's Ken Burns push is deliberately not here. It was built, measured
   and removed: riding it on the curtain reveal cost 16ms of scroll p95 on the
   home page (50ms with it, 34ms without, three runs each). Animating scale on a
   full-bleed background-image layer re-rasters the layer every frame, and those
   same layers are already writing --fm-py per frame for the parallax drift. An
   8% push nobody can point to is not worth a third of the frame budget on the
   page the client asked to be smooth. */

@media (prefers-reduced-motion: reduce) {
  [data-lead-img] {
    scale: none !important;
  }
}

/* --- 10. About "Our Journey": the roadmap ----------------------------------
   Four earlier attempts, none of which should come back: a pinned deck that
   stacked every milestone under the header; a snaking lane grid that needed
   four narrow columns and 2,400px of mostly-empty section; a straight stepper
   rail, compact but flat; and the same road horizontally, which only worked
   because the projects were hidden until you hovered a pin.

   This is the road running down the page, generated from the milestone list
   (see journeyList/journeyPath): pins alternate left and right of centre and
   the project sits opposite each one, always visible. Nothing is hidden behind
   a hover, so it reads on a phone and in a printout the same as on a desktop.

   The map's height is set from the number of milestones to 190px a stop, so the
   spacing stays even whether the admin holds six or sixteen. */
[data-ms-map] {
  max-width: 1100px;
}

/* Stacking, stated rather than left to document order: the road is scenery and
   belongs under everything. Without this the dashes ran across the face of
   every card. */
[data-ms-map] > svg {
  z-index: 0;
}

[data-ms-card] {
  z-index: 1;
  /* the export's sheet is content-box, so the 26px padding was being added to
     the width and both columns hung 24px off the map */
  box-sizing: border-box;
  transform: translateY(-50%);
}

[data-ms-pin] {
  z-index: 2;
  box-shadow: 0 0 0 8px var(--white);
}

/* The reveal (tagged in refresh.js) animates `transform`, which is also what
   centres the card on its pin, so the seated state has to put the centring
   back, or every card drops half its height as it arrives. */
[data-ms-card][data-reveal] {
  transform: translateY(calc(-50% + 36px));
}

[data-ms-card][data-reveal="l"] {
  transform: translate(-44px, calc(-50% + 20px));
}

[data-ms-card][data-reveal="r"] {
  transform: translate(44px, calc(-50% + 20px));
}

[data-ms-card][data-reveal].fm-in {
  transform: translateY(-50%) !important;
}

/* Each card reaches into the empty half of the page opposite its pin. */
[data-ms-stop][data-ms-side="right"] [data-ms-card] {
  left: 74px;
}

[data-ms-stop][data-ms-side="left"] [data-ms-card] {
  right: 74px;
}

/* A phone has no room either side of a road, so the journey turns on its side:
   the road runs down the left margin, each year sits on it, and the project is
   beside it. The first version of this stacked pin-over-card full width, which
   worked but cost 3,437px, more than a third of the About page, and read as
   ten unrelated cards rather than one road.

   The SVG goes: its path is drawn for a two-column map 1,100 units wide, and
   there is nothing sensible to scale that to at 390px. The road here is a
   border on the pin column, which needs no geometry. */
@media (max-width: 860px) {
  [data-ms-map] {
    height: auto !important;
    display: flex;
    flex-direction: column;
    gap: 0;
  }

  [data-ms-map] > svg {
    display: none;
  }

  [data-ms-stop] {
    position: static !important;
    width: 100% !important;
    height: auto !important;
    display: grid;
    grid-template-columns: 64px 1fr;
    align-items: start;
    column-gap: 14px;
    padding-bottom: 22px;
  }

  /* the road: a dashed rule down the pin column, drawn behind the years and
     stopping under the last one */
  [data-ms-stop]::before {
    content: "";
    grid-column: 1;
    grid-row: 1;
    justify-self: center;
    width: 2px;
    height: 100%;
    background: repeating-linear-gradient(
      180deg,
      var(--gold) 0 10px,
      transparent 10px 20px
    );
  }

  [data-ms-stop]:last-child::before {
    height: 30px;
  }

  [data-ms-pin] {
    position: static !important;
    grid-column: 1;
    grid-row: 1;
    margin: 0 !important;
    width: 56px !important;
    height: 56px !important;
    font-size: 13px !important;
    box-shadow: 0 0 0 5px var(--white);
  }

  [data-ms-card] {
    position: static !important;
    grid-column: 2;
    grid-row: 1;
    width: 100% !important;
    transform: none !important;
    padding: 14px 16px 16px !important;
  }

  [data-ms-card] > div:first-child {
    font-size: 17px !important;
    margin-bottom: 6px !important;
  }

  [data-ms-card] > div:nth-child(2) {
    font-size: 11.5px !important;
    letter-spacing: 0.16em !important;
    margin-bottom: 7px !important;
  }

  [data-ms-card] p {
    font-size: 13.5px !important;
    line-height: 1.55 !important;
  }

  /* The cards arrive by fading only. Every reveal here also lifts its element
     24px and scales it 4%, which on a stack this tight reads as uneven spacing
     rather than as motion: mid-scroll, a card that has arrived and one that has
     not sit at different offsets, so the rhythm between them keeps changing.
     The fade still marks their arrival; the spacing stops moving. */
  [data-ms-card],
  [data-ms-pin] {
    transform: none !important;
  }
}

/* --- The greeting gives way to the colophon --------------------------------
   The greeting has no panel behind it, so at the very bottom of the page its
   words landed on top of "Powered by nexprop.ai" and neither set could be read.
   refresh.js adds .fm-hail-hide once that strip reaches the greeting's own band.

   Only the words go. Both launcher discs stay where they are, so the chat is
   never out of reach, which is why this hides rather than moves: anywhere else
   in that corner is already spoken for. */
body.fm-hail-hide > button[data-fm-chat-greeting] {
  /* !important because the greeting's entrance keyframes carry `both`, and an
     animation's filled value beats a plain declaration, without it the class
     applied and nothing moved. */
  opacity: 0 !important;
  transform: translateY(8px) !important;
  pointer-events: none;
}

/* the colour transition above is the one that was here; these two are the
   give-way, and they need naming or the greeting snaps out */
body > button[data-fm-chat-greeting] {
  transition:
    color 320ms linear,
    text-shadow 320ms linear,
    opacity 260ms cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 260ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* --- Short screens: the caption stands down --------------------------------
   Reported from a 14" laptop (2026-08-13): the caption sat on top of the blog
   cards' body copy and neither could be read. It is fixed to the bottom-right
   corner, so on a screen 700-800px tall it lands inside the content column
   rather than below it, on a 15.6" screen the same corner is empty, which is
   why this never showed here.

   Height, not width: the collision is vertical. Both launcher discs stay, so
   the chat is never out of reach, only the words go.

   WITHDRAWN 2026-08-19: the caption is to be visible on every screen, small or
   large. The collision it was guarding against is handled by `.fm-hail-hide`
   above, which stands the caption down when the colophon reaches its band, that is a content-aware guard, where this was a blanket one that took the
   words off every laptop. */

/* --- Project cards: the photo needs room to breathe ------------------------
   Reported 2026-08-13, on the site as well as on a phone: the name sits flush
   against the bottom of the photograph on the residential cards while every
   other card has a gap. The plaque is pulled up over the picture by a negative
   margin, and its own top padding is what stands in for the gap to 12px against
   a 26px name is not enough to read as one.

   Both card variants take the same figure, so the two sections match. */
body .fm-plaque {
  padding-top: 20px !important;
}


/* --- Headings: tighter tracking -------------------------------------------
   Reported 2026-08-18 against "Signature Developments": the display serif is
   tracked out to .06, .08em, which reads as gappy at 44px on mixed case.

   Scoped to the tags, not the folded `fm-s-*` classes, so it survives a design
   re-export (those hashes change; h1/h2/h3 do not). `!important` because the
   export writes letter-spacing inline on most of them.

   Uppercase headings keep their tracking, small-caps set at .03em closes up. */
body h1,
body h2,
body h3 {
  letter-spacing: 0.03em !important;
}

body h1[style*="uppercase"],
body h2[style*="uppercase"],
body h3[style*="uppercase"] {
  letter-spacing: 0.08em !important;
}

/* --- Headings: the leading is the gap ---------------------------------------
   Reported 2026-08-18 against "Creating Strong Foundations Since 2016": too
   much air between the heading and the paragraph under it.

   The stack is a flex column with `gap:18px`, so the gap is not the problem, the display serif has no line-height and falls back to `normal`, which for
   this face is ~1.5. That half-leading sits on top of the 18px as dead space.
   Setting the line box tight makes the rendered gap equal the declared one,
   which is the site-wide rule: one rhythm per stack, never gap + leading.

   `:not([style*="line-height"])` leaves alone every heading the export gives an
   explicit line-height (the hero, the project names), those are deliberate. */
body h1:not([style*="line-height"]),
body h2:not([style*="line-height"]),
body h3:not([style*="line-height"]) {
  line-height: 1.15 !important;
}

/* Our Journey teaser: still too much air after the leading fix above, so the
   declared gap comes down too (2026-08-18). One value for the whole stack, eyebrow, heading, copy, button, so the rhythm stays even.

   Matched on "760px" + "column" and NOT on "gap:18px": the runtime reserializes
   the style attribute with spaces ("gap: 18px"), so the unspaced form never
   matches in the browser even though it is what index.html says. */
body section > div[style*="760px"][style*="column"] {
  gap: 8px !important;
}

/* --- Projects: the unit finder card ----------------------------------------
   Reported 2026-08-18: "Find Your Space" sits too close under nothing and too
   far from its heading, and the search boxes need to stand away from the
   heading block.

   The eyebrow's 10px sat on top of the heading's leading, which reads as a
   double gap now that headings are set at 1.15; the tab pill's 36px is what
   separates the heading block from the selects.

   Keyed on "1060px" + "shadow-card" (the other 1060px container is the project
   grid) and on structure/`inline-flex` rather than on letter-spacing values,
   because the runtime reserializes `.24em` as `0.24em`. */
body div[style*="1060px"][style*="shadow-card"] > div:first-of-type {
  margin-bottom: 2px !important;
}

/* the Residential/Commercial pill needs to stand away from the heading, not sit
   on it, clarified 2026-08-18; its 36px below the pill was already right */
body div[style*="1060px"][style*="shadow-card"] > h2 {
  margin-bottom: 28px !important;
}

/* --- Social discs: one hover, everywhere -----------------------------------
   Two halves of the same report (2026-08-18).

   1. The top stripe's hover only drew a gold ring and set the glyph white, and
      the rule at the top of this file makes every disc's glyph white on hover, so on the light stripe the icon vanished into the background. It gets the
      footer's hover instead: the disc fills burgundy, white glyph on burgundy.

   2. The footer/Contact discs carried a ring at rest that the top stripe did
      not (both now carry the footer's filled disc instead, see "5. Top
      stripe" and "Footer socials" above), so this hover is shared as-is. */
body [data-top-socials] a:hover {
  border-color: var(--burgundy);
  background: var(--burgundy);
}

body .fm-s-76db03 {
  border-color: transparent !important;
}

/* Project cards: the "View project" link had `padding-top:6px` in the export
   and lost it when that style became Tailwind utilities (2026-08-18). One-line
   taglines hid it, a two-line one (Downtown) runs straight into the link.
   Matched as an attribute substring so the bracketed class needs no escaping,
   and set here rather than as a `pt-3` utility because the built daisy.css does
   not carry that class, adding it to the markup alone would do nothing. */
body [class*="mt-auto"][class*="tracking-[0.18em]"] {
  padding-top: 12px !important;
}

/* --- Insights + News cards: the artwork is not cropped ---------------------
   Reported 2026-08-19 against the Insights grid: the poster graphics lose their
   own headline off the left edge ("HYDERABAD'S Q1..." reads as "YDERABAD'S").

   The cause is a ratio mismatch, not a size one: the artwork is 16/9, the card's
   image box is 16/10, and `background-size: cover` fills the box by cropping the
   long side, about 10% off each end. Going two-up instead of three would make
   the cards bigger and crop them by exactly the same 10%, because cover crops on
   ratio alone. Matching the box to the artwork is what actually stops the cut.

   Only the two editorial grids: their pictures are designed graphics with type
   in them. The similar-projects card next door keeps 16/10, it holds a
   photograph, which crops harmlessly, and it carries `background` rather than
   `overflow`, which is how the two are told apart. The gallery strip is excluded
   by its scroll-snap. Both spellings, because the runtime reserializes the style
   attribute with spaces.

   Matched by class now (2026-09-02). The selectors below read the *inline*
   style, and a later export folded that declaration into a generated class, so
   they stopped matching and the cut came back on the F-Desk cards. refresh.js
   finds the box by its computed ratio instead, which no rename can break; the
   two inline spellings are kept underneath for the case where an export writes
   it inline again. */
.fm-p-wide,
body div[style*="aspect-ratio:16/10"][style*="overflow"]:not([style*="scroll-snap"]),
body div[style*="aspect-ratio: 16 / 10"][style*="overflow"]:not([style*="scroll-snap"]) {
  aspect-ratio: 16 / 9 !important;
}

/* --- About: the pinned milestone stage -------------------------------------
   From the design's own export (2026-08-18): the section holds while you scroll
   and each year takes the centre in turn, the story fading in beneath it. The
   stage's travel is written by refresh.js, see `msStep` there for why this is
   scroll-driven rather than `position: sticky`. */
/* One year hands over to the next sideways, not by dissolving in place (asked
   for 2026-08-19): the year you have read leaves to the left and the next one
   arrives from the right. A slide waits off to the right by default, so the
   first hand-over travels the same distance as every later one; `.past` is the
   ones already read, parked off to the left.

   The travel is the whole width of the stage, so this reads as a carousel. The
   fade is deliberately much shorter than the movement, long enough that two
   cards crossing do not both sit at full strength, short enough that the card
   is not a ghost for most of its journey. */
/* Smoothed 2026-08-19. Three things were fighting each other and the result
   read as a stutter rather than a glide:

     * the fade ran in 240ms against a 720ms slide, so the card was invisible
       for two thirds of its journey and then appeared to pop into place. Both
       channels now run the same duration on the same curve, so the card is
       simply visible for the whole move.
     * the year and the copy carried their own rises on top of the card's
       travel, three easings at once, none of them agreeing. The card is the
       only thing that moves now; the year keeps a whisper of a rise.
     * transform and opacity are promoted, so the whole hand-over is composited
       instead of being repainted 60 times.

   The curve is a long tail (0.22, 1, 0.36, 1): most of the distance is covered
   early and the last few pixels ease out, which is what reads as "smooth". */
/* The scroll the pinned deck occupies. refresh.js publishes the number on
   <html> as --fm-ms-h rather than writing it inline on the track, so a
   re-render cannot take it away and leave the page 10,000px shorter than the
   reader's position (see msSize in refresh.js). The phone block above forces
   `height: auto !important`, so this never reaches a stacked deck. */
[data-ms-track] {
  height: var(--fm-ms-h, auto);
}

[data-ms-slide] {
  opacity: 0;
  pointer-events: none;
  transform: translateX(100%);
  will-change: transform, opacity;
  transition:
    opacity 900ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 900ms cubic-bezier(0.22, 1, 0.36, 1);
}

[data-ms-slide].past {
  transform: translateX(-100%);
}

[data-ms-slide].on {
  opacity: 1;
  pointer-events: auto;
  transform: translateX(0);
}

/* so a card leaving clips at the column's edge rather than drifting out across
   the page margin on a wide screen */
[data-ms-stage] > div:last-child {
  overflow: hidden;
}

/* A hair of settle on the year, on the same curve and the same clock as the
   card, so it reads as one movement and not as two. */
[data-ms-slide] [data-ms-year] {
  transform: scale(0.97);
  transition: transform 900ms cubic-bezier(0.22, 1, 0.36, 1);
}

[data-ms-slide].on [data-ms-year] {
  transform: scale(1);
}

/* The copy no longer rises on its own, it travels with the card it is on. Its
   opacity stays with the slide's, which is what removed the second fade. */
[data-ms-slide] [data-ms-body] {
  transform: none;
}

/* One column on a phone: 42% of a 360px screen is not a photograph.
   The card's landscape 7/4 is a DESKTOP shape and has to go here, stacked, it
   made the card 173px tall on a 390px screen, of which the photo took 150 and
   the copy got 41 for 212px of text. The copy was simply clipped away
   (reported 2026-08-19: "where is the matter for this mobile view").

   Rows instead of a ratio: the copy takes the height it needs and the photo
   takes what is left, down to a 120px floor. That way a year carrying two
   projects gives its extra room up out of the picture rather than off the
   bottom of the card. */
@media (max-width: 768px) {
  [data-ms-card] {
    grid-template-columns: 1fr !important;
    grid-template-rows: minmax(120px, 1fr) auto !important;
    aspect-ratio: auto !important;
    flex: 1 1 auto !important;
    align-self: stretch !important;
    max-height: 100%;
  }

  [data-ms-shot] {
    min-height: 0 !important;
  }
}

/* --- The caption on a phone ------------------------------------------------
   Asked for 2026-08-19: "Talk to us" is to be visible on every screen, small or
   big. It was not, and nothing in this file was hiding it, the chat widget
   sets `display:none` on its own greeting inline below a width of its choosing
   (verified by resizing: `block` at 1200px, `none` at 390px). So this has to
   out-shout an inline style, hence `!important`.

   `:not(:has(…Close chat…))` keeps the one case where the widget is right to
   hide it: with the chat panel open the caption would sit on top of the
   conversation. The panel's launcher relabels itself to "Close chat" when it
   opens, which is the signal. */
@media (max-width: 720px) {
  body:not(:has(button[aria-label="Close chat"])) > button[data-fm-chat-greeting] {
    display: none !important;
  }
}

/* The colophon mark: 18px in the export read as loud as the copyright line it
   sits beside. 13px puts it level with the "Crafted by" type (2026-08-19).
   Keyed on the alt text, which survives a re-export where the bundled filename
   does not. */
body a[href*="nexprop.ai"] img[alt="nexprop.ai"] {
  height: 13px !important;
}

/* --- The foot of the page reads like the head of it ------------------------
   Asked for 2026-08-19: the header is a beige stripe over a white bar, and the
   footer was beige over dark grey, the one place on the site where a band went
   dark. Inverted to match: white footer, beige colophon, with the same hairline
   between them that the header has between its two bands.

   `footer + [data-cursor="gold"]` because that attribute is on the header's top
   stripe too, the colophon is identified by being the band right after the
   footer, not by the cursor it sets. The mark's `brightness(0) invert(1)` was
   there to punch it white out of the dark grey; on beige it goes back to ink. */
body footer {
  background: var(--white) !important;
}

body footer + [data-cursor="gold"] {
  background: var(--off-white) !important;
  color: var(--gray-dark) !important;
  border-top: 1px solid var(--light-gray) !important;
}

body footer + [data-cursor="gold"] a {
  color: var(--gray-dark) !important;
}

body footer + [data-cursor="gold"] img[alt="nexprop.ai"] {
  filter: none !important;
}

/* --- 14. The chat panel stands off the page -------------------------------
   The widget's panel is a cross-origin iframe (fairmount.nexprop.ai), so the
   only thing this page can style is its frame, which is all it needs. It
   opened flush to the edge, square and shadowless like everything else here,
   and read as a pane cut into the site rather than a conversation opened over
   it (2026-08-19).

   Square corners stay, they are the site's direction. What separates the
   panel is a thick burgundy edge, the same colour as its own header, and a
   deep shadow under it. Clear air from the right edge is desktop-only: on a
   phone the panel is meant to take the screen. */
body div.fm-p-embed {
  border: 4px solid var(--burgundy) !important;
  box-shadow:
    0 30px 80px rgba(44, 44, 44, 0.34),
    0 8px 22px rgba(44, 44, 44, 0.18) !important;
}

@media (min-width: 861px) {
  body > div:has(> iframe[src*="nexprop"]) {
    right: 24px !important;
    bottom: 96px !important;
  }
}

/* The milestone hand-over is the one place on the site where content crosses the
   screen, so it is also the one that most needs an opt-out. */
@media (prefers-reduced-motion: reduce) {
  [data-ms-slide],
  [data-ms-slide] [data-ms-year] {
    transition: none !important;
  }

  [data-ms-slide],
  [data-ms-slide].past {
    transform: none !important;
  }
}

/* --- The milestone card holds its shape -------------------------------------
   Reported 2026-08-19 against the design's own card: "the photo became very big
   and the info became very small". Both were the same cause, the card was
   `flex:1` inside a 100vh stage, so it took whatever height was going: 756px
   tall on a 1080p screen against the design's 560. At that height the 42% photo
   column is enormous and the copy is a small cluster adrift in a tall white
   field. The design's card is 980x560, so it keeps that ratio and only shrinks
   when the stage is genuinely too short for it.

   The type was following viewport HEIGHT (`clamp(13px,2vh,17px)`), which is why
   it also went small on a 768px laptop, the one place it should not have. It
   follows width now, and the floor is a size that can still be read. */
[data-ms-card] {
  flex: 0 1 auto !important;
  aspect-ratio: 7 / 4;
  max-height: 100%;
}

[data-ms-title] {
  font-size: clamp(23px, 2.1vw, 31px);
}

[data-ms-tag] {
  font-size: 12px;
}

[data-ms-text] {
  font-size: clamp(15px, 1.15vw, 17px);
}

/* "CRAFTED BY" carried .18em, which on 13px uppercase pulled the two words
   almost as wide as the mark beside them (2026-08-19). Taken down twice: .08em
   was still too open, so .03em, barely tracked, just enough that the uppercase
   does not read as jammed together. */
body footer + [data-cursor="gold"] a span {
  letter-spacing: 0.03em !important;
}

/* --- The milestone stage does not pin on a phone ---------------------------
   The stacked card fits a 390x844 screen but not a 360x640 one: the pinned
   stage is one viewport tall, and after the heading and the year there is about
   350px left for a card that needs 415 to show a year carrying two projects.
   No amount of sizing fixes that, the copy has to go somewhere.

   So below 860px the section stops being a stage and becomes what it would have
   been without JavaScript: the years stacked one after another, every one of
   them fully visible, scrolled past normally. It matches the phone gate in
   refresh.js (`phone()`), which no longer arms the driver at this width, one
   less rect read per frame on the device that can least afford it.

   `!important` throughout because the driver may have written the pinned
   geometry inline before the window was resized down to here. */
@media (max-width: 860px) {
  [data-ms-track] {
    height: auto !important;
  }

  [data-ms-stage] {
    position: static !important;
    height: auto !important;
    transform: none !important;
    overflow: visible !important;
    padding-bottom: clamp(40px, 8vw, 72px) !important;
  }

  /* the column the slides are absolutely positioned inside */
  [data-ms-stage] > div:last-child {
    display: block !important;
    flex: none !important;
    overflow: visible !important;
  }

  /* Stacked one by one, scrolling either way (asked for 2026-08-19). Each year
     holds under the header while the next rides up and covers it, plain
     `position: sticky`, no driver, so it costs nothing on the device that can
     least afford a scroll handler.

     Sticky DOES resolve here, contrary to the note on the vision rail: that one
     lives on the desktop page where support.js pins html/body to 100%, but on a
     phone the document itself scrolls (verified: body 8736px, no nested
     scrollport), so there is a scrollport for it to hold against.

     148px is the sticky header (139) plus air. The background is load-bearing:
     without it the card riding up would let the year underneath show through
     and the stack would read as a mess rather than as cards. */
  [data-ms-slide] {
    position: sticky !important;
    /* the slides carry `inset:0` inline for the desktop stage, and a sticky box
       given both top:0 and bottom:0 has nowhere to hold, it simply scrolls.
       The offsets are cleared before the one that matters is set. */
    inset: auto !important;
    top: 148px !important;
    opacity: 1 !important;
    transform: none !important;
    pointer-events: auto !important;
    overflow: visible !important;
    background: var(--white);
    padding-bottom: clamp(20px, 4vw, 32px);
    margin-bottom: clamp(28px, 5vw, 44px);
    /* Every slide at least as tall as the space below the header, so a card
       always covers the one it stacks on. Without it the heights are uneven, a
       year with one project is shorter than a year with two, and the short card
       sticking on top of the tall one left its tail showing underneath, years
       piling up and a stray line of copy reading through (2026-08-19).

       The 720px floor is what makes them EQUAL on a short screen: a two-entry
       year needs 665px, which is taller than a 640px phone, so keying only off
       the viewport left that one slide taller than its neighbours. Anything the
       CMS puts here that grows past 720px would make them uneven again, raise
       the floor if a milestone ever carries a third project. */
    min-height: max(100dvh, 720px);
  }

  [data-ms-slide] [data-ms-year] {
    transform: none !important;
    margin-bottom: clamp(10px, 2vw, 16px);
  }

  /* content height, not viewport height: nothing to clip against any more */
  [data-ms-card] {
    max-height: none !important;
    align-self: auto !important;
  }

  [data-ms-shot] {
    height: 34vw !important;
    min-height: 160px !important;
  }
}

/* --- ...and the phone's hand-over is the stack itself ----------------------
   There WAS a scroll-linked `view()` fade here (2026-08-19) so the years flowed
   into each other on a phone, written while the slides still scrolled freely.
   It is gone, and it has to be: the slides are sticky now, and a stuck element
   stops moving through the scrollport, which freezes its view timeline wherever
   it stuck. The cards sat half-transparent and never finished, worst on a short
   screen, where a two-entry year is tall enough that its `entry` phase outlives
   the whole section ("see its very transperent please fix").

   Nothing replaces it, because nothing needs to: the card physically rising and
   covering the one before it IS the hand-over. A fade on top was belt and braces
   when the cards cut hard, and legibility is worth more than either.

   If it comes back, it must not key off the sticky element's own travel, put
   the timeline on a non-sticky wrapper, or trigger it once with `.fm-in`. */



/* --- Signature Developments: the whole render, not the top of it ------------
   Reported 2026-08-19: "the full photo is not visible, only the top half".
   Measured at 1600px: the frame is 4/3 and the photo is 4/3, so `cover` fits it
   exactly, but the plaque laps 78px over the bottom of a 270px photo, so 29% of
   every render was behind white, and it was always the ground floor and entrance,
   which is the part of a retail render that sells it.

   Two changes, and both are needed:

     * the frame gets taller (17/16 rather than 4/3), which is the room the plaque
       needs to lap into without eating the picture. 15/14 left it 3px short at
       every width, measured, not guessed
     * the photo is `contain`, anchored to the top. That is the guarantee: the
       whole image is on screen at any card width, and the strip left underneath
       is what the plaque covers. `cover` could only ever crop.

   `transform: none` drops this layer's standing scale(1.12) and its scroll
   drift. With `contain` they work against the point, a 12% scale crops back
   exactly what the taller frame just bought. The card keeps its hover lift; only
   the picture stops moving inside its frame. */
body .fm-s-719fbd {
  aspect-ratio: 17 / 16 !important;
}

body .fm-s-719fbd > [data-img-reveal] {
  background-size: contain !important;
  background-position: center top !important;
  background-repeat: no-repeat !important;
  transform: none !important;
}

/* --- 16:9 on desktop, not just on a phone ----------------------------------
   Reported 2026-08-19: the video and the page banners sit correctly on a phone
   and wrong on a desktop. Measured, that is exactly right, and it is the
   container's fault rather than the artwork's, the source is 16:9 (1.78) and
   `cover` crops whatever does not fit:

     phone   390 wide -> 1.78  (16:9 by accident of the viewport) nothing lost
     laptop 1440 wide -> 1.91  ~7% of the frame cropped
     desktop 1920     -> 2.05  ~15% cropped
     banner 1920x560  -> 3.43  ~48% cropped, half the picture

   So both containers are given the ratio their content actually has. `cover`
   stays: with the box at 16:9 there is nothing left to crop, and it still saves
   the layout if a non-16:9 file is ever uploaded.

   Only above the phone tier, mobile is already correct and is left alone. */
@media (min-width: 861px) {
  /* Same cap for the page headers. At 16:9 they came out 1080px tall, which is
     a full screen of photograph before a word of the page, far more than a
     header wants. They keep the ratio while it fits and give it up rather than
     grow past 62% of the window. A photograph crops gracefully where a video
     with captions does not, so these stay `cover` and lose no width to bars. */
  section[data-page-banner] {
    min-height: 0 !important;
    height: min(56.25vw, 62vh);
  }

  /* One rule for every width: the hero IS a 16:9 frame, sized from its width.
     The film is 16:9 too, so box and film agree and there is nothing to crop and
     no bars to fill, the whole frame is on screen at every viewport.

     What this replaces, and why (2026-08-24): the hero used to be told to fill
     the height left under the header, which is a different shape from 16:9 at
     every window size. That is the conflict every previous attempt ran into, a
     16:9 film cannot fill a 2.1-wide space, show its whole frame, AND end on the
     fold. Sizing from width drops the third demand, which is the one that was
     never worth its cost: the film is complete, and the page simply continues
     below it.

     No viewport height, no header arithmetic, no cap. Nothing here needs to know
     what the header measures. */
  body section[data-screen-label="Home hero"][style] {
    /* Nothing is ever trimmed (2026-08-26, asked for explicitly).
       That has one meaning in CSS: the frame takes the FILM's shape and the
       film is contained inside it, so no edge can ever be cut off, whatever
       is uploaded, whatever the screen.

       The consequence has to be stated because it cannot be avoided: window
       shapes vary from 1.54 on a 14-inch MacBook to 2.04 on a maximised 1080p
       screen, so the film cannot match all of them. The leftover shows as the
       page continuing below the hero, or the hero reaching a little past the
       fold. Measured over nine real screens, a 16:9 export keeps that mismatch
       under 145px either way, the smallest of any ratio tested.

       refresh.js reads the film's intrinsic size into --fm-hero-ar, so this
       follows whatever is uploaded next with no change here. */
    min-height: 0 !important;
    height: auto !important;
    max-height: none !important;
    aspect-ratio: var(--fm-hero-ar, 16 / 9) !important;
    width: 100% !important;
  }



  section[data-screen-label="Home hero"] video,
  section[data-screen-label="Home hero"] [data-hero-media] video {
    position: absolute !important;
    inset: 0 !important;
    width: 100% !important;
    height: 100% !important;
    /* `contain`, not `cover`. The frame above is already the film's own shape,
       so the two agree and this costs nothing, but it is what guarantees that
       nothing is ever trimmed, including in the moment before the ratio has
       been measured and for whatever ratio is uploaded next. */
    object-fit: contain !important;
    object-position: center !important;
    display: block;
    z-index: 0;
  }
}

/* --- The milestone photo is landscape, like the file it comes from ----------
   The card is 980x560 and the photo column 42% of it, so the shot was a
   412x560 portrait slot, and a 1200x900 render cropped into it lost its sides
   and read as a tall strip (2026-08-24, "this image is vertical, they want it
   horizontal").

   The shot takes its own 4:3 and sits at the top of the column; the card stops
   forcing a height and takes whatever the taller of photo and copy needs. On a
   year with two projects the copy is the taller of the two and a little ground
   shows under the picture, which is the honest trade for never cropping it. */
@media (min-width: 769px) {
  [data-ms-card] {
    aspect-ratio: auto !important;
    max-height: 100%;
  }

  [data-ms-card] [data-ms-shot] {
    aspect-ratio: 4 / 3;
    align-self: start;
    min-height: 0 !important;
  }
}

/* --- The milestone slide sits in the middle of its stage --------------------
   The stage is a viewport tall because it is pinned, and the slide was
   top-aligned inside it. That was fine when the card was a fixed 560px; now the
   card is only as tall as its content (309 for a single project), so the
   leftover fell in one lump under the card (2026-08-24, "a lot of space here
   below"). Centred, the year and its card sit in the middle of the stage and
   the space is shared top and bottom instead of dumped at the foot. */
@media (min-width: 861px) {
  [data-ms-slide] {
    justify-content: center !important;
  }

  [data-ms-stage] > div:last-child {
    align-items: center !important;
  }

  /* The card grew wider once the photo went landscape, because the stage is a
     full screen and the card had shrunk to 309px inside it. 1160 is the column
     the stage already allows, and a 46% photo share makes the picture the size
     it deserves, together they take the empty space back into the content
     rather than leaving it under the card (2026-08-24). */
  [data-ms-card] {
    width: min(1160px, 100%) !important;
    grid-template-columns: minmax(0, 46%) 1fr !important;
  }

  /* The stage was a full viewport tall while its content came to about 700, so
     centring left 134px of air above the year AND 134 below the card, the two
     gaps reported separately are the same leftover, split. Sizing the stage to
     what it actually holds closes both at once (2026-08-24).

     Capped at 100vh so it can never exceed the screen. 720 measured: every card
     comes out 400px tall (the photo drives it, and even a year with two projects
     fits inside that), plus the heading, the year and the padding. The pin is
     unaffected: the driver's travel comes from the track's height, not the
     stage's. */
  [data-ms-stage] {
    height: min(100vh, 720px) !important;
  }
}

/* --- The header scrolls away with the page ---------------------------------
   Asked for 2026-08-24. responsive.css turns the export's `position:fixed`
   header into `sticky`, so it held the top 149px of every screen for good: the
   hero lost that much of its height, the pinned milestone stage ran underneath
   it (its heading was clipped by the header's lower edge), and every full-height
   section had to be measured around it.

   Static gives the space back. The trade is real and worth saying plainly: the
   navigation is no longer permanently on screen, so a reader deep in a page must
   scroll up to reach it. That is the common pattern for a site whose pages open
   on a full-bleed film, and it is what buys the film its full height.

   Desktop only. On a phone the header is 139px of a much shorter screen and the
   sticky nav is doing more work, so it stays. */
@media (min-width: 861px) {
  body div[style*="position: fixed"][style*="z-index: 50"],
  body div[style*="position:fixed"][style*="z-index:50"] {
    position: static !important;
  }
}

/* --- The F-Desk flipbook gets the full width -------------------------------
   Reported 2026-08-24: the issue is unreadable and the copy beside it is being
   cut. Measured at 1600, the band was two 544px columns and the flipbook had
   518x356 to show a two-page spread in, about 259px per page, which is why the
   text on the pages is a blur and why the copy column had no room.

   Stacked instead: the copy takes the full width, the flipbook takes it below.
   That is 1160x725 at the same window, more than double the width per page, and
   the two can no longer crowd each other. */
@media (min-width: 861px) {
  section[data-magazine] > div {
    grid-template-columns: 1fr !important;
    align-items: start !important;
  }

  /* 16/10 rather than the 16/11 it had: a spread is wider than it is tall, and
     this keeps the band from running past a screen at 1160 wide.
     
     Third spelling added 2026-09-02, and it is the one that actually matches:
     the runtime re-serialises the style attribute through the CSSOM as
     "aspect-ratio: 16 / 11", with spaces around the slash, so neither of the two
     spellings below ever hit and this rule had been doing nothing. Measured, the
     desktop flipbook was still 1134x780, which is 16/11. */
  section[data-magazine] [style*="aspect-ratio:16/11"],
  section[data-magazine] [style*="aspect-ratio: 16/11"],
  section[data-magazine] [style*="aspect-ratio: 16 / 11"] {
    aspect-ratio: 16 / 10 !important;
  }
}

/* --- Footer socials: filled discs, not outlines ----------------------------
   Asked for 2026-08-24 with a reference sheet of solid social discs: "we should
   use icons like this… instead of black we can use 20% grey".

   So the disc is filled at 20% black and the glyph is knocked out of it in
   white, the reference's shape, in the tone asked for rather than the black it
   shows. On the footer's white ground a 20% disc reads as soft rather than
   heavy, which is the point of asking for grey over black.

   Was footer-only: the top stripe's row was kept lighter and unringed on an
   earlier request, so it would not out-shout the logo beside it. Reversed
   2026-08-29 — the top stripe now carries the same 20%-black disc, just at
   its own 28px rather than the footer's 52px (see "5. Top stripe" above). */
body footer .fm-s-76db03 {
  background: rgba(0, 0, 0, 0.2) !important;
  border-color: transparent !important;
}

body footer .fm-s-76db03 svg {
  color: var(--white) !important;
}

/* --- Sections fade with the scroll, both ways ------------------------------
   Asked for 2026-08-24: what you have passed should fade away above, what is
   coming should fade in below, and scrolling back up should reverse it.

   One scroll-linked animation over the whole `cover` range does all of that,
   because a view timeline is a position, not a timer, run the scroll backwards
   and the fade runs backwards with it, for free. The reveal system already in
   this file cannot do it: it adds a class once and unobserves, deliberately, so
   it only ever fades in and only ever once.

   The ramps are short (8% either end) so a section is at full strength for the
   great majority of its pass and only softens as it leaves the frame. 14% was
   tried first and measured: a short section on About dipped to 0.66 while it was
   still under the reading line, which is dim for something you are reading.

   Three exclusions, and they are not cosmetic:
     * the home hero is `position: sticky`
     * the milestone section holds a pinned stage
     A stuck element stops moving through the scrollport, which freezes its view
     timeline wherever it stuck, that is what left the milestone cards at
     opacity 0.36 for ever on 2026-08-19. Anything pinned must not carry this.
     * page banners open the page; fading one in on load reads as a fault.

   Behind @supports because Safari has no view timelines at the time of writing:
   there, sections simply stay at full opacity, which is the current behaviour. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    /* The CONTENT fades, not the section. Animating the section itself takes its
       background with it, so a band with a colour of its own turns translucent
       and the section behind shows through, Trusted Partners came out washed
       pink over the gallery beneath it (2026-08-24). Fading the wrapper inside
       leaves the background solid and moves only what you read. */
    body section:not([data-screen-label="Home hero"]):not([data-page-banner]):not(:has([data-ms-track])) > div {
      animation: fm-scroll-fade linear both;
      animation-timeline: view();
      animation-range: cover;
    }

    /* the section's own edit-mode bar is chrome, not content */
    body section > div.fm-s-d295ba {
      animation: none !important;
      opacity: 1 !important;
    }
  }
}

@keyframes fm-scroll-fade {
  0% {
    opacity: 0;
  }

  8% {
    opacity: 1;
  }

  92% {
    opacity: 1;
  }

  100% {
    opacity: 0;
  }
}

/* --- The hero container is full bleed --------------------------------------
   Asked for 2026-08-24: the film must touch both edges of the viewport at every
   width. The section is already full width in the export; these are stated
   explicitly so a container rule cannot creep in and constrain it again, and so
   the absolutely positioned film has a positioning context of its own.

   `position` is left alone deliberately, the export makes the hero sticky so
   the page scrolls over it, and sticky is already a positioned ancestor, so
   `inset: 0` on the film resolves against the hero either way. */
section[data-screen-label="Home hero"] {
  width: 100% !important;
  max-width: none !important;
  margin-left: 0 !important;
  margin-right: 0 !important;
  padding-left: 0 !important;
  padding-right: 0 !important;
  overflow: hidden !important;
}

/* anything the hero holds sits above the film */
section[data-screen-label="Home hero"] > *:not(video) {
  position: relative;
  z-index: 1;
}

/* --- The hero film is not a parallax layer ---------------------------------
   `drift()` tags any suitable image layer with [data-parallax], and that rule
   carries `transform: scale(1.12)` so a few pixels of drift can never expose the
   frame behind it. Applied to the hero video that does two unwanted things
   (2026-08-24): it blows the film up 12%, cropping that much more of it than the
   container needs, and it overhangs the container on every side to 3px of it
   ended up behind a navbar that is only 86% opaque, so it showed through.

   The film fills its box exactly instead. It is a video, not a still that needs
   hiding a seam. */
section[data-screen-label="Home hero"] video {
  transform: none !important;
}

/* --- The hero standard, at every width -------------------------------------
   The frame is 16:9 and the film is shown whole inside it. Stated outside any
   media query on purpose: the phone tier sets `cover`, which is identical while
   the film is 16:9 but would silently crop a 4:3 or portrait upload. This is the
   guarantee that the standard holds for whatever is uploaded next, rather than
   only for the file that happens to be there today (2026-08-24). */
/* The phone tier follows the film too. It was pinned at 16:9, which was right
   while the film was 16:9 and wrong the moment it became 16:8, that mismatch
   was putting bars above and below the film on every phone. */
@media (max-width: 860px) {
  /* `[style]` and the `body` prefix to out-specify responsive.css, which sets
     `height: 56.25vw !important` here, an explicit height beats an
     aspect-ratio, so without this the phone stayed locked at 16:9 whatever the
     film's shape. */
  body section[data-screen-label="Home hero"][style] {
    aspect-ratio: var(--fm-hero-ar, 16 / 9) !important;
    height: auto !important;
    min-height: 0 !important;
  }
}

/* Last word on the fit, because this sits after the tiers above and shares
   their specificity. `contain` everywhere: the frame is already the film's own
   shape, so there is nothing to crop and nothing to bar. */
section[data-screen-label="Home hero"] video {
  object-fit: contain !important;
  object-position: center !important;
}

/* --- Page banners: one fixed ratio, so one export fits ---------------------
   Asked for 2026-08-26. The band's height was content-driven with a min-height,
   so its shape changed with the window to 3.29 at 1920, 2.87 at 1600, 2.58 at
   1440. No single exported image could fit all of those, which is why a picture
   sized for one screen was cropped on the next.

   3:1 at every width instead. The band is now a known quantity: export at
   1920 x 640 and it fits exactly, on every screen, with nothing trimmed.

   `width: 100%` is load-bearing, with `width: auto` and a ratio, capping the
   height makes the browser derive the width from the ratio instead, which
   collapsed the banner to 1406px and left white beside it (fixed 2026-08-25).

   Desktop only: the phone tier keeps its taller band, where a 3:1 strip would
   leave the heading no room. */
@media (min-width: 861px) {
  body section[data-page-banner] {
    aspect-ratio: 3 / 1 !important;
    width: 100% !important;
    height: auto !important;
    min-height: 0 !important;
  }
}

/* --- Page banners: one fixed ratio, so one export fits every screen --------
   Asked for 2026-08-26. The band's height was content-driven with a min-height,
   so its shape changed with the window to 3.29 at 1920, 2.87 at 1600, 2.58 at
   1440. No single exported image could fit all of those, which is why a picture
   sized for one screen was cropped on the next.

   A fixed ratio is what makes it both predictable and responsive: the band
   still scales with the window, but its shape no longer changes, so a picture
   that fits one screen fits all of them. Export at 1920 x 640 and a 13-inch
   laptop shows it at 1280 x 427, a 16-inch at 1707 x 569, same framing,
   nothing trimmed.

   `width: 100%` is load-bearing, with `width: auto` and a ratio, capping the
   height makes the browser derive the width from the ratio instead, which
   collapsed the banner to 1406px and left white beside it (fixed 2026-08-25).

   Desktop only: the phone tier keeps its taller band, where a 3:1 strip would
   leave the heading no room. */
@media (min-width: 861px) {
  body section[data-page-banner] {
    aspect-ratio: 3 / 1 !important;
    width: 100% !important;
    height: auto !important;
    min-height: 0 !important;
  }
}


/* --- The hero scrolls away with the page ----------------------------------
   Reported 2026-08-26 as the site "sticking". The export gives the hero
   `position: sticky; top: 0`, so a full-screen 1080p film stayed pinned to the
   viewport and kept decoding and compositing for the whole length of the page, measured still on screen, still playing, 3,500px down.

   Nothing was gained by it: every section below has a solid background of its
   own (they have to, or the fade turns them translucent and the film shows
   through, that was the washed-pink partners band on 2026-08-24), so the
   pinned hero was never visible after the first screen.

   Letting it scroll away means the browser drops it, and the observer in
   refresh.js can pause the film once it leaves. */
body section[data-screen-label="Home hero"] {
  position: relative !important;
}

/* --- Phones: don't rasterise what is off screen ---------------------------
   Reported 2026-08-26 as heavy lag on a phone. The Trusted Partners marquee
   renders 187 logo tiles, the list twice per row so the loop is seamless, and a phone was decoding and compositing all of them at once, on top of
   everything else on the page.

   `content-visibility: auto` lets the browser skip layout and paint for a
   subtree that is off screen, and `contain-intrinsic-size` keeps the scrollbar
   honest by reserving the height it would have taken. Nothing is hidden and
   nothing looks different, the work just happens when the band is reached.

   Phones only: on a desktop there is headroom, and skipping work there would
   only risk the marquee popping in. */
@media (max-width: 860px) {
  body section:has([style*="animation"]) > div,
  body [data-ms-track],
  body section > div {
    content-visibility: auto;
    contain-intrinsic-size: auto 600px;
  }

  /* The band itself still needs to paint as one piece, or the two copies of the
     logo strip can tear at the seam mid-scroll. */
  body [style*="width:max-content"],
  body [style*="width: max-content"] {
    content-visibility: visible;
  }
}

/* --- The project page's photograph, whole ---------------------------------
   The frame is `height:70vh; min-height:520px` inside a 1400px column, and the
   picture in it is the 4:3 render the admin asks for (1600 x 1200). Filled
   with `cover`, the two ratios never agreed and the difference was cropped in
   silence: 28% off the top and bottom of every building on a 1080p screen,
   40% on a laptop. Measured, not guessed.

   The frame takes the picture's own shape instead, so the whole render is
   visible and the hero still runs the full width of its column.

   2:1, a fixed shape an editor can upload to, the way the home hero has its
   own 1920 x 790. Fixed rather than viewport-relative on purpose: the frame no
   longer changes with the height of the window, so one uploaded size is
   correct on every screen.

   Why 2:1 and not something taller. The header is 230px and the wrapper's own
   air puts the frame at about 352px down the page, so on a 1080p screen the
   hero has roughly 680px before the fold. 2:1 is 700px at the column's full
   1400, which lands the whole photograph on one screen. 16:9 was tried first
   and ran 110px past the bottom; 4:3 stood 1050px tall, and capping that to
   the viewport boxed it into the middle of the page with off-white either
   side. All three measured on the day (2026-09-07). */
body div[style*="height:70vh"][style*="min-height:520px"],
body div[style*="height: 70vh"][style*="min-height: 520px"] {
  height: auto !important;
  min-height: 0 !important;
  aspect-ratio: 2 / 1 !important;
}

/* --- The project photograph zooms on hover, not on its own -----------------
   The design gave this one picture `animation: fm-kenburns 16s ease-out both`,
   a slow creep from scale(1) to scale(1.08) that runs the moment the page
   opens whether anyone is looking at it or not, and with `both` it then stays
   zoomed for good. Asked for 2026-09-07: the zoom should answer a hover, the
   way every other picture on the site does.

   `animation: none` rather than removing the inline style, so the design's own
   markup is untouched and a re-export cannot bring it back. The transition and
   the 1.05 are the same pair the project cards and the gallery tiles use, so
   the hero now behaves like the rest of the site. */
body [data-zoom="1"] {
  animation: none !important;
  transform: none !important;
  transition: transform 0.7s cubic-bezier(0.22, 0.61, 0.36, 1) !important;
}

/* Only where a pointer can actually hover: on a touch screen :hover sticks
   after a tap, which would leave the picture zoomed with no way back. */
@media (hover: hover) and (pointer: fine) {
  body [data-zoom="1"]:hover {
    transform: scale(1.05) !important;
  }
}
