/*
 * Public marketing design layer — the luxury "first visual world".
 * Ink + gold on cream/near-black, editorial serif display (Cormorant Garamond) + Jost sans.
 * Tokens mirror the Claude Design project (tokens/*.css).
 */
:root {
    --ink:        #0f0e0c;
    --ink-2:      #16140f;
    --cream:      #f6f3ec;
    --cream-2:    #f3efe6;
    --gold:       #c9a86a;
    --gold-2:     #e2c58f;
    --gold-deep:  #a8874a;
    --gold-lit:   #d9b878;
    --stone:      #7a6a4d;

    --font-display: 'Cormorant Garamond', Georgia, 'Times New Roman', serif;
    --font-sans:    'Jost', system-ui, -apple-system, sans-serif;

    /* Motion tokens.
       Not new values — these are the numbers already repeated literally across this file and
       the GSAP controllers, given names so a property-page section can reference the house
       curve instead of copying `cubic-bezier(.2,.7,.2,1)` a fourth time. The existing `.ar-*`
       and `.pc-*` blocks still carry their literals; they are left alone deliberately, since
       rewriting shipped pages to prove a point is how a refactor breaks something. New work
       uses the tokens.

       `drift` is the slow one that images scale on, `move` for small positional changes,
       `tint` for colour alone. `reveal` mirrors gsap_reveal_controller.js, which owns the
       fade-rise and cannot read a custom property — stated in both places, and they must
       change together. */
    --ease-drift:      cubic-bezier(.2, .7, .2, 1);
    --duration-drift:  1.6s;
    --duration-move:   .5s;
    --duration-tint:   .4s;
    --reveal-distance: 24px;

    /* The transit sheet (assets/js/page_transition.js). `drift` is an out-curve — right for
       something settling into place, wrong for a sheet passing *through* the frame, which
       has to gather itself and arrive. `sweep` is a quart in-out. The two durations are
       mirrored as COVER_MS / LIFT_MS in the module and must change together. */
    --ease-sweep:      cubic-bezier(.7, 0, .3, 1);
    --transit-cover:   .7s;
    --transit-lift:    .8s;
    /* Cream, not ink: an ink sheet over ink pages reads as the page itself going dark, and
       the reader cannot tell a transition from a stall. The sheet has to be something
       plainly *placed over* the page. The entrance curtain stays ink — it has nothing to be
       told apart from. */
    --transit-sheet:   var(--cream);

    /* ---- vertical rhythm, shared with the property pages (page-themes PRD §3.1) ----
       Half-gaps, deliberately. A section owns the space above *and* below itself as padding
       rather than taking a margin from its neighbour, so two adjacent sections sum to the
       full gap and the number below is half of what the eye reads between them.

       Padding rather than margin is not a style preference: a themed band paints its own
       ground, and a margin between two grounds shows the page ground through the seam. The
       moment sections can carry different backgrounds, margin rhythm stops working. */
    --lp-section-pad-tight: clamp(20px, 3vw, 42px);
    --lp-section-pad:       clamp(36px, 5.5vw, 84px);
    --lp-section-pad-grand: clamp(48px, 7.5vw, 116px);

    /* Height of the fixed public nav (px-6 py-5 around a 15px wordmark). Only the spacer
       that stands in for it in the flow reads this — the nav itself is sized by content. */
    --nav-h: 64px;

    /* ---- the cover seam (scroll-architecture PRD §3) ----
       The three phases the Forbes monument is scrolled through while stuck at top: 0. Unitless
       so CSS can multiply them by 1svh; nothing in JS reads them back. The phases are geometry
       now — the burial's ScrollTrigger measures the covering sheet directly, and the hold is
       simply the gap these numbers open before that sheet arrives.

       They live here rather than on `.fb` because the covering section is not its descendant
       and needs --fb-cover for its negative margin — the two halves of the seam must agree, and
       inheritance from a shared ancestor is what makes disagreement impossible.

       svh, never dvh: dvh changes continuously as iOS collapses the address bar, which would
       move the sticky release point mid-scroll. That is the jitter AC-10 forbids. */
    --fb-hold:    0;   /* the internal sequence. Zero deliberately: held travel is scroll the
                          visitor spends on a still image, and the claim's reveal plays in real
                          seconds whether or not the page waits for it. The sheet begins
                          arriving on the pixel the monument locks. */
    --fb-rest:    0;   /* the pause before the sheet arrives. Also zero, same reason. */
    --fb-cover:  50;   /* THE OVERLAP, AND THE ONLY DIAL THAT COSTS SOMETHING.

                          Two things at once, which is what makes it a dial rather than a
                          constant: it is how far `.hp-cover` is pulled up into `.fb`, and
                          therefore how much of the viewport the sheet has covered at the pixel
                          the sticky box releases. Release is at scrollY == --fb-cover from the
                          lock; the sheet's top edge is then (100 − --fb-cover)% down the screen.

                          At 100 the monument is fully buried before it moves — the original
                          seam, and 150svh of scroll to get through. At 50 it is half covered,
                          releases, and the exposed top half slides away with the sheet. That
                          is the deliberate trade for halving the section: the burial reads as
                          a pause and a hand-off rather than a full page-turn.

                          Whatever it is set to, the depth push finishes exactly on the release
                          pixel — forbes_scrub reads this token to place its end. */
}

/* The fixed nav overlaps anything an anchor scrolls to. One declaration covers every fragment
   on the site, including the `/#contact` redirect the consultation form issues on success —
   which lands under the nav today. */
html { scroll-padding-top: calc(var(--nav-h) + 8px); }

.lux {
    background: var(--ink);
    color: var(--cream);
    font-family: var(--font-sans);
    -webkit-font-smoothing: antialiased;
}

.lux a { color: var(--gold); text-decoration: none; }
.lux a:hover { color: var(--gold-2); }

.lux-display { font-family: var(--font-display); font-weight: 300; }
.lux-eyebrow {
    font-family: var(--font-sans);
    font-weight: 400;
    font-size: 10px;
    letter-spacing: .4em;
    text-transform: uppercase;
    color: var(--gold);
}
.lux-rule {
    height: 1px;
    background: linear-gradient(90deg, rgba(201,168,106,.7), rgba(201,168,106,.05));
}

/* The curtain — one element, two moments. See templates/public/_curtain.html.twig.

   The entrance: inert unless the inline head script put `curtain-active` on <html> before
   first paint — which is the only moment the decision can be made, since every Encore script
   is deferred until after the browser has painted.

   The transit: `transit-active` goes on <html> for the length of a page change and the sheet
   walks through three states — parked below the frame, covering, lifting — each one a class
   assets/js/page_transition.js sets.

   `display: none` rather than opacity or visibility in the resting state: on the pages where
   it does not run it must cost nothing, not even a compositor layer. */
.lux-curtain { display: none; }

.curtain-active .lux-curtain,
.transit-active .lux-curtain {
    --curtain-mark: var(--cream);   /* the wordmark reads this; see the partial */
    display: flex;
    position: fixed;
    inset: 0;
    z-index: 200;               /* over the nav, which is 60 */
    align-items: center;
    justify-content: center;
    background: var(--ink);
    transform: translateY(0);
    /* 101%, not 100%: a subpixel seam at the bottom edge is visible against ink. */
    transition: transform .9s var(--ease-drift);
}
.transit-active .lux-curtain {
    --curtain-mark: var(--ink);
    background: var(--transit-sheet);
    /* Promoted before it moves. Without this the compositor builds the layer in the same
       frame the transition starts, and that first frame drops — which is the hitch. */
    will-change: transform;
}
.curtain-active .lux-curtain.is-lifting { transform: translateY(-101%); }

/* No scroll lock during a transit, unlike the entrance. Locking removes the scrollbar and
   giving it back reflows the page by its width at the very moment the sheet is lifting off
   the new page — a visible jog, in exchange for preventing a scroll nobody can see happen
   under an opaque sheet.

   The scrollbar is the one thing a fixed sheet cannot cover, and it is exactly where a body
   swap shows: the thumb of a page scrolled halfway down snaps to the top and changes length
   the instant the new body lands — a hop at the edge of an otherwise motionless screen.
   So for the length of a transit the scrollbar keeps its gutter and loses its paint. The
   colours are set on the root at rest too, not only during the transit: switching
   `scrollbar-color` on and off would switch Chrome between its native and CSS-drawn
   scrollbars, and those are not the same width. */
html {
    scrollbar-color: rgba(122, 106, 77, .55) transparent;   /* --stone, on nothing */
}
html.transit-active {
    scrollbar-color: transparent transparent;
}

/* The three transit states. Parked first, with no transition, so the element can be shown
   below the frame without sliding there; the module waits a frame before adding
   `is-covering`, or the browser would coalesce the two and the sheet would simply appear. */
.transit-active .lux-curtain.is-transit {
    transform: translateY(101%);
    transition: none;
}
.transit-active .lux-curtain.is-covering {
    transform: translateY(0);
    transition: transform var(--transit-cover) var(--ease-sweep);
}
.transit-active .lux-curtain.is-lifting {
    transform: translateY(-101%);
    transition: transform var(--transit-lift) var(--ease-sweep);
}

/* The rule under the wordmark reports readiness on the entrance. A transit has nothing to
   report — the sheet is down for as long as it takes and no longer. */
.transit-active .lux-curtain .lux-curtain-track { display: none; }

/* Turbo's own progress bar would draw a blue line along the top of a sheet that is already
   the indicator. Only where the sheet runs: a reduced-motion reader gets no sheet, and the
   bar is then the one honest sign that something is happening. */
.js-motion .turbo-progress-bar { display: none; }

/* ---- The column variant --------------------------------------------------------------
   `data-transit-style="columns"` on #lux-curtain trades the single sheet for four 25vw
   columns that arrive and leave a beat apart, left to right. Site-wide, not per page: the
   curtain is `data-turbo-permanent`, so the first page's markup is the session's markup.
   A hard reload shows a change.

   The module is unaware of the difference. It listens for `transitionend` on the sheet, and
   a child's transitionend bubbles to it — the last column's is the one that lands after
   COVER_MS's insurance timer anyway, so the timing simply holds.

   The sheet itself stops moving and goes transparent; the columns carry the ink. The
   wordmark waits for the last column before it shows. The last column carries
   `data-transit-last`, which is how the module tells its transitionend from the others. */
.lux-curtain-col { display: none; }
.transit-active .lux-curtain[data-transit-style="columns"] {
    background: transparent;
    transform: none;
    transition: none;
}
.transit-active .lux-curtain[data-transit-style="columns"] .lux-curtain-col {
    display: block;
    position: absolute;
    top: 0;
    bottom: 0;
    width: 25.5vw;              /* a hair over: no seam between columns against a photograph */
    left: calc(var(--i) * 25vw);
    background: var(--transit-sheet);
    will-change: transform;
    transform: translateY(101%);
    transition: none;
}
.transit-active .lux-curtain[data-transit-style="columns"].is-covering .lux-curtain-col {
    transform: translateY(0);
    transition: transform var(--transit-cover) var(--ease-sweep) calc(var(--i) * 70ms);
}
.transit-active .lux-curtain[data-transit-style="columns"].is-lifting .lux-curtain-col {
    transform: translateY(-101%);
    transition: transform var(--transit-lift) var(--ease-sweep) calc(var(--i) * 70ms);
}
.transit-active .lux-curtain[data-transit-style="columns"] .lux-curtain-mark {
    position: relative;         /* above the columns */
    opacity: 0;
    transition: opacity .3s var(--ease-drift);
}
.transit-active .lux-curtain[data-transit-style="columns"].is-covering .lux-curtain-mark {
    opacity: 1;
    transition-delay: calc(var(--transit-cover) + 210ms);
}
.transit-active .lux-curtain[data-transit-style="columns"].is-lifting .lux-curtain-mark {
    opacity: 0;
    transition-delay: 0s;
}

.lux-curtain-mark { text-align: center; }

/* The rule tracks real readiness, so it moves in three deliberate steps rather than
   continuously — the transition is what keeps that from reading as a stutter of its own. */
.lux-curtain-track {
    width: min(240px, 46vw);
    height: 1px;
    margin: 22px auto 0;
    background: rgba(201, 168, 106, .22);
}
.lux-curtain-rule {
    height: 100%;
    background: var(--gold);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform .5s var(--ease-drift);
}

/* The public nav. Fixed and transparent so a masthead photograph runs the full height of
   the screen behind it, then fading to frosted ink once the reader scrolls — nav_scroll
   controller toggles `is-scrolled`, everything visual happens here.

   No rule and no border in either state: the tint alone separates it from the page.

   The nav's own links are white and stay white — `.lux a` above paints every public link
   gold, which is right everywhere except here, where the menu reads as one line of type
   over a photograph and a colour change on hover would break that line. The dropdown
   panel's links are unaffected: they carry their colours inline, which outranks this. */
.lux-nav {
    position: fixed;
    inset: 0 0 auto;
    z-index: 60;
    background: transparent;
    transition: background var(--duration-tint) var(--ease-drift),
                backdrop-filter var(--duration-tint) var(--ease-drift);
}
.lux-nav.is-scrolled {
    background: rgba(15, 14, 12, .55);
    backdrop-filter: blur(14px);
}
/* Where the browser cannot blur, the tint alone has to carry the contrast. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .lux-nav.is-scrolled { background: rgba(15, 14, 12, .78); }
}
.lux-nav a,
.lux-nav a:hover { color: #fff; }
@media (prefers-reduced-motion: reduce) {
    .lux-nav { transition: none; }
}

/* Stands in for the fixed nav on pages that open on a text block instead of a hero. */
.lux-nav-spacer { height: var(--nav-h); }

/* Reveal animation base — the gsap_reveal controller animates these in.
   With no JS (or reduced motion) they stay fully visible.

   Gated on `.js-motion`, not `.js`. `.js` only means scripts are running; the question this
   rule actually asks is "did the controller that reveals these mount?", and only
   gsap_reveal_controller.js can answer it — it sets the flag immediately before it builds the
   triggers. Gating on `.js` meant a failed lazy chunk or a GSAP throw left the whole page
   present in the DOM and permanently invisible on screen. */
.reveal { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
    .js-motion .reveal { opacity: 0; transform: translateY(var(--reveal-distance)); }
}

/* The composer's preview frame — layout, not choreography. Wins over anything GSAP might
   have left inline; the attribute exists only on `?static=1`, so it cannot leak to a reader. */
[data-lp-static] .reveal { opacity: 1 !important; transform: none !important; }

/* Headings that split into characters, hidden until split_text_controller has built their
   line masks. `visibility` rather than `opacity` or `display` — it keeps the element in
   layout, so it cannot corrupt the line measurement the split depends on. Same reason
   `.pc-caption` and `.ls-name` further down do it this way.

   Keyed off the controller attribute rather than a hand-added class so the markup cannot say
   "animate me" and "hide me" separately and get the two out of step. `js-split` is set by the
   eager public entry and dropped again on a timer — see assets/public.js for why the flag
   lives there and not in the controller. */
@media (prefers-reduced-motion: no-preference) {
    .js-split [data-controller~="split-text"]:not([data-split-ready]) { visibility: hidden; }
}
[data-lp-static] [data-controller~="split-text"] { visibility: visible !important; }

/* ============================================================
   Area lifestyle report (Claude Design: "Neighborhood - Atherton Lifestyle").
   The design is authored at a fixed 1440/56px desktop grid; layout lives inline
   on the elements to stay 1:1 with it. Everything here is what inline styles
   cannot express — media queries and hover — so the page degrades on narrow
   viewports instead of overflowing.
   Namespaced .ar-* so none of it can reach the dashboard styles.

   SHARED with /lifestyles/{slug}, which is the same design language rather than a
   second one: hero, chapter spread, offerings grid and closing band are identical
   components with a different owner. The `ar` prefix is historical — read it as
   "the editorial report layout", not "areas only". Anything genuinely specific to
   a lifestyle page lives under .lf-* further down.

   SHARED with the homepage hero too, which uses .ar-hero, .ar-hero-copy and
   .ar-hero-stats so the masthead is one component across all three pages. It brings its
   own parallaxed photograph rather than the area partial's; nothing else differs.
   ============================================================ */
.ar-wrap {
    max-width: 1440px;
    margin: 0 auto;
    padding-left: clamp(20px, 5vw, 56px);
    padding-right: clamp(20px, 5vw, 56px);
}

/* Hero: 760px in the design, but never taller than the viewport on short screens. */
.ar-hero { height: min(760px, 92vh); min-height: 560px; }

/* The homepage variant: a full-screen stage with the copy on its centreline. `svh` (with a
   `vh` fallback for older engines) rather than `vh` on purpose — on mobile Safari `100vh`
   is the height with the browser chrome *hidden*, so a `100vh` photograph is shorter than
   the box it fills while the chrome is showing, and grey appears under it. */
.ar-hero--full { height: 100vh; height: 100svh; min-height: 0; }
.ar-hero-copy { left: clamp(20px, 5vw, 56px); right: clamp(20px, 5vw, 56px); }
.ar-hero-stats { left: clamp(20px, 5vw, 56px); right: clamp(20px, 5vw, 56px); }

/* The vertical "Lifestyle Report No. 04" rail needs real estate to the right of
   the headline; below that it would sit on top of the copy. */
@media (max-width: 1180px) { .ar-rail { display: none; } }

/* Below this the hero stops being a fixed-height stage: the copy and the stat
   band rejoin the flow so they cannot land on top of each other.
   `position: relative` rather than `static` — the photo and its scrims are
   absolutely positioned siblings, and a static block would paint underneath
   them. The !important flags are load-bearing: layout is authored inline on the
   elements (to stay 1:1 with the design file) and inline wins otherwise. */
@media (max-width: 900px) {
    .ar-hero { height: auto; min-height: 0; padding: 112px 0 44px; }
    .ar-hero-copy,
    .ar-hero-stats {
        position: relative !important;
        z-index: 2;
        top: auto !important;
        bottom: auto !important;
        left: auto !important;
        right: auto !important;
        max-width: none !important;
        margin-left: clamp(20px, 5vw, 56px);
        margin-right: clamp(20px, 5vw, 56px);
    }
    .ar-hero-stats {
        margin-top: 44px;
        grid-template-columns: repeat(2, 1fr) !important;
    }
    .ar-hero-stats > * {
        padding: 22px 0 !important;
        border-left: 0 !important;
    }

    /* …except the full-screen homepage hero, which stays a stage at every width: it is one
       photograph filling the screen, and stacking it into the flow would be a different
       page rather than a narrow version of this one. These rules undo the block above, so
       they have to be at least as emphatic as it is. */
    .ar-hero--full { height: 100svh !important; min-height: 0 !important; padding: 0 !important; }
    .ar-hero--full .ar-hero-copy,
    .ar-hero--full .ar-hero-stats {
        position: absolute !important;
        left: clamp(20px, 5vw, 56px) !important;
        right: clamp(20px, 5vw, 56px) !important;
        margin-left: 0 !important;
        margin-right: 0 !important;
    }
    .ar-hero--full .ar-hero-copy {
        top: 50% !important;
        transform: translateY(-50%);
    }
    .ar-hero--full .ar-hero-stats {
        top: auto !important;
        bottom: 0 !important;
        margin-top: 0 !important;
    }
}
@media (max-width: 560px) {
    .ar-hero-stats { grid-template-columns: 1fr !important; }
    /* Four rows of figures would climb into the centred copy — the full hero holds at two. */
    .ar-hero--full .ar-hero-stats { grid-template-columns: repeat(2, 1fr) !important; }
}

/* Chapter blocks: photo beside an overlapping vellum plate. The plate is pulled
   over the photo with a negative margin — that has to unwind when they stack. */
@media (max-width: 1024px) {
    .ar-chapter { grid-template-columns: 1fr !important; }
    .ar-chapter-media { height: clamp(260px, 46vw, 420px) !important; }
    .ar-plate {
        margin: -56px clamp(12px, 4vw, 40px) 0 clamp(12px, 4vw, 40px) !important;
        padding: clamp(30px, 5vw, 52px) clamp(26px, 5vw, 54px) !important;
    }
    /* Photo first in every chapter once stacked, whichever side it was on. */
    .ar-chapter-media { order: 1; }
    .ar-plate { order: 2; }
}

@media (max-width: 900px) {
    .ar-split { grid-template-columns: 1fr !important; gap: 40px !important; }
    .ar-amenity {
        grid-template-columns: 40px 1fr !important;
        gap: 6px 20px !important;
    }
    .ar-amenity-benefit { grid-column: 2; }
    .ar-amenity-time { grid-column: 2; text-align: left !important; }
    .ar-market { grid-template-columns: repeat(2, 1fr) !important; }
    .ar-offerings { grid-template-columns: 1fr !important; }
    .ar-valuation-copy {
        position: static !important;
        padding: clamp(48px, 9vw, 80px) clamp(20px, 5vw, 56px);
        max-width: none !important;
    }
    .ar-valuation-frame { height: auto !important; min-height: 0 !important; }
}
@media (max-width: 560px) {
    .ar-market { grid-template-columns: 1fr !important; }
    .ar-schools-grid { grid-template-columns: 1fr !important; }
    .ar-cta-row { flex-direction: column; align-items: flex-start; }
}

/* Section heads are "title left, note right" until there is no room for both. */
@media (max-width: 860px) {
    .ar-head { flex-direction: column; align-items: flex-start !important; gap: 22px !important; }
    .ar-head p { max-width: none !important; }
}

/* Slow push-in on the report imagery, matching the homepage card idiom. */
.ar-frame img { transition: transform 1.6s cubic-bezier(.2,.7,.2,1); }
.ar-frame:hover img { transform: scale(1.05); }
@media (prefers-reduced-motion: reduce) {
    .ar-frame img, .ar-frame:hover img { transition: none; transform: none; }
}

/* Area centroid map — the panel above "Inside fifteen minutes".
   The map is locked (no pan, no zoom, no controls) and unmarked: it is centred on the
   area centroid, and the town name Google already prints there says where it is far
   better than a pin sitting on top of that name. No marker also means no map id, and
   a map with no map id is the only kind that still honours the inline muted palette
   in assets/controllers/area_map_controller.js.

   Nothing here targets Google's own DOM — the attribution and logo it injects must
   stay visible, and those class names change between API releases. */
/* The one full-bleed element on the page: the map runs edge to edge rather than
   sitting in the 1440px .ar-wrap measure, so it reads as a break in the report
   rather than another card in the column. Vertical rhythm still comes from the
   section's inline padding-top. */
.ar-map-band { width: 100%; }

.ar-map {
    position: relative;
    height: clamp(300px, 32vw, 520px);
    overflow: hidden;
    background: var(--cream-2);
    /* Hairlines top and bottom only — side borders would cut the bleed. */
    border-top: 1px solid rgba(201, 168, 106, .28);
    border-bottom: 1px solid rgba(201, 168, 106, .28);
}
.ar-map-canvas { position: absolute; inset: 0; }

/* Footer town list wraps rather than squeezing the disclaimer to nothing. */
.ar-footer-towns { flex-wrap: wrap; }

/* The solid gold button keeps ink text on hover — `.lux a:hover` would otherwise
   repaint it gold-on-gold. */
.lux a.ar-btn-solid:hover { color: var(--ink-2); background: var(--gold-2); }
.lux a.ar-btn-ghost:hover { color: var(--cream); border-color: rgba(243, 239, 230, .7); }

/* ============================================================
   Homepage pillar cards — square photo morphs to a circle on scroll,
   caption revealed underneath as masked lines (pillar_cards_controller.js).

   Base state is the finished, readable state: square photo, caption visible.
   That is what no-JS, reduced-motion, and search crawlers get. The enhanced
   state lives behind BOTH `.js` and (prefers-reduced-motion: no-preference),
   so a reduced-motion user can never end up with a hidden caption if the
   controller bails early.
   Layout is in classes here, not inline on the elements, so the media queries
   below don't need !important the way .ar-* does.
   ============================================================ */
.pc-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: clamp(20px, 2.6vw, 44px);
}

.pc-card { display: block; }

.pc-figure {
    position: relative;
    aspect-ratio: 1 / 1;   /* load-bearing: `round 50%` is only a circle on a square */
    overflow: hidden;
    background: var(--ink-2);
}
.pc-figure img {
    display: block;
    width: 100%;
    height: 100%;
    -o-object-fit: cover;
       object-fit: cover;
    transition: transform 1.6s cubic-bezier(.2, .7, .2, 1);
}
.pc-card:hover .pc-figure img { transform: scale(1.05); }

/* The gold hairline, as an overlay rather than a box-shadow on .pc-figure so it
   sits above the image without the shadow bleeding over the object-fit edge. */
.pc-ring {
    position: absolute;
    inset: 0;
    border: 1px solid rgba(201, 168, 106, .3);
    pointer-events: none;
}

.pc-caption {
    margin-top: clamp(18px, 2vw, 28px);
    text-align: center;
}
.pc-title {
    margin: 0;
    font-size: clamp(21px, 2.1vw, 27px);
    line-height: 1.2;
    color: var(--cream);
}
.pc-blurb {
    margin: 12px auto 0;
    max-width: 30ch;
    font: 300 13.5px/1.75 var(--font-sans);
    color: rgba(246, 243, 236, .6);
}

/* SplitText's mask wrappers get `overflow: clip` inline and take their class
   from linesClass + "-mask". Declared here so the intent is greppable and so a
   cached-but-unreverted DOM still clips correctly. */
.pc-line-mask { overflow: clip; }
.pc-line { will-change: transform; }

/* `.lux a` paints every link gold; these cards are photo + cream text. */
.lux a.pc-card,
.lux a.pc-card:hover { color: inherit; }
.lux a.pc-card:hover .pc-title { color: var(--gold-2); }

/* ---- Enhanced state ---- */
@media (prefers-reduced-motion: no-preference) {
    /* Matches the wipe's from-state exactly, so the figure is hidden from first
       paint instead of flashing in before the controller lazy-loads. */
    .js .pc-figure {
        clip-path: inset(100% 0% 0% 0%);
        will-change: clip-path;
    }
    /* visibility, not opacity or display: layout (and therefore SplitText's line
       measurement) is unaffected, and the controller flips it inside onSplit. */
    .js .pc-caption { visibility: hidden; }
}

@media (prefers-reduced-motion: reduce) {
    .pc-figure img,
    .pc-card:hover .pc-figure img { transition: none; transform: none; }
}

/* ---- Responsive ---- */
@media (max-width: 1000px) {
    .pc-grid { gap: clamp(18px, 3vw, 32px); }
    .pc-blurb { font-size: 13px; }
}
@media (max-width: 760px) {
    /* Two up, third centred underneath — three columns at this width makes each
       circle too small to read as a subject. */
    .pc-grid { grid-template-columns: repeat(2, 1fr); }
    .pc-grid > :last-child {
        grid-column: 1 / -1;
        justify-self: center;
        width: calc(50% - clamp(9px, 1.5vw, 16px));
    }
}
@media (max-width: 560px) {
    .pc-grid { grid-template-columns: 1fr; max-width: 380px; margin-inline: auto; gap: 44px; }
    .pc-grid > :last-child { grid-column: auto; width: auto; }
}

/* ============================================================
   Homepage agent film band (PRD §4). Editorial split: the walkthrough loop on the left
   across seven columns, the claim on the right across four, a full gutter between.

   The frame is a window, not a picture. `--af-open` runs 0 → 1 under the scroll and the
   clip-path below derives the inset from it, so the media opens out to the column's bleed
   without the element ever changing size — no layout, no reflow of the copy beside it, and
   no zoom of the picture the way a scale would give. agent_film_controller.js writes the
   one number; every geometry decision is here.
   ============================================================ */
/* How far the media has to reach to clear the section's own horizontal padding once the
   band stacks. Two values because that padding is `px-6 md:px-14`. */
.af { --af-bleed: 24px; }
@media (min-width: 768px) { .af { --af-bleed: 56px; } }

.af-grid {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    gap: clamp(28px, 4vw, 64px);
    align-items: center;
}
.af-frame {
    grid-column: 1 / span 7;
    position: relative;
    aspect-ratio: 16 / 9;
    overflow: hidden;

    /* Resolved by the controller under scroll. The static value is the finished state, so
       reduced motion, a touch viewport and a failed chunk all land on the same open frame
       rather than on a card that never opens. */
    --af-open: 1;
    clip-path: inset(
        calc((1 - var(--af-open)) * 6%)
        calc((1 - var(--af-open)) * 10%)
        round calc((1 - var(--af-open)) * 3px)
    );
}
.af-media { position: absolute; inset: 0; background: var(--ink-2); }
.af-poster { position: absolute; inset: 0; width: 100%; height: 100%; -o-object-fit: cover; object-fit: cover; }
/* Weighted to the foot, where the label sits, and kept off the middle of the picture. */
.af-scrim {
    position: absolute; inset: 0; pointer-events: none;
    background: linear-gradient(180deg, rgba(15,14,12,.34) 0%, rgba(15,14,12,0) 34%, rgba(15,14,12,.52) 100%);
}
/* The affordance is the whole picture. One real button rather than a clickable div with a
   small button inside it: the accessible name and the keyboard behaviour come free, and
   there is exactly one thing in the tab order for one thing a reader can do. */
.af-cue {
    position: absolute; inset: 0;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 18px;
    cursor: pointer;
    background: none; border: 0; padding: 0;
    color: var(--cream);
}
.af-ring {
    width: 64px; height: 64px; border-radius: 50%;
    border: 1px solid rgba(246,243,236,.55);
    background: rgba(15,14,12,.18);
}
.af-cue-label {
    font: 400 10.5px/1 var(--font-sans);
    letter-spacing: .22em; text-transform: uppercase;
    color: rgba(246,243,236,.9);
}
@media (prefers-reduced-motion: no-preference) {
    .af-ring { transition: transform var(--duration-move) var(--ease-drift), border-color var(--duration-tint) var(--ease-drift); }
}
.af-cue:hover .af-ring,
.af-cue:focus-visible .af-ring { transform: scale(1.06); border-color: rgba(226,197,143,.9); }
.af-cue:focus-visible { outline: 2px solid var(--gold); outline-offset: 4px; }

.af-copy { grid-column: 9 / span 4; }
.af-headline {
    margin: 0;
    font-size: clamp(30px, 3.6vw, 44px);
    line-height: 1.14;
    text-wrap: balance;
}
.af-graf {
    margin: 22px 0 0;
    font: 300 15px/1.85 var(--font-sans);
    color: rgba(246,243,236,.72);
    text-wrap: pretty;
}
.af-link {
    display: inline-block;
    margin-top: 30px; padding: 0 0 6px;
    background: none; border: 0; border-bottom: 1px solid rgba(201,168,106,.5);
    cursor: pointer; color: var(--gold);
}

/* Stacked, media first and edge to edge — the gutters the rest of the page keeps are what
   would make a phone-width film look like a thumbnail. The negative margins undo the
   section's own padding rather than fighting it from a wider ancestor. */
@media (max-width: 900px) {
    .af-grid { grid-template-columns: 1fr; gap: 34px; }
    .af-frame {
        grid-column: 1; --af-open: 1;
        margin-inline: calc(var(--af-bleed) * -1);
        width: calc(100% + var(--af-bleed) * 2);
    }
    .af-copy { grid-column: 1; }
}

/* ============================================================
   The film modal. Poster-first is a standing rule, so the surface ships empty: the iframe
   is created on open and destroyed on close by deferred-embed, and nothing is requested
   from YouTube until a reader asks for the film.
   ============================================================ */
.af-modal {
    position: fixed; inset: 0; z-index: 90;
    display: flex; align-items: center; justify-content: center;
    padding: clamp(20px, 5vw, 64px);
    opacity: 0;
}
.af-modal[hidden] { display: none; }
.af-modal__scrim { position: absolute; inset: 0; background: rgba(15,14,12,.92); }
.af-modal__stage { position: relative; width: min(1100px, 100%); }
.af-modal__player { position: relative; aspect-ratio: 16 / 9; background: #000; }
.af-modal__frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.af-modal__close {
    position: absolute; top: clamp(14px, 3vw, 28px); right: clamp(14px, 3vw, 28px);
    width: 44px; height: 44px;
    display: flex; align-items: center; justify-content: center;
    font: 300 26px/1 var(--font-sans);
    color: var(--cream);
    background: none; border: 1px solid rgba(246,243,236,.28);
    cursor: pointer;
}
.af-modal__close:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; }
.af-modal[data-open] { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
    .af-modal { transition: opacity var(--duration-tint) var(--ease-drift); }
}

/* ============================================================
   Homepage Forbes band (Claude Design: "Forbes Exclusive Section").
   The word is the section: 300px Jost filled with a dimmed cream-to-ink gradient so it
   reads as texture and its baseline recedes into the page, with the claim laid over its
   lower third on an elliptical scrim. Choreography lives in forbes_scrub_controller.js,
   where the word is scrubbed to the scroll and the text plays to completion; the
   from-states here are gated on `.js-forbes`, which only that controller sets.
   ============================================================ */
/* No top padding, deliberately: the band's top edge is what the sticky range is keyed to, so
   any space here is both a visible gap under the hero and a delay before the lock. The hero
   ends and the band begins on the same pixel.

   THE HEIGHT IS THE SEAM BUDGET. One screen for the monument itself, then the phases it is
   scrolled through while stuck: the internal sequence, the rest gap, and — once `.fb--covered`
   says a sheet is coming — the overlap. Because height − 100svh == hold + rest + cover, the
   sticky box releases exactly --fb-cover after the lock, which is the pixel on which the sheet
   has covered --fb-cover% of the viewport. Set that token to 100 and release and full burial
   are the same pixel; set it lower, as it is now, and the monument hands off to the sheet
   part-covered and rides up with it. Either way there is no dead travel at either end.

   Nothing may put `overflow` on an ancestor of this section. That would make the ancestor the
   sticky element's scroll container and the seam would stop working — silently, with no error. */
.fb {
    position: relative;   /* z-index deliberately absent — see the stacking note on .hp-cover */
    background: var(--ink);
    padding: 0;           /* the old bottom padding is inside the sticky budget now */
    /* One screen for the monument, then the internal sequence and the rest gap. The overlap's
       own travel is added only when there is something to bury it — the covering section is
       conditional on published lifestyles, and on an install with none this would otherwise be
       a screen of empty scroll below a monument nothing arrives to cover. */
    height: calc((100 + var(--fb-hold) + var(--fb-rest)) * 1svh);

    /* How far the claim lockup hangs below the wordmark's optical centre. Declared on the
       section, not the stage, because `.fb-rule` is a sibling of the stage and has to carry
       the same number — see the stage's comment for why it exists at all. */
    --fb-lockup-drop: clamp(34px, 6.4vw, 100px);
    --fb-lockup-top: calc(74% + var(--fb-lockup-drop));
}
/* Set by the template when the covering section will render. See the height above. */
.fb--covered {
    height: calc((100 + var(--fb-hold) + var(--fb-rest) + var(--fb-cover)) * 1svh);
}
/* The stuck element, and native sticky rather than a ScrollTrigger pin. A pin injects a spacer
   whose height GSAP authors at refresh time and drops the element at the spacer's foot on
   release — so the monument would scroll away instead of being buried, and the ≥20svh gap would
   be a number measured after layout rather than one stated in the budget above. Sticky also
   avoids `position: fixed` on desktop and `pinType: transform` on touch, which are two different
   stacking behaviours for one seam and Safari's favourite repaint case.

   `height`, not `min-height`: a sticky box whose height depends on its content makes the release
   point a measured number, and the arithmetic above stops holding. The glow's 120vw bleed is
   clipped here. */
.fb-viewport {
    position: sticky;
    top: 0;
    height: 100svh;
    overflow: hidden;
    background: var(--ink);
}
/* What the depth push moves, and the reason it is a wrapper rather than either box around it.
   A transform on .fb would make it the containing block for its own sticky child, and sticky
   silently stops working. A transform on .fb-viewport would scale the clipping edge along with
   the contents, cropping rather than receding. This div is inside the clip and carries the whole
   composition, so `scale(.96)` reads as the monument going away from you. */
.fb-depth {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    padding: 0 clamp(20px, 5vw, 56px);
    transform-origin: 50% 50%;
}
/* The burial dim. A sibling above .fb-depth rather than a child of it, so the dim stays flat
   against the viewport instead of receding with the composition it is dimming. Ink rather than
   black: the page is closing over the monument, not a light going out.

   It rests at 0, which is what makes reduced motion and a failed JS chunk need no rule at all —
   both land on "no dim", which is the specified degraded state. */
.fb-dim {
    position: absolute; inset: 0;
    background: var(--ink);
    opacity: 0;
    pointer-events: none;
}
.fb-weave {
    position: absolute; inset: 0;
    background-image: repeating-linear-gradient(45deg, rgba(255,255,255,.014) 0 2px, transparent 2px 4px);
}
.fb-glow {
    position: absolute; left: 50%; top: 50%;
    width: min(1100px, 120vw); height: 520px;
    transform: translate(-50%, -50%);
    background: radial-gradient(ellipse at center, rgba(201,168,106,.10), rgba(201,168,106,0) 62%);
    pointer-events: none;
}
.fb-inner { position: relative; width: 100%; display: flex; flex-direction: column; align-items: center; }
/* Negative on purpose. The gap the eye reads is not this margin: the word's line box
   is centred in a stage taller than itself, and the box carries ascender slack above
   the caps — together ~7.3vw of empty space before the first pixel of "FORBES". The
   calc cancels that so the optical gap lands near 8px at any width: the eyebrow is
   specified to sit tight under the word, not to float above it. */
.fb-eyebrow { display: flex; align-items: center; gap: 16px; margin-bottom: clamp(-96px, calc(8px - 7.3vw), 8px); }
.fb-eyebrow-rule { width: 44px; height: 1px; background: linear-gradient(90deg, transparent, var(--gold)); }
.fb-eyebrow-rule--end { background: linear-gradient(90deg, var(--gold), transparent); }

/* The word and the headline share one stage so their overlap is a layout fact rather
   than a guessed offset. 300/360 at 1440 scales down in lockstep via the clamp.
   The caps of a 300px Jost word stand ~210px tall, centred, so the baseline sits at
   ~79% of the stage; the claim hangs `--fb-lockup-drop` below the 74% mark, which puts
   its block in the lower third of the word's bounding box rather than across the
   x-height, where a collision costs the most legibility.

   That drop is `--fb-lockup-drop`, declared on `.fb` and not written as a literal here,
   because three rules need the same number: the claim positions on it, the scrim centres
   on it, and the rule's top margin has to carry it or the claim's overflow out of the
   stage lands on the hairline. It scales — a fixed 100px is a third of the word at 1440
   and most of the stage at 375. */
.fb-stage {
    position: relative;
    width: min(1180px, 100%);
    height: clamp(160px, 25vw, 360px);
}
.fb-word {
    position: absolute; left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    margin: 0;
    font: 500 clamp(120px, 20.8vw, 300px)/1 var(--font-sans);
    letter-spacing: -.025em;
    white-space: nowrap;
    /* Pulled well below cream on purpose. At full luminance the word reads as the
       headline of the section and fights the claim laid over it; dimmed, it is the
       texture the claim sits on. The scrim below does the contrast work, not this. */
    background: linear-gradient(180deg, #cfc8ba 0%, #b3aa9b 30%, #6f6a60 62%, #171410 100%);
    -webkit-background-clip: text; background-clip: text;
    color: transparent; -webkit-text-fill-color: transparent;
}
/* The mechanism that guarantees the claim passes AA, at every scroll position and at
   every width. The wordmark's own gradient cannot be that mechanism: it is a fixed
   vertical ramp, so wherever the claim lands it meets whatever luminance happens to be
   there. An ellipse of the ground colour centred on the lockup meets the claim with the
   same value every time. Sits above the word and below the claim; the gold `.fb-glow`
   stays underneath both — ambience and legibility are different jobs. */
.fb-scrim {
    position: absolute; left: 50%; top: var(--fb-lockup-top);
    transform: translate(-50%, -50%);
    width: min(1180px, 116%); height: clamp(220px, 30vw, 420px);
    background: radial-gradient(ellipse at center,
        rgba(15,14,12,.72) 0%, rgba(15,14,12,.62) 42%, rgba(15,14,12,0) 72%);
    pointer-events: none;
}
.fb-headline {
    position: absolute; left: 50%; top: var(--fb-lockup-top);
    transform: translateX(-50%);
    margin: 0;
    width: min(980px, 100%);
    text-align: center;
    font: italic 300 clamp(30px, 4.2vw, 60px)/1.12 var(--font-display);
    color: var(--cream);
    text-wrap: balance;
    text-shadow: 0 2px 30px rgba(15,14,12,.8);
}
.fb-headline em { font-style: inherit; color: #d9b878; }
/* SplitText's line masks clip at the line box, and this face's descenders hang below it —
   without the headroom the reveal shears the tails off every g, j and y. The negative
   margin gives the padding back to the layout so the claim's line spacing is unchanged. */
.fb-headline .st-line { padding-bottom: .22em; margin-bottom: -.22em; }
.fb-rule {
    width: 1px; height: clamp(40px, 6vh, 72px);
    margin: calc(clamp(24px, 3.4vh, 56px) + var(--fb-lockup-drop)) 0 clamp(22px, 3.2vh, 40px);
    background: linear-gradient(180deg, rgba(201,168,106,.7), rgba(201,168,106,.05));
    transform-origin: top center;
}
.fb-copy {
    margin: 0; max-width: 560px; text-align: center;
    font: 300 15.5px/1.85 var(--font-sans);
    color: rgba(246,243,236,.82);
    text-wrap: pretty;
}
.fb-cta {
    margin-top: clamp(20px, 3vh, 34px);
    font: 400 10.5px/1 var(--font-sans);
    letter-spacing: .22em; text-transform: uppercase;
    border: 1px solid rgba(201,168,106,.55);
    padding: 17px 26px;
}

/* Hidden from first paint so nothing flashes before the lazy controller mounts;
   each matches the timeline's from-state exactly. */
@media (prefers-reduced-motion: no-preference) {
    .js-forbes .fb-glow,
    .js-forbes .fb-scrim,
    .js-forbes .fb-copy,
    .js-forbes .fb-cta { opacity: 0; visibility: hidden; }
    /* The claim is hidden by its own line masks, not by opacity — but the masks do not
       exist until SplitText has cut them, and an unsplit claim would be legible for that
       window. Held back on the attribute the controller sets the frame the masks land,
       and never on opacity: the reveal writes transform only. */
    .js-forbes .fb-headline:not([data-split-ready]) { visibility: hidden; }
    .js-forbes .fb-word { opacity: 0; visibility: hidden; transform: translate(-50%, calc(-50% + 140px)); }
    .js-forbes .fb-rule { transform: scaleY(0); }

    /* Promoted only while something is moving it. During burial that is the *wrapper*, never
       the word: promoting a 300px gradient clipped to text while an ancestor's scale animates
       forces the promoted layer to re-raster every frame, which is worse than not promoting it
       at all. Promote the thing that moves. The class is toggled by the burial trigger's own
       onToggle, so the promotion lasts exactly as long as the scrub.

       The word gets no promotion because the word no longer moves once it has locked — see the
       approach-and-burial note in forbes_scrub_controller.js. */
    .fb--burying .fb-depth { will-change: transform; }
}

/* Dormant: the sheet has finished covering the monument (scroll-architecture PRD §3.4).
   `visibility: hidden` is the strongest lever available without touching layout — it stops the
   background-clip:text gradient being rastered at all, and takes the subtree out of hit-testing
   and the tab order in one property. Safe only because the class is set exclusively while an
   opaque sheet is over the top of it, which is the one moment nobody can tell. */
.fb--dormant .fb-viewport { visibility: hidden; }

/* The sticky cover itself survives reduced motion — it is native scrolling, not motion, and the
   PRD keeps it deliberately. What goes is everything the scroll ranges existed to give room to:
   with no scrub and no depth push, the hold and the rest gap are dead travel, so the section
   collapses to one screen plus the sheet's own approach. */
@media (prefers-reduced-motion: reduce) {
    /* A floor, not a change: both already rest at 0 above. Kept so that raising either of them
       for the motion path cannot silently add dead scroll to the reduced-motion one, where
       there is no scrub and no depth push for that travel to give room to. */
    .fb { --fb-hold: 0; --fb-rest: 0; }
}

/* ============================================================
   THE COVER SEAM — the sheet.

   Exactly one section on the page carries this (scroll-architecture PRD §3, AC-2). It scrolls
   over the Forbes monument, which is stuck at top: 0 behind it, burying the wordmark like a page
   turning. One cover, used once, reads as hierarchy; a second one would read as a gimmick.

   Three declarations here are load-bearing, and each fails in a way that is hard to attribute:

   THE NEGATIVE MARGIN IS THE MECHANISM. Exactly --fb-cover, pulling this section's box up into
   the foot of .fb's height. That overlap is the whole of what keeps the monument stuck while the
   sheet travels over it, and its size is how much of the monument the sheet gets to cover before
   the sticky box releases — see the token's comment. Without the margin there is no overlap at
   all: .fb releases first and Forbes simply scrolls away with nothing over it.

   `position: relative` WITH NO Z-INDEX. Positioned puts this in the same paint step as the
   sticky monument, and being later in tree order is what wins — no z-index needed, and that is
   the point. An actual z-index here would form a stacking context around everything nested in
   this section and every section after it in the flow; the film modal is `position: fixed;
   z-index: 90` precisely so it outranks the nav at 60, and scoped inside a context of 1 it would
   lose to it. The nav would paint over an open film modal. (And -1 is not the escape hatch: .lux
   is a div, not <body>, so its ink ground is an ordinary element background — a negative index
   would put the monument behind *that* and it would vanish outright.)

   AN OPAQUE GROUND, for the same reason. The monument paints in the positioned step, above .lux's
   background, so a transparent sheet shows it straight through. This one declaration is what
   satisfies AC-5, not the stacking order.

   One more rule, which lives in the template because CSS cannot state it: the element carrying
   this class must never also carry `.reveal`. gsap-reveal animates `.reveal` from opacity 0, and
   a sheet that is transparent for the first 900ms of the seam is exactly the bleed-through this
   section exists to prevent. Its children reveal; it does not.
   ============================================================ */
.hp-cover {
    position: relative;
    margin-top: calc(var(--fb-cover, 100) * -1svh);
    background: var(--ink);
    /* The sheet's own edge, and the only ornament the seam gets. */
    border-top: 1px solid rgba(201, 168, 106, .45);
    /* The arriving page leads with type, not whitespace: its first screen is composed as a
       cover. Also guarantees the sheet is at least as tall as the viewport, which is what lets
       the dormancy observer treat "its top passed the fold" as "the monument is covered". */
    min-height: 100svh;
    padding-top: clamp(72px, 9vw, 132px);
}
/* No burial to compose for, and .fb has collapsed to one screen plus the approach. */
@media (prefers-reduced-motion: reduce) { .hp-cover { min-height: 0; } }

/* ============================================================
   Shared marketing-page primitives — the hero band and the closing CTA.

   Named .pl-* because they started life on the service pillar pages, which have
   all since graduated to their own templates. The prefix is now historical: the
   five pages using these are /global-exposure, /luxury-marketing,
   /concierge-service, /sellers and /about. Kept rather than renamed because the
   name is load-bearing in five templates and means nothing beyond "the standard
   hero and CTA"; add to it rather than forking a second set.
   ============================================================ */
.pl-hero { height: min(52vh, 460px); min-height: 340px; }

@media (max-width: 760px) {
    .pl-cta { flex-direction: column; align-items: flex-start !important; }
}

/* Same gold-on-gold hover fix as .ar-btn-solid. */
.lux a.pl-btn-solid:hover { color: var(--ink-2); background: var(--gold-2); }

/* ============================================================
   /global-exposure — the Forbes Global Properties proof page.

   Two charts, both built from one primitive: a real <table> with a decorative
   bar layered behind the numeral. HTML bars rather than SVG because the numbers
   stay selectable text, the source lines stay ordinary flow content, and there
   are no curves or axes here that would repay an <svg>.

   As everywhere in this file, layout that inline styles can express stays
   inline on the elements. What lives here is the chart geometry (which needs
   position/z-index interplay), the responsive collapses, and the .js
   from-states for chart_bars_controller.js.
   ============================================================ */

/* Taller than .pl-hero: this is the flagship proof page, not a stub, and the
   lockup needs room beneath the subline. */
.ge-hero { height: min(76vh, 680px); min-height: 460px; }

/* The territory claim runs long for an eyebrow. Letting it wrap at .4em
   tracking would strand two words on a second line, so it steps down a notch
   and is allowed its own line-height. */
.ge-hero-eyebrow {
    max-width: 62ch;
    font-size: 9.5px;
    line-height: 1.9;
    letter-spacing: .34em;
}

/* The co-brand lockup. Set in type, hairline-separated — the supplied marks are
   not in the repo, and FGP's governance asks for a quiet composition rather
   than two logos butted together. */
.ge-lockup {
    display: flex;
    align-items: center;
    gap: clamp(12px, 1.6vw, 20px);
    margin-top: clamp(28px, 3.4vw, 42px);
    padding-top: clamp(20px, 2.4vw, 28px);
    border-top: 1px solid rgba(201, 168, 106, .28);
    max-width: 560px;
    font-size: clamp(14px, 1.5vw, 18px);
    letter-spacing: .06em;
    color: rgba(246, 243, 236, .8);
}
.ge-lockup-x { color: var(--gold); font-size: 13px; }

/* The attribution line under every band and chart. A class rather than inline
   because it appears six times and has to look identical each time — the page
   makes measured competitive claims, and the source is part of the claim. */
.ge-source {
    margin: 22px 0 0;
    font: 300 10px/1.7 var(--font-sans);
    letter-spacing: .16em;
    text-transform: uppercase;
    color: rgba(246, 243, 236, .38);
}
.ge-note {
    margin: 26px 0 0;
    max-width: 64ch;
    font: 300 13px/1.8 var(--font-sans);
    color: rgba(246, 243, 236, .55);
}

/* Clipped, not display:none — display:none would take these out of the
   accessibility tree, and they exist solely for it. The visible headings are
   the <h2>s above each chart; repeating them would make a screen reader say
   everything twice. */
.ge-a11y {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.ge-pull {
    margin: 34px 0 0;
    padding-top: 26px;
    border-top: 1px solid rgba(201, 168, 106, .22);
    font-size: clamp(22px, 2.6vw, 30px);
    line-height: 1.4;
    color: var(--gold-2);
    text-wrap: pretty;
}

/* ---- Stat bands ----
   Cells and type come from .ab-stat* on /about; only the rule logic differs,
   because this band is eight cells over two rows rather than four over one.
   Without the nth-child(n+5) rule the second row would float ruleless. */
.ge-stats {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    border-top: 1px solid rgba(201, 168, 106, .22);
    border-bottom: 1px solid rgba(201, 168, 106, .22);
}
.ge-stats .ab-stat + .ab-stat { border-left: 1px solid rgba(201, 168, 106, .16); }
.ge-stats .ab-stat:nth-child(4n + 1) { border-left: 0; }
.ge-stats .ab-stat:nth-child(n + 5) { border-top: 1px solid rgba(201, 168, 106, .16); }

/* The network band is three cells, so the 4n rule above would fire on the wrong
   cell and never wrap. Reset it rather than parameterise. */
.ge-stats-3 { grid-template-columns: repeat(3, 1fr); }
.ge-stats-3 .ab-stat:nth-child(4n + 1) { border-left: 1px solid rgba(201, 168, 106, .16); }
.ge-stats-3 .ab-stat:first-child { border-left: 0; }

/* ---- The hairline grid ----
   area.html.twig does this inline (.ar-market): a 1px grid gap over a gold
   ground, each cell painting its own background so the gap reads as a rule. It
   earns a class here because this page uses it twice, and because the inset
   shadow hairlines the outer edge without a border that would double the gap. */
.ge-hairgrid {
    display: grid;
    gap: 1px;
    background: rgba(201, 168, 106, .2);
    box-shadow: inset 0 0 0 1px rgba(201, 168, 106, .2);
}
.ge-hairgrid > * { background: #141210; }

.ge-audience { grid-template-columns: repeat(3, 1fr); }
.ge-audience-cell { padding: clamp(28px, 3vw, 42px) clamp(22px, 2.4vw, 32px); }
.ge-audience-v {
    font-size: clamp(38px, 4.6vw, 58px);
    line-height: 1;
    color: var(--gold);
}
.ge-audience-k {
    margin: 16px 0 0;
    max-width: 26ch;
    font: 300 14px/1.75 var(--font-sans);
    color: rgba(246, 243, 236, .62);
    text-wrap: pretty;
}

.ge-regions { grid-template-columns: repeat(4, 1fr); }
.ge-region { padding: clamp(26px, 2.6vw, 34px) clamp(20px, 2.2vw, 28px); }
.ge-region-list {
    margin: 18px 0 0;
    padding: 0;
    list-style: none;
    font: 300 13.5px/2 var(--font-sans);
    color: rgba(246, 243, 236, .62);
}

/* ---- Chart primitive, shared by both tables ----
   The track is absolutely positioned so the bar sits BEHIND the numeral rather
   than beside it: the numeral is the data, the bar is the impression, and a
   dedicated bar column would halve the space the numerals need on a phone.
   z-index via position rather than flex order, because these are table cells
   and making them flex containers would break column alignment. */
.ge-bar-track {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 8px;
    height: 10px;
    display: block;
}
.ge-bar-fill {
    display: block;
    height: 100%;
    background: linear-gradient(90deg, var(--gold-deep), var(--gold));
}
/* Hollow rather than filled. The comparison IS filled-versus-outline, which
   means the second series never needs a second colour — and this palette has
   three golds and no second colour to give it. */
.ge-bar-b {
    background: none;
    box-shadow: inset 0 0 0 1px rgba(201, 168, 106, .38);
}

/* ---- Share of voice ---- */
.ge-sov {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
}
.ge-sov-row > * {
    position: relative;
    padding: 26px 0 30px;
    border-bottom: 1px solid rgba(201, 168, 106, .16);
    text-align: left;
    font-weight: 300;
}
.ge-sov-row > :first-child {
    font-size: clamp(16px, 1.5vw, 20px);
    line-height: 1.3;
    color: rgba(246, 243, 236, .74);
}
.ge-sov-val {
    width: 46%;
    padding-left: clamp(20px, 4vw, 56px);
}
.ge-sov-num {
    position: relative;   /* lifts the numeral above .ge-bar-track */
    display: block;
    font-size: 22px;
    line-height: 1;
    color: #d9b878;
}
/* The lead row at stat-band scale. The whole chart is one comparison, so the
   winning row is allowed to be four times the size of the others — that
   proportion IS the finding. */
.ge-sov-row.is-lead > :first-child {
    font-size: clamp(21px, 2.2vw, 29px);
    color: var(--cream);
}
.ge-sov-row.is-lead .ge-sov-num {
    font-size: clamp(34px, 4.4vw, 54px);
    color: var(--gold);
}
.ge-sov-row.is-lead .ge-bar-track { height: 16px; bottom: 12px; }
/* The callout below the table states the argument instead of plotting it: the
   two shares side by side at display scale, between hairline rules. Forbes
   keeps the gold; the combined figure is cream — same grammar as the bars,
   translated into type. */
.ge-sov-callout {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: clamp(20px, 4vw, 56px);
    margin-top: clamp(40px, 5vw, 64px);
    padding: clamp(34px, 4vw, 52px) 0;
    border-top: 1px solid rgba(201, 168, 106, .4);
    border-bottom: 1px solid rgba(201, 168, 106, .16);
}
.ge-sov-co-cell {
    display: flex;
    flex-direction: column;
    gap: 14px;
}
.ge-sov-co-cell:last-child { text-align: right; }
.ge-sov-co-num {
    font-size: clamp(44px, 6.5vw, 84px);
    line-height: 1;
    color: var(--gold);
}
.ge-sov-co-num-b { color: rgba(246, 243, 236, .55); }
.ge-sov-co-label {
    font: 300 13px/1.5 var(--font-sans);
    letter-spacing: .04em;
    color: rgba(246, 243, 236, .74);
}
.ge-sov-co-vs { color: rgba(246, 243, 236, .45); }

/* ---- Age bands ---- */
/* The legend is the table's own header row, so each series name sits directly
   over its column of bars. With table-layout fixed, these cells set the column
   widths — keep the first-column width in step with .ge-bars-k. */
.ge-bars-head th {
    width: auto;
    padding: 0 clamp(16px, 3vw, 40px) 14px 0;
    border-bottom: 1px solid rgba(201, 168, 106, .4);
    text-align: left;
    font: 300 10px/1 var(--font-sans);
    letter-spacing: .2em;
    text-transform: uppercase;
    color: rgba(246, 243, 236, .6);
}
.ge-bars-head th:first-child { width: 16%; }
.ge-bars-unit { color: rgba(246, 243, 236, .38); }
/* Swatches match the two bar treatments exactly: solid gold, and hollow. */
.ge-key-a::before,
.ge-key-b::before {
    content: '';
    display: inline-block;
    width: 18px;
    height: 10px;
    margin-right: 10px;
    vertical-align: middle;
}
.ge-key-a::before { background: var(--gold); }
.ge-key-b::before { box-shadow: inset 0 0 0 1px rgba(201, 168, 106, .38); }

.ge-bars {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
}
.ge-bars-k {
    width: 16%;
    text-align: left;
    font-weight: 300;
    font-size: 20px;
    color: rgba(246, 243, 236, .6);
}
.ge-bars-cell {
    position: relative;
    padding: 30px clamp(16px, 3vw, 40px) 30px 0;
    border-bottom: 1px solid rgba(201, 168, 106, .16);
}
.ge-bars-num {
    position: relative;
    font-size: 26px;
    line-height: 1;
    color: var(--cream);
}

/* ---- Services ledger ----
   .ar-amenity's geometry minus its fourth column: there is no numeric value to
   right-align here, and a decorative fourth column would be exactly the
   dashboard instinct this page argues against. */
.ge-service {
    display: grid;
    grid-template-columns: 52px 1.05fr 1.5fr;
    gap: 36px;
    align-items: baseline;
    padding: 30px 0;
    border-bottom: 1px solid rgba(201, 168, 106, .16);
}
.ge-service-n { font-size: 20px; line-height: 1; color: var(--gold-deep); }
.ge-service-name {
    font-size: clamp(21px, 2.1vw, 27px);
    line-height: 1.25;
    color: var(--cream);
}
.ge-service-kind {
    margin-top: 9px;
    font: 300 10px/1.5 var(--font-sans);
    letter-spacing: .22em;
    text-transform: uppercase;
    color: rgba(246, 243, 236, .5);
}
.ge-service-body {
    margin: 0;
    font: 300 14.5px/1.8 var(--font-sans);
    color: rgba(246, 243, 236, .72);
    text-wrap: pretty;
}

/* ---- The Private Office band ----
   Full-bleed change of ground, borrowing /about's numbered .ab-value list for
   the four points so the shift reads as register, not as new vocabulary. */
.ge-po {
    background: var(--ink-2);
    margin-top: 132px;
    border-top: 1px solid rgba(201, 168, 106, .18);
    border-bottom: 1px solid rgba(201, 168, 106, .18);
}
.ge-po-points {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: clamp(26px, 3.2vw, 52px) clamp(40px, 6vw, 96px);
}

.ge-secondary {
    display: flex;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;
    margin-top: 28px;
    font: 300 10.5px/1 var(--font-sans);
    letter-spacing: .2em;
    text-transform: uppercase;
    color: rgba(246, 243, 236, .3);
}

/* ---- Enhanced state ----
   Matches chart_bars_controller's from-state exactly, so the bars start at zero
   instead of flashing full-width before the lazy controller loads. Behind BOTH
   `.js` and no-preference: a reduced-motion visitor, or one whose JS never
   arrives, must never be left looking at an empty chart. */
@media (prefers-reduced-motion: no-preference) {
    .js .ge-bar-fill {
        transform: scaleX(0);
        transform-origin: left center;
        will-change: transform;
    }
}
@media (prefers-reduced-motion: reduce) {
    .ge-bar-fill { transform: none; }
}

/* ---- Responsive ---- */
@media (max-width: 1000px) {
    .ge-stats { grid-template-columns: repeat(2, 1fr); }
    .ge-stats .ab-stat:nth-child(odd) { border-left: 0; }
    .ge-stats .ab-stat:nth-child(even) { border-left: 1px solid rgba(201, 168, 106, .16); }
    .ge-stats .ab-stat:nth-child(n + 3) { border-top: 1px solid rgba(201, 168, 106, .16); }
    .ge-audience { grid-template-columns: repeat(2, 1fr); }
    .ge-regions { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 900px) {
    /* Same collapse as .ar-amenity: the body prose drops under the name rather
       than squeezing into a four-word column. */
    .ge-service { grid-template-columns: 40px 1fr; gap: 10px 20px; }
    .ge-service-body { grid-column: 2; }
    /* Also used by /contact, which is the same editorial-column-plus-panel
       shape and wants the same collapse point — earlier than .ab-split's 760px,
       because a form squeezed into 45% of a tablet is unusable. */
    .ge-split { grid-template-columns: 1fr !important; }
    .ge-po-points { grid-template-columns: 1fr; }
}
@media (max-width: 700px) {
    /* Below this the value column is narrower than the numeral it holds, so the
       bar would be shorter than the number it is scaling. Tighten both. */
    .ge-sov-val { width: 40%; padding-left: 18px; }
    .ge-sov-num { font-size: 18px; }
    .ge-sov-row.is-lead .ge-sov-num { font-size: clamp(28px, 8vw, 38px); }
    .ge-sov-row > :first-child { font-size: 14.5px; }
    .ge-sov-callout { gap: 14px; }
    .ge-sov-co-num { font-size: clamp(32px, 9vw, 44px); }
    .ge-sov-co-label { font-size: 11.5px; }
    .ge-bars-k { width: 22%; font-size: 16px; }
    .ge-bars-head th:first-child { width: 22%; }
    .ge-bars-head th { padding-right: 14px; }
    .ge-bars-num { font-size: 19px; }
    .ge-bars-cell { padding: 24px 14px 26px 0; }
    .ge-po { margin-top: 96px; }
}
@media (max-width: 560px) {
    .ge-stats,
    .ge-stats-3 { grid-template-columns: 1fr; }
    .ge-stats .ab-stat,
    .ge-stats-3 .ab-stat { border-left: 0 !important; border-top: 1px solid rgba(201, 168, 106, .16); }
    .ge-stats .ab-stat:first-child,
    .ge-stats-3 .ab-stat:first-child { border-top: 0; }
    .ge-audience,
    .ge-regions { grid-template-columns: 1fr; }
    .ge-lockup { flex-wrap: wrap; gap: 6px 12px; }
}

/* ============================================================
   /about — the firm: stat band, team portraits, values list.

   No controller of its own; the page uses gsap-reveal like the other editorial
   pages, so everything here is static layout.
   ============================================================ */
.ab-stats {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    border-top: 1px solid rgba(201, 168, 106, .22);
    border-bottom: 1px solid rgba(201, 168, 106, .22);
}
.ab-stat {
    padding: clamp(26px, 3vw, 40px) clamp(16px, 2vw, 28px);
}
/* Rules between, not around — the band's own border does the outside. */
.ab-stat + .ab-stat { border-left: 1px solid rgba(201, 168, 106, .16); }
.ab-stat-v {
    font-size: clamp(34px, 4.4vw, 54px);
    line-height: 1;
    color: var(--gold);
}
.ab-stat-k {
    margin-top: 14px;
    font: 300 10.5px/1.6 var(--font-sans);
    letter-spacing: .2em;
    text-transform: uppercase;
    color: rgba(246, 243, 236, .5);
}

.ab-team {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: clamp(24px, 3.4vw, 52px);
}
.ab-portrait {
    position: relative;
    aspect-ratio: 4 / 5;
    overflow: hidden;
    background: var(--ink-2);
}
.ab-portrait img {
    display: block;
    width: 100%;
    height: 100%;
    -o-object-fit: cover;
       object-fit: cover;
    filter: grayscale(1);
    transition: transform 1.6s cubic-bezier(.2, .7, .2, 1), filter 1.2s ease;
}
.ab-member:hover .ab-portrait img { transform: scale(1.04); filter: grayscale(0); }
/* Overlay rather than an inset shadow so it sits above the image edge. */
.ab-ring {
    position: absolute;
    inset: 0;
    border: 1px solid rgba(201, 168, 106, .3);
    pointer-events: none;
}
.ab-name {
    margin: clamp(16px, 1.8vw, 24px) 0 0;
    font-size: clamp(20px, 2vw, 26px);
    line-height: 1.2;
    color: var(--cream);
}
.ab-role {
    margin-top: 9px;
    font: 400 10px/1.6 var(--font-sans);
    letter-spacing: .22em;
    text-transform: uppercase;
    color: var(--gold-deep);
}
.ab-bio {
    margin: 14px 0 0;
    font: 300 13.5px/1.8 var(--font-sans);
    color: rgba(246, 243, 236, .6);
    text-wrap: pretty;
}

.ab-values {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: clamp(28px, 3.4vw, 56px) clamp(40px, 6vw, 96px);
}
.ab-value {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: clamp(18px, 2vw, 28px);
    padding-top: 26px;
    border-top: 1px solid rgba(201, 168, 106, .18);
}
.ab-value-n {
    font-size: 20px;
    line-height: 1;
    color: var(--gold-deep);
}
.ab-value-name {
    font-size: clamp(19px, 1.9vw, 24px);
    line-height: 1.25;
    color: var(--cream);
}
.ab-value-body {
    margin: 11px 0 0;
    max-width: 46ch;
    font: 300 14px/1.85 var(--font-sans);
    color: rgba(246, 243, 236, .62);
    text-wrap: pretty;
}

@media (prefers-reduced-motion: reduce) {
    .ab-portrait img,
    .ab-member:hover .ab-portrait img { transition: none; transform: none; }
}

/* ---- Responsive ---- */
@media (max-width: 1000px) {
    /* Two columns, so the stat rules must reset per row: every third cell
       starts a new row and drops its left border. */
    .ab-stats { grid-template-columns: repeat(2, 1fr); }
    .ab-stat:nth-child(odd) { border-left: 0; }
    .ab-stat:nth-child(n + 3) { border-top: 1px solid rgba(201, 168, 106, .16); }
    .ab-team { grid-template-columns: repeat(2, 1fr); }
    .ab-team > :last-child {
        grid-column: 1 / -1;
        max-width: calc(50% - clamp(12px, 1.7vw, 26px));
    }
}
@media (max-width: 760px) {
    .ab-split { grid-template-columns: 1fr !important; }
    .ab-values { grid-template-columns: 1fr; }
}
@media (max-width: 560px) {
    .ab-stats { grid-template-columns: 1fr; }
    .ab-stat + .ab-stat { border-left: 0; border-top: 1px solid rgba(201, 168, 106, .16); }
    .ab-team { grid-template-columns: 1fr; }
    .ab-team > :last-child { max-width: none; }
}

/* ============================================================
   /sellers — the marketing menu (marketing_menu_controller.js).

   Eighteen rows on the left drive a stack of photographs in a sticky panel on
   the right. Hovering a row sweeps its image in from right to left with
   clip-path; images never reverse, they accumulate, so the panel fills up with
   the campaign as you read down the list.

   Base state is the finished, readable state: every feature and benefit is
   plain server-rendered text, and the panel simply shows the last image. That
   is what no-JS gets. Only the hidden clip-path state is behind `.js`.
   Layout is in classes, not inline, so the media queries need no !important.
   ============================================================ */
.mm-grid {
    display: grid;
    grid-template-columns: 1fr .86fr;
    gap: clamp(40px, 5vw, 88px);
    align-items: start;
}

/* ---- The photograph stack ---- */
.mm-panel {
    position: sticky;
    top: clamp(80px, 12vh, 140px);
    aspect-ratio: 4 / 5;
    overflow: hidden;
    background: var(--ink-2);
    /* Safe as an inset shadow: the panel itself is never clipped — only the
       images inside it are — so the ring stays rectangular. */
    box-shadow: inset 0 0 0 1px rgba(201, 168, 106, .3);
}
.mm-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    -o-object-fit: cover;
       object-fit: cover;
}

/* Gated on `.js` ONLY — deliberately not inside a prefers-reduced-motion query.
   A reduced-motion user still needs the images to start hidden, or all eighteen
   stack visible at once; the controller swaps them instantly instead of tweening.
   With no JS at all nothing is hidden and the last image in DOM order shows,
   which is a fine baseline. */
.js .mm-img { clip-path: inset(0% 0% 0% 100%); }

/* ---- Phase headings ---- */
.mm-phase {
    display: flex;
    align-items: center;
    gap: 20px;
    padding: 46px 0 18px;
}
.mm-phase:first-child { padding-top: 0; }
.mm-phase-rule {
    flex: 1;
    height: 1px;
    background: linear-gradient(90deg, rgba(201, 168, 106, .55), rgba(201, 168, 106, .04));
}

/* ---- Rows ---- */
.mm-row {
    display: grid;
    grid-template-columns: 46px 1fr;
    gap: 24px;
    padding: 26px 0;
    border-bottom: 1px solid rgba(201, 168, 106, .16);
    transition: transform .5s cubic-bezier(.2, .7, .2, 1);
}
.mm-num {
    font-size: 22px;
    line-height: 1.3;
    color: rgba(201, 168, 106, .45);
    transition: color .4s ease;
}
.mm-name {
    font-size: clamp(23px, 2.2vw, 29px);
    line-height: 1.22;
    color: var(--cream);
    transition: color .4s ease;
}
.mm-kind {
    margin-top: 9px;
    font: 300 10px/1 var(--font-sans);
    letter-spacing: .22em;
    text-transform: uppercase;
    color: rgba(246, 243, 236, .5);
}
.mm-benefit {
    margin: 14px 0 0;
    max-width: 54ch;
    font: 300 14.5px/1.8 var(--font-sans);
    color: rgba(246, 243, 236, .72);
    text-wrap: pretty;
}
/* The Forbes row: wording has to come from the agent's affiliation agreement,
   so the placeholder is visible on the page rather than buried in a comment. */
.mm-note {
    margin-top: 12px;
    font: 300 10px/1.7 var(--font-sans);
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--gold-deep);
}

.mm-row.is-active { transform: translateX(6px); }
.mm-row.is-active .mm-name { color: var(--gold-2); }
.mm-row.is-active .mm-num { color: var(--gold); }

/* ---- Responsive ---- */
@media (max-width: 900px) {
    /* Panel above the list and pinned to the top: below this width the rows are
       activated by scroll position, not hover, so the image has to stay in view. */
    .mm-grid { grid-template-columns: 1fr; gap: 0; }
    .mm-panel {
        order: -1;
        aspect-ratio: 3 / 2;
        top: 0;
        margin-bottom: 30px;
    }
    .mm-thesis { grid-template-columns: 1fr !important; }
}
@media (max-width: 560px) {
    .mm-row { grid-template-columns: 34px 1fr; gap: 16px; }
    .mm-num { font-size: 18px; }
    .mm-benefit { font-size: 14px; }
}

@media (prefers-reduced-motion: reduce) {
    .mm-row, .mm-num, .mm-name { transition: none; }
    .mm-row.is-active { transform: none; }
}

/* ============================================================
   Homepage featured-area cards. One per published city or region, so the
   count is whatever the database holds — the grid is auto-fit and the card
   only has to know how to behave at any width.

   Layout stays inline in the template alongside the rest of that section;
   this carries the hover, which an inline style cannot express.
   ============================================================ */
.ha-card img { transition: transform 1.6s cubic-bezier(.2, .7, .2, 1); }
.ha-card:hover img { transform: scale(1.05); }
.ha-name { transition: color .4s ease; }

/* `.lux a` paints every link gold; these are photo cards. */
.lux a.ha-card,
.lux a.ha-card:hover { color: inherit; }
.lux a.ha-card:hover .ha-name { color: var(--gold-2); }

@media (prefers-reduced-motion: reduce) {
    .ha-card img, .ha-card:hover img { transition: none; transform: none; }
    .ha-name { transition: none; }
}

/* ============================================================
   THE GUIDE INDEXES — the homepage's lifestyle and area routing sections.

   One component, rendered twice (templates/public/_guide_index.html.twig). Both are
   deliberately *light*: their job is to route to the lifestyle and area pages, not to hold
   content. Large serif rows, hairline separators, letterspaced metadata, and a photograph that
   arrives beside the index on hover.

   AN INDEX, NOT A GRID, and that is the durable part. Seven areas break a five-column row and
   nine break a four; a list has no arithmetic to break at any count, which is why both sections
   compose identically at four rows and at fifteen.

   The two read as siblings rather than repetition by scale, not by decoration: lifestyles take
   ordinals and the larger row (`.gd--lg`), areas are the denser list (`.gd--sm`) with no
   ordinals and a landscape frame where lifestyles take a portrait one.

   This file owns the layout, the rows' hover and focus states, and the panel's geometry;
   guide_index_controller.js owns only the crossfade. That split is what makes the rows work
   before the JS chunk lands, and keyboard-operable for free — nothing here hides content behind
   a flag the controller has to set.
   ============================================================ */

.gd-grid {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    gap: clamp(24px, 3vw, 56px);
    align-items: start;
}
.gd-index { grid-column: 1 / span 7; }
/* The photograph's column. Reserved at every width whether or not a picture is showing, so the
   index never reflows when one arrives — the panel is in flow, unlike the cursor-following
   version this replaced, and an unreserved column would be a layout shift on first hover. */
.gd-media {
    grid-column: 9 / span 4;
    /* `stretch` against the grid's own `align-items: start`, and it is what makes the panel
       sticky at all: a sticky element travels inside its containing block, which here is this
       column. Left to shrink-wrap the panel, the column would be exactly as tall as the thing
       inside it, there would be nowhere to travel, and the photograph would simply scroll away
       with the rows — which looks like sticky "not working" and is really a zero-height range. */
    align-self: stretch;
}

/* ---- the index row ---- */

.gd-row {
    display: flex;
    align-items: center;
    gap: clamp(16px, 2vw, 28px);
    padding: clamp(22px, 2.6vw, 34px) 0;
    border-bottom: 1px solid rgba(246, 243, 236, .10);
    text-decoration: none;
}
/* The rows must tile their column exactly — the separator is a border on the anchor, never a
   margin between anchors. The controller reads "the pointer left a row and did not enter
   another" as "left the index"; a gap between rows would make that true for a single frame on
   every crossing, and the photograph would flicker its way down the list. */
.gd-index .gd-row:first-of-type { border-top: 1px solid rgba(246, 243, 236, .10); }

/* `.lux a` paints every public link gold. A row is a line of type, not a link. */
.lux a.gd-row,
.lux a.gd-row:hover { color: inherit; }

.gd-ordinal {
    flex: 0 0 auto;
    min-width: 2.5em;
    color: var(--gold-deep);
    letter-spacing: .34em;
}
.gd-row-copy { flex: 1 1 auto; min-width: 0; }

.gd-name {
    display: block;
    font-family: var(--font-display);
    font-size: clamp(26px, 3vw, 42px);
    line-height: 1.08;
    color: rgba(246, 243, 236, .88);
    transition: color var(--duration-tint) var(--ease-drift);
}
/* Areas are the denser list, so the same row at a smaller scale. */
.gd--sm .gd-name { font-size: clamp(20px, 2.2vw, 30px); }

.gd-meta {
    display: block;
    margin-top: 10px;
    letter-spacing: .24em;
    color: rgba(246, 243, 236, .6);
    text-wrap: pretty;
    transition: color var(--duration-tint) var(--ease-drift);
}

.gd-arrow {
    flex: 0 0 auto;
    font: 400 16px/1 var(--font-sans);
    color: var(--gold-deep);
    transition: transform var(--duration-move) var(--ease-drift),
                color var(--duration-tint) var(--ease-drift);
}

/* Colour and transform only. A background fill on hover would turn the index back into a stack
   of cards, which is the shape these sections exist to leave behind. */
.gd-row:is(:hover, :focus-visible) .gd-name  { color: var(--gold-2); }
.gd-row:is(:hover, :focus-visible) .gd-meta  { color: rgba(246, 243, 236, .78); }
.gd-row:is(:hover, :focus-visible) .gd-arrow { transform: translateX(5px); color: var(--gold); }
.gd-row:focus-visible { outline: 1px solid var(--gold-2); outline-offset: 4px; }

/* Wherever a cursor exists the panel is the picture, so the inline thumbnail is for touch only
   — and the media query states the controller's gate from the other direction, so the two
   cannot disagree about which world they are in. */
.gd-thumb { display: none; flex: 0 0 auto; width: 72px; height: 96px; -o-object-fit: cover; object-fit: cover; }
@media (hover: none), (pointer: coarse) { .gd-thumb { display: block; } }

/* ---- the photograph ---- */

/* Sticky rather than fixed, and in flow rather than following the cursor. The cursor-following
   version was built and removed: the preview sat on the pointer, and the pointer is by
   definition on the row you are trying to read. Held at the nav line, the picture is beside the
   row instead of over it, and being in normal flow means an ancestor transform (gsap-reveal
   writes one on every `.reveal`) cannot turn it into a containing-block puzzle. */
.gd-panel {
    display: none;
    position: sticky;
    top: calc(var(--nav-h) + clamp(24px, 4vw, 56px));
    aspect-ratio: 3 / 4;
    overflow: hidden;
    background: var(--ink-2);
    /* Hidden only until the template can seed it. `.is-open` says a photograph was rendered
       into the front layer, so the panel is part of the composition from first paint rather
       than a hole in it — the column is reserved at every width, and an empty reserved column
       is what the reader sees for as long as they are not pointing at a row, which is most of
       the time. A section whose rows have no photographs at all keeps the old behaviour and
       simply shows nothing. */
    opacity: 0;
    box-shadow: 0 34px 90px rgba(0, 0, 0, .5);
}
.gd-panel.is-open { opacity: 1; }
.gd--landscape .gd-panel { aspect-ratio: 4 / 3; }
.gd-panel::after {
    content: '';
    position: absolute; inset: 0;
    box-shadow: inset 0 0 0 1px rgba(201, 168, 106, .32);
}
/* Promoted only while a crossfade is running, and the controller takes it off again. */
.gd-panel.is-live { will-change: opacity; }
@media (hover: hover) and (pointer: fine) { .gd-panel { display: block; } }

/* Two layers that trade opacity, so a swap is a crossfade and never a blank flash. No `src` in
   the markup: an empty src="" is a request for the current page. */
.gd-layer {
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    -o-object-fit: cover;
       object-fit: cover;
    opacity: 0;
}

/* ---- stacked ---- */

@media (max-width: 900px) {
    .gd-grid { grid-template-columns: 1fr; }
    .gd-index, .gd-media { grid-column: 1; }
    .gd-panel { display: none; }
    .gd-row { padding: 16px 0; min-height: 44px; }
    .gd-ordinal { min-width: 2em; }
    .gd-meta { letter-spacing: .18em; }
}
@media (max-width: 560px) {
    .gd-thumb { width: 60px; height: 80px; }
}

/* The panel stays under reduced motion: a photograph standing still is not motion, and hiding
   it would take a picture out of the composition rather than take movement out of it. The
   controller returns early there and never wires the swap, so it simply holds its opener. */
@media (prefers-reduced-motion: reduce) {
    .gd-name, .gd-meta, .gd-arrow { transition: none; }
    .gd-row:is(:hover, :focus-visible) .gd-arrow { transform: none; }
}

/* ============================================================
   THE STATEMENT — the homepage's one sentence, lit a word at a time by the scroll.

   quote_scrub_controller.js splits the sentence into words and scrubs each from --sq-dim to
   --sq-lit across the section's travel through the viewport. The two colours are declared
   here and read by the controller, so the palette lives beside the type.

   Set in the Forbes wordmark's register — Jost at 500, tracked tight — rather than the
   display serif: this is a claim the firm makes, not a line somebody said, and it runs the
   full width of the page inside the ordinary side paddings.

   The section is deliberately taller than its content: the scrub's length IS the section's
   height, and a band the height of two lines would light all seven words in one flick.

   The base state is fully lit. Reduced motion, the static preview and no-JS all land on it
   without a rule of their own; the hiding rule below is the pre-split flash guard only, gated
   on `.js-split` — the flag the inline head script sets and its 4s failsafe removes — so a
   chunk that never arrives cannot leave the sentence hidden. Same shape as the split-text rule.
   ============================================================ */
.sq {
    --sq-dim: rgba(246, 243, 236, .16);
    --sq-lit: var(--cream);
    position: relative;
    display: flex;
    align-items: center;
    min-height: 56svh;
    margin-top: 56px;
    padding: clamp(40px, 5vw, 72px) 0;
    background: var(--ink-2);
}
.sq-inner {
    width: 100%;
}
.sq-quote {
    margin: 0;
    font: 500 clamp(40px, 7.6vw, 140px)/1.02 var(--font-sans);
    letter-spacing: -.025em;
    text-wrap: balance;
    color: var(--sq-lit);
}
@media (prefers-reduced-motion: no-preference) {
    .js-split [data-controller~="quote-scrub"] .sq-quote:not([data-split-ready]) { visibility: hidden; }
}
@media (max-width: 560px) {
    .sq { min-height: 48svh; }
}

/* ============================================================
   THE AREAS CAROUSEL — the homepage's markets strip, and the card it shares with /areas.

   The first twelve markets drift left in a loop (area_carousel_controller.js). The card is
   a photograph standing on its name: a hairline ring inside the frame is the only ornament,
   and the name answers hover the way the guide-index row does.

   The viewport is a native horizontal scroller BY DEFAULT — that is the reduced-motion and
   no-script rendering, with exactly N cards. `.ac--live` is what the controller adds once it
   has built the loop, and only then does the overflow close and the edges fade.

   Card width is a pure CSS contract on the viewport's inline size (`cqw`), so the controller
   never measures a card: `--ac-cols` is how many fit across, with a half card showing at the
   edge to say the strip continues. The track spaces cards with a flex `gap`, which the loop
   helper reads off the resolved column-gap and honours at the wrap.
   ============================================================ */
.ac-all {
    display: inline-block;
    margin-top: 18px;
    padding-bottom: 4px;
    border-bottom: 1px solid rgba(201, 168, 106, .35);
    letter-spacing: .28em;
    transition: color var(--duration-tint) var(--ease-drift), border-color var(--duration-tint) var(--ease-drift);
}
.lux a.ac-all:hover { color: var(--gold-2); border-color: var(--gold-2); }

.ac-viewport {
    --ac-gap: clamp(14px, 1.4vw, 22px);
    --ac-cols: 5.5;
    position: relative;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x proximity;
    scrollbar-width: none;
    container-type: inline-size;
}
.ac-viewport::-webkit-scrollbar { display: none; }
.ac--live .ac-viewport {
    overflow: hidden;
    scroll-snap-type: none;
    mask-image: linear-gradient(90deg, transparent, #000 5%, #000 95%, transparent);
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 5%, #000 95%, transparent);
}

.ac-track {
    display: flex;
    gap: var(--ac-gap);
    width: -moz-max-content;
    width: max-content;
}

.ac-card {
    flex: none;
    display: block;
    width: calc((100cqw - (var(--ac-cols) - 1) * var(--ac-gap)) / var(--ac-cols));
    scroll-snap-align: start;
    text-decoration: none;
}
.lux a.ac-card, .lux a.ac-card:hover { color: inherit; }
.ac-card:focus-visible { outline: 1px solid var(--gold-2); outline-offset: 4px; }

.ac-figure {
    position: relative;
    display: block;
    aspect-ratio: 4 / 5;
    overflow: hidden;
    background: var(--ink-2);
}
.ac-figure img {
    display: block;
    width: 100%;
    height: 100%;
    -o-object-fit: cover;
       object-fit: cover;
    transform: scale(1.001);   /* a sub-pixel head start keeps the slow zoom from snapping in */
}
.ac-plate {
    position: absolute;
    inset: 0;
    background: linear-gradient(140deg, #1a1713, #0f0e0c);
}
.ac-ring {
    position: absolute;
    inset: 10px;
    border: 1px solid rgba(201, 168, 106, .28);
    pointer-events: none;
}

.ac-name {
    display: block;
    margin-top: 16px;
    font-size: clamp(19px, 1.6vw, 24px);
    line-height: 1.15;
    color: rgba(246, 243, 236, .88);
}
.ac-meta {
    display: block;
    margin-top: 8px;
    letter-spacing: .2em;
    line-height: 1.6;
    color: rgba(246, 243, 236, .55);
    text-wrap: pretty;
}
.ac-card:is(:hover, :focus-visible) .ac-name { color: var(--gold-2); }

@media (prefers-reduced-motion: no-preference) {
    .ac-figure img { transition: transform var(--duration-drift) var(--ease-drift); }
    .ac-card:is(:hover, :focus-visible) .ac-figure img { transform: scale(1.04); }
    .ac-name { transition: color var(--duration-tint) var(--ease-drift); }
}

@media (max-width: 1200px) { .ac-viewport { --ac-cols: 4.5; } }
@media (max-width: 900px)  { .ac-viewport { --ac-cols: 3.2; } }
@media (max-width: 560px)  { .ac-viewport { --ac-cols: 1.6; } }

/* ------------------------------------------------------------
   THE PROPERTIES CAROUSEL — the same strip, carrying listings (_listing_card.html.twig).

   Wider cards than the markets, because a house wants its photograph read and a price set
   under it; the status sits in the frame's top-left corner on its own ink so it is legible
   over any photograph, and the figure is the one gold line under the name. Pending is told
   apart by tint alone, never by a badge shape the design language does not have.
   ------------------------------------------------------------ */
.ac--listings .ac-viewport { --ac-cols: 4.5; }
.ac-card--listing .ac-figure { aspect-ratio: 4 / 3; }
.ac-status {
    position: absolute;
    top: 18px;
    left: 18px;
    padding: 7px 10px 6px;
    font-size: 9px;
    letter-spacing: .28em;
    line-height: 1;
    color: rgba(246, 243, 236, .88);
    background: rgba(15, 14, 12, .72);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
}
.ac-card--listing[data-status="active"] .ac-status { color: var(--gold-2); }
.ac-card--listing[data-status="pending"] .ac-status { color: rgba(246, 243, 236, .7); }
.ac-price {
    display: block;
    margin-top: 10px;
    font: 300 11px/1.5 var(--font-sans);
    letter-spacing: .2em;
    text-transform: uppercase;
    color: var(--gold-2);
}
.ac-card--listing .ac-meta { margin-top: 8px; }

@media (max-width: 1200px) { .ac--listings .ac-viewport { --ac-cols: 3.5; } }
@media (max-width: 900px)  { .ac--listings .ac-viewport { --ac-cols: 2.4; } }
@media (max-width: 560px)  { .ac--listings .ac-viewport { --ac-cols: 1.3; } }

/* ============================================================
   THE AREAS INDEX — /areas. The same card, three across, nine at a time; or a map.

   The view switch and "Show more" are typographic — letterspaced text on a hairline — not
   buttons. `hidden` does the hiding on the wrappers, and each wrapper's own `display` is
   declared under `[hidden]` too, or the class's display would beat the attribute.
   ============================================================ */
.ax-head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 40px;
    flex-wrap: wrap;
}

.ax-switch {
    display: inline-flex;
    gap: 28px;
    padding-bottom: 2px;
}
.ax-tab {
    padding: 0 0 8px;
    border: 0;
    border-bottom: 1px solid transparent;
    background: none;
    cursor: pointer;
    font: 400 10.5px/1 var(--font-sans);
    letter-spacing: .3em;
    text-transform: uppercase;
    color: rgba(243, 239, 230, .55);
    transition: color var(--duration-tint) var(--ease-drift), border-color var(--duration-tint) var(--ease-drift);
}
.ax-tab:hover { color: var(--cream); }
.ax-tab[aria-pressed="true"] { color: var(--gold); border-bottom-color: var(--gold); }
.ax-tab:focus-visible { outline: 1px solid var(--gold-2); outline-offset: 6px; }

.ax-gallery { margin-top: clamp(40px, 5vw, 64px); }
.ax-gallery[hidden] { display: none; }

.ag-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: clamp(28px, 3vw, 44px) clamp(20px, 2.4vw, 32px);
}
.ag-cell[hidden] { display: none; }
.ag-grid .ac-figure { aspect-ratio: 4 / 3; }
.ag-grid .ac-name { font-size: clamp(22px, 2.1vw, 28px); }

.ax-more {
    display: table;   /* shrink-wraps a block-level anchor so the hairline is the word's width */
    margin: clamp(40px, 5vw, 64px) auto 0;
    padding-bottom: 6px;
    border-bottom: 1px solid rgba(201, 168, 106, .35);
    font: 400 11px/1 var(--font-sans);
    letter-spacing: .3em;
    text-transform: uppercase;
    transition: color var(--duration-tint) var(--ease-drift), border-color var(--duration-tint) var(--ease-drift);
}
.lux a.ax-more:hover { color: var(--gold-2); border-color: var(--gold-2); }

.ax-map {
    position: relative;
    margin-top: clamp(40px, 5vw, 64px);
    height: clamp(420px, 60vh, 720px);
    overflow: hidden;
    background: var(--cream-2);
    /* Hairlines top and bottom only, as the area page's own map. */
    border-top: 1px solid rgba(201, 168, 106, .28);
    border-bottom: 1px solid rgba(201, 168, 106, .28);
}
.ax-map[hidden] { display: none; }
.ax-map-canvas { position: absolute; inset: 0; }
.ax-map-canvas[hidden] { display: none; }
.ax-map-fallback {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    margin: 0;
    font: 300 13px/1.6 var(--font-sans);
    letter-spacing: .2em;
    text-transform: uppercase;
    color: var(--stone);
}
.ax-map-fallback[hidden] { display: none; }

@media (max-width: 900px) {
    .ag-grid { grid-template-columns: repeat(2, 1fr); }
    .ax-map { height: clamp(360px, 55vh, 560px); }
}
@media (max-width: 560px) {
    .ag-grid { grid-template-columns: 1fr; }
}

/* ============================================================
   Homepage lifestyle panels. An accordion rather than a grid: the sections
   above and below it are both grids, and a lifestyle is a thing a reader
   leans into one at a time.

   The expansion is pure CSS on :hover / :focus-visible — no controller
   decides it, so it works before the JS chunk lands and it is keyboard
   operable for free. The controller (lifestyle_panels) only owns the
   entrance: the clip-path wipe and the masked name lines.

   `ls-`, not `lp-`: that prefix belongs to the listing page.
   ============================================================ */
.ls-row {
    display: flex;
    gap: 6px;
    height: clamp(420px, 52vw, 620px);
}

.ls-panel {
    position: relative;
    display: block;
    flex: 1 1 0;
    /* Without this a flex item refuses to shrink past its content's intrinsic
       width, and the long standfirst inside the expanded panel would stop the
       others collapsing. */
    min-width: 0;
    overflow: hidden;
    background: var(--ink-2);
    transition: flex-grow var(--duration-move) var(--ease-drift);
}

.ls-figure { position: absolute; inset: 0; }

.ls-figure img {
    /* Muted at rest so the expanded panel arriving at full colour reads as the
       page choosing it, rather than four photographs competing at once. */
    filter: saturate(.5) brightness(.62);
    transition: filter var(--duration-tint) var(--ease-drift),
                transform var(--duration-drift) var(--ease-drift);
}

.ls-scrim {
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, rgba(15, 14, 12, 0) 38%, rgba(15, 14, 12, .88));
}

.ls-ring {
    position: absolute;
    inset: 0;
    border: 1px solid rgba(201, 168, 106, .3);
    pointer-events: none;
    transition: border-color var(--duration-tint) var(--ease-drift);
}

.ls-body {
    position: absolute;
    left: clamp(20px, 2vw, 30px);
    right: clamp(20px, 2vw, 30px);
    bottom: clamp(22px, 2.2vw, 32px);
}

.ls-name {
    margin: 0;
    font-size: clamp(24px, 2.4vw, 34px);
    line-height: 1.14;
    color: var(--cream);
    transition: color var(--duration-tint) ease;
}

/* 0fr → 1fr animates the panel's real height, so the name rides up on the same
   curve the panel widens on — a fade would leave it sitting still. */
.ls-copy {
    display: grid;
    grid-template-rows: 0fr;
    opacity: 0;
    transition: grid-template-rows var(--duration-move) var(--ease-drift),
                opacity var(--duration-move) var(--ease-drift);
}

.ls-copy-inner { overflow: hidden; min-height: 0; }

.ls-rule {
    width: 44px;
    height: 1px;
    margin: 20px 0 16px;
    background: linear-gradient(90deg, var(--gold), transparent);
}

.ls-standfirst {
    margin: 0;
    max-width: 46ch;
    font: 300 13.5px/1.8 var(--font-sans);
    color: rgba(246, 243, 236, .72);
    text-wrap: pretty;
    /* The standfirst was written to open a full page, not a panel. Three lines
       is the promise; the page itself is one click away. */
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    overflow: hidden;
}

.ls-cta {
    display: block;
    margin-top: 16px;
    color: var(--gold);
}

.ls-panel:is(:hover, :focus-visible) { flex-grow: 3.2; }
.ls-panel:is(:hover, :focus-visible) .ls-figure img { filter: none; transform: scale(1.05); }
.ls-panel:is(:hover, :focus-visible) .ls-ring { border-color: rgba(201, 168, 106, .55); }
.ls-panel:is(:hover, :focus-visible) .ls-name { color: var(--gold-2); }
.ls-panel:is(:hover, :focus-visible) .ls-copy { grid-template-rows: 1fr; opacity: 1; }

/* `.lux a` paints every link gold; these are photo panels. */
.lux a.ls-panel,
.lux a.ls-panel:hover { color: inherit; }

/* The default focus ring would sit outside a panel that is already announcing
   itself by widening; keep one anyway, inset, for the high-contrast case. */
.ls-panel:focus-visible { outline: 1px solid var(--gold-2); outline-offset: -3px; }

@media (prefers-reduced-motion: no-preference) {
    /* Gated on .js as well: a failed chunk must not leave the row clipped out
       of existence or the names permanently hidden. */
    .js .ls-figure { clip-path: inset(100% 0% 0% 0%); will-change: clip-path; }
    .js .ls-name { visibility: hidden; }
}

.ls-line { will-change: transform; }

@media (prefers-reduced-motion: reduce) {
    .ls-panel,
    .ls-figure img,
    .ls-ring,
    .ls-name,
    .ls-copy { transition: none; }
    .ls-panel:is(:hover, :focus-visible) { flex-grow: 1; }
    .ls-figure img { filter: none; }
    .ls-copy { grid-template-rows: 1fr; opacity: 1; }
}

/* Below this the accordion stops being one: there is not enough width for a
   panel to expand into, and on touch there is no hover to expand it with. The
   copy is simply always open. */
@media (max-width: 900px) {
    .ls-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; height: auto; }
    .ls-panel { height: 340px; }
    .ls-panel:is(:hover, :focus-visible) { flex-grow: 1; }
    .ls-figure img { filter: saturate(.72) brightness(.72); }
    .ls-copy { grid-template-rows: 1fr; opacity: 1; }
    .ls-standfirst { -webkit-line-clamp: 2; }
}

@media (max-width: 560px) {
    .ls-row { grid-template-columns: 1fr; }
    .ls-panel { height: 300px; }
}

/* ============================================================
   Homepage campaign section — all eighteen marketing acts as two
   counter-scrolling marquees (campaign_marquee_controller.js).
   Top row travels right, bottom row travels left; hovering a row eases that
   row to a stop while the other keeps going.

   Base state is a natively scrollable band, so with no JS all nine acts per
   row stay reachable. `.js` swaps that for a clipped, animated track — the
   same shape of gating as `.js .mm-img` and as `.js-motion .reveal` above.
   ============================================================ */
.cm-section {
    --cm-gap: clamp(12px, 1.2vw, 18px);
    /* Fallback until the controller measures the row exactly. 100vw includes the
       scrollbar, so this runs a few px wide; .cm-row clips it, and JS corrects it. */
    --cm-card: calc((100vw - 2 * var(--cm-gap)) / 3);
}

.cm-row {
    overflow-x: auto;                 /* no-JS: the nine stay reachable */
    overflow-y: hidden;
    scrollbar-width: none;
    margin-bottom: var(--cm-gap);
}
.cm-row::-webkit-scrollbar { display: none; }
.cm-row:last-of-type { margin-bottom: 0; }

.js .cm-row { overflow: hidden; }

.cm-track {
    display: flex;
    width: -moz-max-content;
    width: max-content;
    /* NO `gap` here, deliberately. With a gap the track is 18 cards + 17 gaps,
       which is not twice 9 cards + 9 gaps — so translating by exactly -50% would
       land half a gap out and the loop would visibly jump once per cycle. The
       spacing is margin-right on every card instead, including the last. */
    will-change: transform;
}

.cm-card {
    position: relative;
    flex: none;
    width: var(--cm-card);
    height: 330px;
    margin-right: var(--cm-gap);
    overflow: hidden;
    box-shadow: inset 0 0 0 1px rgba(201, 168, 106, .3);
}
.cm-card img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    -o-object-fit: cover;
       object-fit: cover;
    transition: transform 1.6s cubic-bezier(.2, .7, .2, 1);
}
.cm-card:hover img { transform: scale(1.05); }

.cm-scrim {
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, rgba(15, 14, 12, 0) 36%, rgba(15, 14, 12, .88));
}
.cm-caption {
    position: absolute;
    left: 24px;
    right: 24px;
    bottom: 26px;
}
.cm-num {
    font-size: 20px;
    line-height: 1;
    color: var(--gold);
    margin-bottom: 10px;
}
.cm-name {
    font-size: 24px;
    line-height: 1.2;
    transition: color .4s ease;
}
.cm-meta {
    margin-top: 8px;
    letter-spacing: .22em;
    color: rgba(246, 243, 236, .55);
}

/* `.lux a` paints every link gold; these are photo cards. */
.lux a.cm-card,
.lux a.cm-card:hover { color: inherit; }
.lux a.cm-card:hover .cm-name { color: var(--gold-2); }

/* ---- Reduced motion ----
   The controller returns before building any tween, so this has to beat
   `.js .cm-row` and hand the row back to native scrolling. The duplicate set is
   hidden too — without the animation it is just the same nine acts twice. */
@media (prefers-reduced-motion: reduce) {
    .js .cm-row {
        overflow-x: auto;
        scroll-snap-type: x mandatory;
    }
    .cm-card { scroll-snap-align: start; }
    .cm-dupe { display: none; }
    .cm-card img, .cm-card:hover img { transition: none; transform: none; }
    .cm-name { transition: none; }
}

/* ---- Responsive ---- */
@media (max-width: 900px) {
    .cm-section { --cm-card: calc((100vw - 1.5 * var(--cm-gap)) / 2); }
    .cm-card { height: 280px; }
    .cm-name { font-size: 21px; }
}
@media (max-width: 560px) {
    /* ~1.2 across, so a partial card always hints that the row continues. */
    .cm-section { --cm-card: calc(100vw / 1.35); }
    .cm-card { height: 240px; }
}

/* ============================================================
   Back to top — the button itself lives in base.html.twig and is
   styled in app.css; on marketing pages it takes the ink + gold skin.
   ============================================================ */
body:has(.lux) .scroll-top {
    --st-bg: var(--ink-2);
    --st-fg: var(--gold);
    --st-border: rgba(201, 168, 106, .45);
}
body:has(.lux) .scroll-top:hover {
    --st-fg: var(--gold-2);
    --st-border: rgba(201, 168, 106, .8);
}

/* ============================================================
   /luxury-marketing — the six distribution channels.

   Unlike the .ar-* and .ge-* blocks, layout lives in these classes rather than
   inline on the elements. This page was not authored against a fixed-width
   design file, so there is nothing to stay 1:1 with, and the media queries
   below need no !important as a result — the preference stated for new work.

   The eighteen-act ledger below the grid reuses .mm-* wholesale and adds only
   .lm-acts, because on /sellers those rows sit in a two-column grid next to the
   hover panel and here they are full width.
   ============================================================ */
.lm-channels {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: clamp(28px, 3vw, 44px);
}

.lm-channel {
    padding: 34px 32px 36px;
    box-shadow: inset 0 0 0 1px rgba(201, 168, 106, .16);
    transition: box-shadow .5s ease, background-color .5s ease;
}
.lm-channel:hover {
    background: var(--ink-2);
    box-shadow: inset 0 0 0 1px rgba(201, 168, 106, .45);
}

.lm-channel-n {
    font-size: 34px;
    line-height: 1;
    /* The "big gold numeral" value, as on .ab-value-n and .ge-sov-num. */
    color: #d9b878;
    margin-bottom: 20px;
}
.lm-channel-name {
    margin: 0;
    font-size: clamp(21px, 2.1vw, 27px);
    line-height: 1.25;
    color: var(--cream);
}
.lm-channel-kind {
    margin-top: 9px;
    font: 300 10px/1.5 var(--font-sans);
    letter-spacing: .22em;
    text-transform: uppercase;
    color: rgba(246, 243, 236, .5);
}
.lm-channel-body {
    margin: 18px 0 0;
    font: 300 14.5px/1.8 var(--font-sans);
    color: rgba(246, 243, 236, .72);
    text-wrap: pretty;
}

/* Forbes is the one conditional channel. The flag sits above the prose so the
   qualifier is read before the promise, not after it. .lm-note carries the same
   placeholder warning .mm-note does on the matching act. */
.lm-flag {
    display: inline-block;
    margin-top: 14px;
    padding: 6px 11px;
    font: 400 9px/1 var(--font-sans);
    letter-spacing: .24em;
    text-transform: uppercase;
    color: var(--gold);
    box-shadow: inset 0 0 0 1px rgba(201, 168, 106, .38);
}
.lm-note {
    margin-top: 14px;
    font: 300 10px/1.7 var(--font-sans);
    letter-spacing: .16em;
    text-transform: uppercase;
    color: var(--gold-deep);
}

/* The .mm-* ledger without /sellers' hover panel — shared by the eighteen acts
   on /luxury-marketing and the nine services on /concierge-service. With no
   panel beside them the rows run the full width, so the benefit column is capped
   here rather than being constrained by that page's two-column grid, and the
   rows are not interactive. Any third page reusing the ledger joins this
   selector rather than duplicating the two rules. */
.lm-acts .mm-row,
.cs-services .mm-row { cursor: default; }
.lm-acts .mm-benefit,
.cs-services .mm-benefit { max-width: 720px; }

/* ---- Responsive ---- */
@media (max-width: 1000px) {
    .lm-channels { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 700px) {
    .lm-channels { grid-template-columns: 1fr; }
    .lm-channel { padding: 28px 24px 30px; }
    .lm-channel-n { font-size: 28px; }
}

@media (prefers-reduced-motion: reduce) {
    .lm-channel { transition: none; }
}

/* ============================================================
   /lifestyles/{slug} — the one section with no counterpart on an area page.

   Everything else on that page reuses the .ar-* report layout above; this is the
   "where it lives" strip, which exists because a lifestyle spans places and an
   area does not.
   ============================================================ */
.lf-areas {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 1px;
    background: rgba(201, 168, 106, .2);
    box-shadow: inset 0 0 0 1px rgba(201, 168, 106, .2);
}

.lf-area {
    display: block;
    background: #141210;
    padding: 30px 28px;
    transition: background-color .4s ease;
}
/* `.lux a` paints every link gold; these are cards with their own type colour. */
.lux a.lf-area,
.lux a.lf-area:hover { color: inherit; }
.lux a.lf-area:hover { background: #191612; }

.lf-area-name {
    display: block;
    font-size: 26px;
    line-height: 1.2;
    color: var(--cream);
    transition: color .4s ease;
}
.lux a.lf-area:hover .lf-area-name { color: var(--gold-2); }

.lf-area-meta {
    display: block;
    margin-top: 10px;
    font: 300 10px/1 var(--font-sans);
    letter-spacing: .2em;
    text-transform: uppercase;
    color: rgba(246, 243, 236, .55);
}

@media (prefers-reduced-motion: reduce) {
    .lf-area, .lf-area-name { transition: none; }
}

/* ============================================================
   Rendered content blocks — the public article page.

   The same markup BlockRenderer produces for the admin review screen, in the editorial
   register: serif display faces for headings and quotes, cream on ink, gold for the marks.
   Nothing here overrides a class the renderer set, because the renderer sets none.
   ============================================================ */

.lux-prose {
    font: 300 17px/1.75 var(--font-sans);
    color: rgba(246, 243, 236, .82);
}

.lux-prose > * + * { margin-top: 1.35em; }

.lux-prose a { color: var(--gold-2); text-decoration: underline; text-underline-offset: 3px; }
.lux-prose a:hover { color: var(--cream); }

.lux-prose h2,
.lux-prose h3 {
    margin-top: 2.2em;
    font-family: var(--font-display);
    font-weight: 400;
    line-height: 1.15;
    color: var(--cream);
    text-wrap: balance;
}
.lux-prose h2 { font-size: clamp(26px, 3vw, 34px); }
.lux-prose h3 { font-size: clamp(21px, 2.4vw, 26px); }

.lux-prose ul, .lux-prose ol { padding-left: 1.4em; }
.lux-prose ul { list-style: disc; }
.lux-prose ol { list-style: decimal; }
.lux-prose li::marker { color: var(--gold); }
.lux-prose li + li { margin-top: .5em; }

.lux-prose hr {
    margin: 3em 0;
    border: 0;
    border-top: 1px solid rgba(201, 168, 106, .3);
}

.lux-prose blockquote {
    margin: 2.4em 0;
    padding-left: 1.4em;
    border-left: 1px solid var(--gold);
    font-family: var(--font-display);
    font-size: clamp(22px, 2.6vw, 28px);
    line-height: 1.4;
    color: var(--cream);
}
.lux-prose blockquote p { margin: 0; }
.lux-prose blockquote cite {
    display: block;
    margin-top: .7em;
    font: 300 10.5px/1 var(--font-sans);
    font-style: normal;
    letter-spacing: .24em;
    text-transform: uppercase;
    color: var(--gold);
}

.lux-prose figure { margin: 2.6em 0; }
.lux-prose figure img {
    width: 100%;
    display: block;
    box-shadow: inset 0 0 0 1px rgba(201, 168, 106, .3);
}
/* Out to the column edges — the article column is padded, this ignores it. */
.lux-prose figure[data-block="full-bleed"] { margin-inline: calc(50% - 50vw); }
.lux-prose figure[data-block="full-bleed"] img { max-height: 70vh; -o-object-fit: cover; object-fit: cover; }
.lux-prose figure[data-block="two-up"] > div {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 18px;
}
.lux-prose figcaption {
    margin-top: .9em;
    font: 300 11px/1.6 var(--font-sans);
    letter-spacing: .1em;
    text-transform: uppercase;
    color: rgba(246, 243, 236, .5);
}

.lux-prose dl[data-block="key-details"] {
    margin: 2.6em 0;
    padding: 6px 22px;
    box-shadow: inset 0 0 0 1px rgba(201, 168, 106, .3);
}
.lux-prose dl > div {
    display: flex;
    justify-content: space-between;
    gap: 24px;
    padding: 13px 0;
    border-bottom: 1px solid rgba(201, 168, 106, .16);
}
.lux-prose dl > div:last-child { border-bottom: 0; }
.lux-prose dt {
    font: 300 10px/1.4 var(--font-sans);
    letter-spacing: .22em;
    text-transform: uppercase;
    color: var(--gold);
    align-self: center;
}
.lux-prose dd {
    margin: 0;
    font-family: var(--font-display);
    font-size: 19px;
    color: var(--cream);
    text-align: right;
}

.lux-prose [data-block="embed"] { margin: 2.6em 0; }
.lux-prose .cb-embed-card {
    box-shadow: inset 0 0 0 1px rgba(201, 168, 106, .3);
    background: rgba(255, 255, 255, .02);
}
.lux-prose [data-block="embed"] a { text-decoration: none; display: block; }
.lux-prose .cb-embed-card img { width: 100%; height: 220px; -o-object-fit: cover; object-fit: cover; display: block; }
.lux-prose .cb-embed-body { padding: 22px 24px 24px; }
.lux-prose .cb-embed-meta {
    font: 300 10px/1 var(--font-sans);
    letter-spacing: .24em;
    text-transform: uppercase;
    color: var(--gold);
}
.lux-prose .cb-embed-title {
    margin-top: 10px;
    font-family: var(--font-display);
    font-size: 25px;
    line-height: 1.2;
    color: var(--cream);
}
.lux-prose .cb-embed-sub { margin-top: 8px; font-size: 14px; color: rgba(246, 243, 236, .6); }

@media (max-width: 720px) {
    .lux-prose figure[data-block="two-up"] > div { grid-template-columns: 1fr; }
    .lux-prose dl > div { flex-direction: column; gap: 6px; }
    .lux-prose dd { text-align: left; }
}

/* ============================================================
   THE ASK — the homepage's closing band: the photograph and the form.

   The form used to be a link to /contact. It is inline now because this is the
   last thing on the page and a second page is where people leave; the CSS here
   is only the band, since the fields themselves come from
   security/_form_theme.html.twig and must keep matching /contact and /register.

   The photograph sits BESIDE the fields, not behind them. A form is the one
   thing on a page that must never be argued with for contrast, and every other
   picture on this page is already carrying type of its own; putting one more
   behind the fields would make it read as a house tic rather than a decision.
   ============================================================ */

/* A band on its own ground, not another section on the page's.

   Every other section here is separated by 104px of nothing, which works while
   the compositions differ. It stops working directly under the agent film: that
   band is also a picture on the left and copy on the right over the same ink, so
   at the same spacing the two read as one continuous block — and the last thing
   on the page is the one thing that cannot be missed.

   So the divide is a change of register rather than more distance: a gold
   hairline, a step to --ink-2, and generous room inside. Full-bleed the house way
   (see .ge-po) — the section carries the ground and takes no measure, and the
   grid inside it keeps the page's 1560px. No negative margins, no 100vw.

   No border-bottom: the footer supplies its own hairline 96px below, and two
   rules that close together read as a mistake. */
.ct-band {
    margin-top: clamp(88px, 10vw, 148px);
    padding-block: clamp(64px, 7vw, 104px);
    background: var(--ink-2);
    border-top: 1px solid rgba(201, 168, 106, .22);
}

.ct-grid {
    max-width: 1560px;
    margin: 0 auto;
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    gap: clamp(28px, 4vw, 64px);
    align-items: center;
}

.ct-frame {
    grid-column: 1 / span 6;
    grid-row: 1;                 /* named, for the same reason .at-* names theirs */
    position: relative;
    margin: 0;
    aspect-ratio: 4 / 5;
    overflow: hidden;
}
.ct-frame img { position: absolute; inset: 0; width: 100%; height: 100%; -o-object-fit: cover; object-fit: cover; }

/* Weighted to the foot, where the caption sits, and kept off the middle of the
   picture — the .af-scrim call, one stop lighter because nothing is read over
   this one except the caption. */
.ct-scrim {
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, rgba(15, 14, 12, .1) 0%, rgba(15, 14, 12, 0) 42%, rgba(15, 14, 12, .72) 100%);
}
.ct-ring { position: absolute; inset: 0; box-shadow: inset 0 0 0 1px rgba(201, 168, 106, .3); }

.ct-caption {
    position: absolute;
    left: clamp(18px, 2vw, 26px);
    bottom: clamp(16px, 1.8vw, 22px);
    margin: 0;
    color: rgba(243, 239, 230, .85);
}

.ct-copy { grid-column: 8 / span 5; grid-row: 1; }

.ct-headline {
    margin: 0;
    font-size: clamp(28px, 3.4vw, 42px);
    line-height: 1.14;
    text-wrap: balance;
}
/* One accent, and it is the same gold the rest of the page reserves for one
   phrase per section. */
.ct-headline em { font-style: italic; color: #d9b878; }

.ct-graf {
    margin: 22px 0 0;
    max-width: 46ch;
    font: 300 14.5px/1.85 var(--font-sans);
    color: rgba(246, 243, 236, .72);
    text-wrap: pretty;
}

.ct-form { margin-top: clamp(26px, 3vw, 38px); }

.ct-submit {
    width: 100%;
    margin-top: 16px;
    padding: 18px;
    font: 400 10.5px/1 var(--font-sans);
    letter-spacing: .22em;
    color: var(--ink);
    background: var(--gold);
    transition: background var(--duration-tint) var(--ease-drift);
}
.ct-submit:hover { background: var(--gold-2); }

.ct-note {
    margin: 20px 0 0;
    font: 300 12.5px/1.7 var(--font-sans);
    color: rgba(246, 243, 236, .42);
}

@media (max-width: 900px) {
    .ct-grid { grid-template-columns: 1fr; }
    .ct-frame,
    .ct-copy { grid-column: 1; grid-row: auto; }
    /* The picture stops being half the band and becomes its header, so it wants
       a shape that does not push the first field off the screen. */
    .ct-frame { aspect-ratio: 16 / 10; }
    .ct-copy { margin-top: clamp(28px, 6vw, 44px); }
}

@media (prefers-reduced-motion: reduce) {
    .ct-submit { transition: none; }
}

/* The confirmation, in the place the form just was. Bordered on the gold edge
   like the base template's flash, so it reads as the same kind of message even
   though it is rendered here rather than at the top of the page. */
.ct-sent {
    margin-top: clamp(26px, 3vw, 38px);
    padding: clamp(22px, 2.4vw, 30px) clamp(22px, 2.4vw, 30px);
    border-left: 2px solid var(--gold);
    background: rgba(15, 14, 12, .55);
}
.ct-sent-line {
    margin: 0;
    font: 300 15px/1.8 var(--font-sans);
    color: rgba(246, 243, 236, .86);
    text-wrap: pretty;
}

/* ============================================================
   THE SITE FOOTER.

   A sitemap rather than a row of four links: the footer is on every page and is
   the one place a crawler and a lost reader both look. Its headings and links
   reuse the Resources dropdown's exact type treatment, because they are the same
   list in two places — almost-but-not-quite alike would be worse than either.

   Stays on --ink rather than stepping to --ink-2 like the bands above it. On the
   homepage the contact band is already ink-2 and lands directly overhead, so a
   second step would merge the two; the page's own ground is what separates them.
   ============================================================ */

.lf {
    position: relative;
    margin-top: clamp(84px, 9vw, 132px);
    padding-block: clamp(52px, 6vw, 80px) clamp(30px, 3.5vw, 44px);
    border-top: 1px solid rgba(201, 168, 106, .22);
    overflow: hidden;
}

/* A page that closes on the contact band needs no gap before the footer: the band is
   already a surface of its own, and its padding is the gap. */
:has(> .ct-band:last-child) + .lf { margin-top: 0; }

/* The same 45° weave the hero and the Forbes band carry, a shade fainter. It is
   the only texture down here, and it stops a tall block of small type reading as
   a flat panel. */
.lf-weave {
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image: repeating-linear-gradient(45deg, rgba(255, 255, 255, .012) 0 2px, transparent 2px 4px);
}

.lf-inner { position: relative; max-width: 1560px; margin: 0 auto; }

.lf-top {
    display: grid;
    grid-template-columns: minmax(260px, 1fr) 2.3fr;
    gap: clamp(40px, 6vw, 96px);
}

/* ---- the brand column ---- */

.lf-mark { display: inline-flex; align-items: center; gap: 14px; text-decoration: none; }
.lf-diamond {
    width: 6px;
    height: 6px;
    flex: 0 0 auto;
    transform: rotate(45deg);
    background: var(--gold);
}
.lf-word {
    font-size: 14px;
    letter-spacing: .4em;
    text-transform: uppercase;
    color: var(--cream);
}
.lux a.lf-mark,
.lux a.lf-mark:hover { color: inherit; }

.lf-line {
    margin: 22px 0 0;
    max-width: 34ch;
    font: 300 13px/1.85 var(--font-sans);
    color: rgba(246, 243, 236, .55);
    text-wrap: pretty;
}

.lf-cta { display: inline-block; margin-top: 24px; color: var(--gold); }
.lf-arrow {
    display: inline-block;
    margin-left: 6px;
    transition: transform var(--duration-move) var(--ease-drift);
}
.lf-cta:hover .lf-arrow { transform: translateX(5px); }

/* ---- the sitemap ---- */

.lf-map {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: clamp(22px, 2.6vw, 40px);
}

/* Verbatim from the Resources dropdown, so the two lists are one system. */
.lf-head {
    margin: 0;
    padding-bottom: 10px;
    border-bottom: 1px solid rgba(201, 168, 106, .22);
    font: 300 9.5px/1 var(--font-sans);
    letter-spacing: .26em;
    text-transform: uppercase;
    color: var(--gold);
}

.lf-list { margin: 4px 0 0; padding: 0; list-style: none; }

.lf-link {
    display: block;
    padding: 7px 0;
    font: 300 13px/1.35 var(--font-sans);
    color: rgba(246, 243, 236, .7);
    text-decoration: none;
    transition: color var(--duration-tint) var(--ease-drift);
}
.lux a.lf-link { color: rgba(246, 243, 236, .7); }
.lux a.lf-link:hover,
.lf-link:hover { color: var(--cream); }
.lf-link:focus-visible { outline: 1px solid var(--gold-2); outline-offset: 3px; }

.lf-empty {
    padding: 7px 0;
    font: 300 13px/1.35 var(--font-sans);
    color: rgba(246, 243, 236, .32);
}

/* ---- the legal bar ---- */

.lf-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 22px;
    flex-wrap: wrap;
    margin-top: clamp(40px, 5vw, 64px);
    padding-top: clamp(20px, 2.4vw, 28px);
    border-top: 1px solid rgba(201, 168, 106, .12);
}

.lf-legal {
    font: 300 10px/1.8 var(--font-sans);
    letter-spacing: .16em;
    text-transform: uppercase;
    color: rgba(246, 243, 236, .38);
}

.lf-account { display: flex; align-items: center; gap: 26px; }

.lf-link--small {
    padding: 0;
    font-size: 10px;
    letter-spacing: .2em;
    text-transform: uppercase;
    color: rgba(243, 239, 230, .55);
}
.lux a.lf-link--small { color: rgba(243, 239, 230, .55); }
.lux a.lf-link--gold,
.lf-link--gold { color: var(--gold); }
.lux a.lf-link--gold:hover { color: var(--gold-2); }

/* ---- responsive ---- */

@media (max-width: 1100px) {
    .lf-top { grid-template-columns: 1fr; gap: clamp(36px, 6vw, 56px); }
    .lf-line { max-width: 52ch; }
}

@media (max-width: 700px) {
    .lf-map { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 30px 24px; }
    .lf-foot { flex-direction: column; align-items: flex-start; }
}

@media (prefers-reduced-motion: reduce) {
    .lf-arrow, .lf-link { transition: none; }
    .lf-cta:hover .lf-arrow { transform: none; }
}

