/*
 * Bespoke property pages — the `.lp-*` layer.
 *
 * Loaded only on `/properties/{slug}` (the `listing_page` Encore entry), on top of
 * `public.css`, which supplies the fonts, the site tokens and the motion tokens.
 *
 * ── The one rule ────────────────────────────────────────────────────────────────
 * **No colour literals below this block, and no theme tokens either.** A section rule may
 * name exactly one kind of colour: `var(--ctx-*)` — the role it wants in whatever band it
 * happens to sit in. It may not name `--lp-*`, which is the *page's* theme and belongs to the
 * band bindings below; a section that reads the page ground directly is a section that knows
 * which theme it is in, and that is the thing this contract exists to prevent.
 *
 * The nine roles: bg, text, text-2, surface, accent, rule, rule-strong, input-line, scrim.
 * A section that needs something not on that list grows the list by review — never by a
 * one-off variable, never by a literal.
 *
 * A hex value in a section is a hole in the theme system: it is the one thing that would look
 * identical on every property no matter what the photography said. The two deliberate
 * exceptions are documented where they appear — scrims over photography (which are neutral
 * shadow, not palette) and the compliance footer (fixed brand assets that must read
 * identically everywhere).
 *
 * ── Layout convention ───────────────────────────────────────────────────────────
 * Unlike the area and lifestyle reports — ports of fixed-grid design files, which keep their
 * layout inline — these sections are composable and reorderable, so their layout lives here.
 * A section whose spacing depended on what happened to sit above it would break the moment
 * an admin dragged it somewhere else.
 *
 * ── Motion ──────────────────────────────────────────────────────────────────────
 * Hidden state is gated on `.js` AND `prefers-reduced-motion: no-preference`, matching
 * `.reveal` in public.css. No JS, reduced motion, or a crawler all get the full page.
 */

/* ============================================================
   Root, rhythm and the shared primitives
   ============================================================ */

.lp {
    background: var(--lp-background);
    color: var(--lp-text-primary);
    font-family: var(--font-sans);
    -webkit-font-smoothing: antialiased;

    /* Fallbacks so the page is coherent before a theme exists — an uncomposed draft in
       preview, or a theme that failed to derive. */
    --lp-background: var(--cream);
    --lp-surface: var(--cream-2);
    --lp-text-primary: var(--ink);
    --lp-text-secondary: var(--stone);
    --lp-accent: var(--gold-deep);
    --lp-rule: var(--gold);
    --lp-type-scale: 1;
    --lp-motion-scale: 1;
}

/* The theme's own values win over the fallbacks above — set inline on the element. */
.lp[style] { }

/* ============================================================
   Band contexts — Layer 3 of the theme contract (page-themes PRD §3.2–3.3)
   ============================================================

   The one thing that lets a section be written once and rendered on any ground. A section
   never names a theme token: it names a *role* in whatever band it happens to sit in, and the
   band binds that role to the active theme's value for its context. `--ctx-bg` on an ivory
   page is ivory; on an evening page it is warm charcoal; in a `cinema` band it is near-black
   on both. The section's CSS is identical in all three cases and does not know which it is in.

   Four contexts, named for the role they play rather than the colour they happen to be —
   "dark band" is meaningless on a dark page:

     base      the page ground
     alt       one tonal step off base, so a boundary reads without a rule
     contrast  the register inversion, the full-stop moment
     cinema    media viewing; near-black in every theme, because posters and players read
               best on near-black regardless of register

   Each binding falls through to the pre-context `--lp-*` role, so a theme that emits no
   context tokens — every row written before this iteration, and every revision of one —
   renders exactly as it did before. That fallback tail is what makes adopting this a
   mechanical change rather than a data migration. `contrast` and `cinema` fall back to the
   base ground deliberately: until a theme defines them they are simply the page, which is
   what those sections looked like yesterday.

   `rule-strong`, `input-line` and `scrim` have no tail. No pre-context token meant the same
   thing, and nothing consumes them until the theme is guaranteed to emit them. */

.lp,
.lp-band--base {
    --ctx-bg:          var(--lp-ctx-base-bg, var(--lp-background));
    --ctx-text:        var(--lp-ctx-base-text, var(--lp-text-primary));
    --ctx-text-2:      var(--lp-ctx-base-text-2, var(--lp-text-secondary));
    --ctx-surface:     var(--lp-ctx-base-surface, var(--lp-surface));
    --ctx-accent:      var(--lp-ctx-base-accent, var(--lp-accent));
    --ctx-rule:        var(--lp-ctx-base-rule, var(--lp-rule));
    --ctx-rule-strong: var(--lp-ctx-base-rule-strong);
    --ctx-input-line:  var(--lp-ctx-base-input-line);
    --ctx-scrim:       var(--lp-ctx-base-scrim);
}

.lp-band--alt {
    --ctx-bg:          var(--lp-ctx-alt-bg, var(--lp-surface));
    --ctx-text:        var(--lp-ctx-alt-text, var(--lp-text-primary));
    --ctx-text-2:      var(--lp-ctx-alt-text-2, var(--lp-text-secondary));
    --ctx-surface:     var(--lp-ctx-alt-surface, var(--lp-background));
    --ctx-accent:      var(--lp-ctx-alt-accent, var(--lp-accent));
    --ctx-rule:        var(--lp-ctx-alt-rule, var(--lp-rule));
    --ctx-rule-strong: var(--lp-ctx-alt-rule-strong);
    --ctx-input-line:  var(--lp-ctx-alt-input-line);
    --ctx-scrim:       var(--lp-ctx-alt-scrim);
}

.lp-band--contrast {
    --ctx-bg:          var(--lp-ctx-contrast-bg, var(--lp-background));
    --ctx-text:        var(--lp-ctx-contrast-text, var(--lp-text-primary));
    --ctx-text-2:      var(--lp-ctx-contrast-text-2, var(--lp-text-secondary));
    --ctx-surface:     var(--lp-ctx-contrast-surface, var(--lp-surface));
    --ctx-accent:      var(--lp-ctx-contrast-accent, var(--lp-accent));
    --ctx-rule:        var(--lp-ctx-contrast-rule, var(--lp-rule));
    --ctx-rule-strong: var(--lp-ctx-contrast-rule-strong);
    --ctx-input-line:  var(--lp-ctx-contrast-input-line);
    --ctx-scrim:       var(--lp-ctx-contrast-scrim);
}

.lp-band--cinema {
    --ctx-bg:          var(--lp-ctx-cinema-bg, var(--lp-background));
    --ctx-text:        var(--lp-ctx-cinema-text, var(--lp-text-primary));
    --ctx-text-2:      var(--lp-ctx-cinema-text-2, var(--lp-text-secondary));
    --ctx-surface:     var(--lp-ctx-cinema-surface, var(--lp-surface));
    --ctx-accent:      var(--lp-ctx-cinema-accent, var(--lp-accent));
    --ctx-rule:        var(--lp-ctx-cinema-rule, var(--lp-rule));
    --ctx-rule-strong: var(--lp-ctx-cinema-rule-strong);
    --ctx-input-line:  var(--lp-ctx-cinema-input-line);
    --ctx-scrim:       var(--lp-ctx-cinema-scrim);
}

/* A band paints its own ground. This is the declaration that makes the register rhythm
   visible, and the reason section rhythm had to become padding first. */
.lp-band {
    background: var(--ctx-bg);
    color: var(--ctx-text);
}

/* Selection and focus, set once for the whole page.

   Both default to the browser's own blue, which is the single loudest colour on an otherwise
   muted page and the one nobody notices until a screenshot is taken mid-drag. The focus ring
   is the accessibility half of the same token: visible enough to satisfy an audit, drawn in
   the page's own accent rather than in the platform's. */
.lp ::-moz-selection { background: var(--lp-selection-bg); }
.lp ::selection { background: var(--lp-selection-bg); }

.lp :focus-visible {
    outline: 2px solid var(--lp-focus);
    outline-offset: 2px;
}

/* The imagery grade, applied once to every photograph the page carries so a gallery of mixed
   origins reads as one shoot. A theme decides how far down, and the evening register asks for
   less because its surround is already doing the work. */
.lp img,
.lp video {
    filter: var(--lp-image-grade, none);
}

/* Vertical rhythm. Every section owns the space above *and* below itself, so reordering never
   changes the spacing and two neighbours sum to the full gap (--lp-section-pad is a half-gap;
   see public.css). Padding rather than margin because a themed band paints its own ground and
   a margin between two grounds would show the page ground through the seam. */
.lp-section { padding-block: var(--lp-section-pad); }

/* The two sections that paint to the page edges and must not be inset by theme ground: the
   hero is a full-height photograph, and the compliance footer is deliberately outside the
   theme entirely. The footer takes a margin instead — it separates from the page rather than
   belonging to it, which is the one case where margin is the honest answer. */
.lp-section--hero { padding-block: 0; }

/* ---- how the page ends -------------------------------------------------------
   The showing request is the last thing a reader is asked to do, and it used to be followed by
   three separate pieces of empty ground: its own half-gap, the legal strip's margin above it,
   and the site footer's own top margin. Nearly three hundred pixels of nothing between the
   form and the end of the page, which read as the page having stopped early.

   It closes tight against the footer instead — tight, not flush. The section ends on the
   agent's licence line, and type set hard against the edge of a band reads as type that was
   cut off rather than as type that was placed; the tight pad is the smallest space that still
   looks deliberate.

   `.lp ~ .lf` is the site footer seen from a property page: it is a sibling of the page shell,
   not a descendant, and its standing top margin is written for pages that end on a text block
   rather than on a form. */
.lp-section--showing-request { padding-bottom: var(--lp-section-pad-tight); }

.lp ~ .lf { margin-top: 0; }

/* The legal strip now prints below the site footer (see page.html.twig), where it is outside
   `.lp` and therefore outside the theme — which is what it always wanted to be. It paints its
   own ground, so the band wrapper around it has no colour to resolve and needs none. */
.lp-section--compliance-footer { padding-block: 0; }

/* A section title introduces what follows rather than standing alone, so it closes tight
   against it. A full section's worth of air underneath would read as a heading that had lost
   its chapter — and when the title happens to sit on the same ground as the section beneath
   it, the two are read as one block, which is what a chapter opening is. */
.lp-section--section-title { padding-bottom: var(--lp-section-pad-tight); }

/* The map opens its own band. It carries no heading and no address line any more, so the first
   thing in the section is the full-width panel itself — and a half-gap of ground above a picture
   that runs to both edges reads as a misaligned photograph rather than as air. */
.lp-section--location-map { padding-top: 0; }

.lp-shell {
    max-width: 1240px;
    margin: 0 auto;
    padding: 0 24px;
}

@media (min-width: 900px) {
    .lp-shell { padding: 0 56px; }
}

.lp-display {
    font-family: var(--font-display);
    font-weight: 300;
    letter-spacing: -0.01em;
}

.lp-eyebrow {
    display: flex;
    align-items: center;
    gap: 14px;
    font: 400 10px/1 var(--font-sans);
    letter-spacing: .4em;
    text-transform: uppercase;
    color: var(--ctx-accent);
}

.lp-eyebrow__tick {
    display: block;
    width: 42px;
    height: 1px;
    background: linear-gradient(90deg, transparent, var(--ctx-accent));
}

/* The hairline. The only rule the page owns — no borders, no dividers, no boxes. */
.lp-rule {
    display: block;
    width: 92px;
    height: 1px;
    background: linear-gradient(90deg, var(--ctx-rule), transparent);
    transform-origin: left center;
}

@media (prefers-reduced-motion: no-preference) {
    .js .lp-rule.reveal { transform: scaleX(0); }
}

/* Body copy rendered from BlockRenderer, which emits elements and no classes. */
.lp-prose {
    font: 300 16px/1.85 var(--font-sans);
    color: var(--ctx-text-2);
}

.lp-prose p { margin: 0 0 1.1em; }
.lp-prose p:last-child { margin-bottom: 0; }
.lp-prose h2, .lp-prose h3 {
    font-family: var(--font-display);
    font-weight: 300;
    color: var(--ctx-text);
    margin: 1.6em 0 .6em;
}
.lp-prose blockquote {
    margin: 1.6em 0;
    padding-left: 22px;
    border-left: 1px solid var(--ctx-rule);
    font-family: var(--font-display);
    font-size: 1.2em;
    font-style: italic;
    color: var(--ctx-text);
}
.lp-prose a { color: var(--ctx-accent); text-decoration: none; border-bottom: 1px solid currentColor; }

/* ---- The copy column ---------------------------------------------------------
   Eyebrow, title, hairline, paragraph — the lockup the film, the walkthrough, the feature
   stories, the location and the narrative opening all use. It was written out four times in
   four sets of numbers that had drifted apart; these are the site's, taken from the homepage's
   editorial bands so a reader moving from one to the other meets one typographic system.

   The sections keep their own BEM classes for layout and for the odd exception (a tighter
   measure on one title, a wider one on another); what they no longer each own is the type. */

.lp-copy__eyebrow { margin-bottom: 22px; }

/* `--lp-title-step` is a per-section multiplier on the shared size, defaulting to 1 so every
   section that has never heard of it is unchanged. It exists for the section title, which heads a
   run of other sections and has to outrank the titles inside them — see the Section title block
   at the end of this file. A second clamp for that one section is exactly the drift the custom
   sections block warns about, so it multiplies these numbers rather than restating them. */

.lp-copy__title {
    margin: 0;
    font-size: calc(clamp(30px, 3.6vw, 44px) * var(--lp-type-scale) * var(--lp-title-step, 1));
    line-height: 1.14;
    color: var(--ctx-text);
}

.lp-copy__rule { margin: 22px 0 24px; }

.lp-copy__graf {
    margin: 0;
    max-width: 46ch;
    font: 300 15px/1.85 var(--font-sans);
    color: var(--ctx-text-2);
    text-wrap: pretty;
}

/* ---- Split headings ----------------------------------------------------------
   `split-text` masks each line and slides the characters up out of it. Two consequences the
   markup cannot express:

   The mask is `overflow: hidden` on a box the height of one line, so a descender on the last
   line is shaved off. The homepage carries the same correction; these headings need it more,
   because they are set tighter — the hero runs at .96 and the area name at 1.02, where the
   homepage's tightest is 1.12.

   The padding is read back by split_text_controller, which parks each character one line box
   PLUS this headroom below its mask. The two belong together: headroom added here and not
   accounted for there is a band of letter-tops visible before the reveal. See headroom() in
   assets/controllers/split_text_controller.js — it measures this value rather than assuming
   it, so changing .22em needs no change there.

   And the hero's headline carries a 44px text-shadow for legibility over photography. Clipped
   at each line box that reads as a bar of shadow between the lines, so the mask is given room
   on both axes and the space taken back with a matching negative margin. */

.lp [data-controller~="split-text"] .st-line {
    padding-bottom: .22em;
    margin-bottom: -.22em;
}

.lp-hero__headline .st-line {
    padding: 0 .5em .22em;
    margin: 0 -.5em -.22em;
}

/* Balanced line breaks are what `autoSplit` exists to survive — see split_text_controller. */
.lp-display[data-controller~="split-text"] { text-wrap: balance; }

/* ---- The frame that opens ----------------------------------------------------
   The film's stage and the walkthrough's frame enter as an inset card and open out to their
   column as the spread crosses the viewport. frame_open_controller writes one number,
   `--lp-frame-open`, and every geometry decision is here — a clip-path string does not
   interpolate reliably across browsers, and a scale would zoom the picture rather than widen
   the window onto it. The frame is a window, not a picture.

   The static value is the finished state, so reduced motion, a touch pointer, the composer's
   static preview and a chunk that never arrived all land on an open frame rather than on a
   card that never opens. This is the entrance for these two frames — neither is also a
   `.reveal` item, because a fade underneath the opening is two entrances for one object. */

.lp-film__stage,
.lp-tour {
    --lp-frame-open: 1;
    clip-path: inset(
        calc((1 - var(--lp-frame-open)) * 6%)
        calc((1 - var(--lp-frame-open)) * 10%)
        round calc((1 - var(--lp-frame-open)) * 3px)
    );
}

/* Stacked, 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 margin undoes `.lp-shell`'s own 24px
   rather than fighting it from a wider ancestor. Nothing scrubs at this width — the media is
   already at full bleed and an expansion would have nothing left to reveal. */
@media (max-width: 899.98px) {
    .lp-film__stage,
    .lp-tour {
        margin-inline: -24px;
        width: calc(100% + 48px);
    }
}

/* ============================================================
   Hero
   ============================================================ */

.lp-hero {
    position: relative;
    /* The full screen, at every width. It used to stop short — 78vh, 92vh above 900px — from
       the composition it was given when the site nav still sat above it. The nav and its
       spacer are gone (see page.html.twig), and what the shortfall left behind was a strip of
       the next section showing under the photograph.

       `svh` with a `vh` fallback, matching `.ar-hero--full`: 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. `dvh` is worse than either — it would resize the
       stage as the chrome slides away and fight the scrubbed parallax.

       `min-height` rather than `height`: the copy is centred flex content, and a long headline
       on a narrow screen should grow the stage rather than be clipped by it. */
    min-height: 100vh;
    min-height: 100svh;
    display: flex;
    /* Centred, not bottom-weighted: the copy block sits at the optical centre (its own
       padding-bottom biases it slightly high) and the fact band takes the bottom edge. */
    align-items: center;
    overflow: hidden;
}

/* The backdrop, and the element the parallax scrubs.
 *
 * Sized to hero_parallax_controller's contract rather than to the hero: for an overshoot of
 * O percent the frame must be `top: -O%` / `height: (100 + O)%`, and the scrub then travels
 * `100 * O / (100 + O)` — both ends covering 0..100% exactly, so no grey band can open above
 * or below the photograph at any scroll position. O is 18 here, the homepage's default.
 *
 * The frame moves rather than the images inside it, which is what lets one rule serve all
 * three hero variants: Ken Burns transforms its `.lp-hero__layer` children and the ambient
 * poster fills the frame flat, so scrubbing their shared parent composes with both instead of
 * overwriting either. `.lp-hero`'s own `overflow: hidden` is what clips the overshoot. */
.lp-hero__frame {
    position: absolute;
    top: -18%;
    left: 0;
    right: 0;
    height: 118%;
    overflow: hidden;
}

/* The Ken Burns rotation's frames.
 *
 * Inset beyond the frame so the pan has somewhere to travel without exposing an edge — the
 * same trick the still hero plays inline on its single image, with the headroom moved here
 * because three layers should not each restate it.
 *
 * Only the first is painted. The others are raised to full opacity by kenburns_controller.js,
 * which is also the reason the reduced-motion rule below removes them from the box entirely
 * rather than leaving them transparent: a controller that declines to run must leave a hero
 * that looks composed on purpose, not one photograph with two invisible ones stacked on it. */
.lp-hero__layer {
    position: absolute;
    inset: -3%;
    width: 106%;
    height: 106%;
    opacity: 0;
    will-change: transform, opacity;
}
.lp-hero__layer:first-child { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
    .lp-hero__layer { will-change: auto; }
    .lp-hero__layer:not(:first-child) { display: none; }
}

/* Neutral shadow rather than palette: a scrim exists to make text legible over a
   photograph, and tinting it would push the photograph's own colour around. */
/* The hero's legibility gradient. The stops are the theme's, not this rule's: a light page
   needs the photograph darkened under the copy and an evening page needs a lighter hand, and
   that is a property of the theme rather than of the hero. */
.lp-hero__scrim {
    position: absolute;
    inset: 0;
    background:
        linear-gradient(to right,
            var(--lp-hero-scrim-left) 0%,
            var(--lp-hero-scrim-left-mid) 36%,
            transparent 68%),
        linear-gradient(to top,
            var(--lp-hero-scrim-bottom) 0%,
            transparent 30%);
    /* The vignette: the photograph falls off into shadow at all four edges instead of ending
       at them. The homepage masthead has carried this since it was built, and the same blur
       and spread are used here so the two read as one system rather than as two heroes that
       happen to be dark.

       Sits on the scrim rather than on a fourth overlay div — this element is already a
       full-bleed `inset: 0` box, and an inset shadow costs it nothing.

       How dark is the theme's call, exactly as the gradient stops above are: an evening page
       whose surround is already dark wants a lighter hand than an ivory one. */
    box-shadow: inset 0 0 200px 70px var(--lp-hero-vignette);
}

.lp-hero__copy {
    position: relative;
    width: 100%;
    /* Full-bleed to the page gutter, not centred in a column.
     *
     * It used to be left-aligned inside a centred 1240px block, which on a wide screen put the
     * headline some 400px in from the edge — while the fact band directly beneath it was
     * already flush to the 56px gutter. One hero, two left edges. The gutter and the
     * breakpoint below are `.lp-hero__band`'s, deliberately: sharing the values is what keeps
     * the two aligned when either is next tuned.
     *
     * Vertically centred, biased a touch above true centre (the padding-bottom is the bias):
     * the fact band owns the bottom edge, and copy at the optical ~46% never crowds it. */
    padding: 0 24px 14svh;
    /* The hero copy always sits on the scrim, never on the theme ground, so it is ivory on
       every register — the one place a fixed value is correct. */
    color: var(--lp-media-ink);
}

@media (min-width: 700px) {
    .lp-hero__copy { padding: 0 56px 14svh; }
}

.lp-hero__eyebrow { margin-bottom: 26px; color: var(--lp-media-accent); }
.lp-hero__eyebrow .lp-eyebrow__tick { background: linear-gradient(90deg, transparent, var(--lp-media-accent)); }

.lp-hero__headline {
    margin: 0;
    font-size: calc(clamp(44px, 8.5vw, 104px) * var(--lp-type-scale));
    line-height: .96;
    color: var(--lp-media-ink);
    /* stylelint-disable-next-line color-no-hex, color-named, declaration-property-value-disallowed-list -- neutral shadow beneath type on a photograph, not palette: it darkens the image, and a themed value here would tint the picture rather than the text. */
    text-shadow: 0 2px 44px rgba(0, 0, 0, .55);
    max-width: 16ch;
}

.lp-hero__location {
    margin: 26px 0 0;
    font: 300 12px/1.6 var(--font-sans);
    letter-spacing: .24em;
    text-transform: uppercase;
    color: color-mix(in srgb, var(--lp-media-ink) 80%, transparent);
}

/* The fact band. Along the hero's bottom edge beneath a full-width hairline: up to four
   cells, a letterspaced micro-caps label over a serif value at display size, vertical
   hairlines between them. The values are decisively larger than body text and still quiet —
   one face, regular weight, the same ivory for every fact including the price. `--cream`
   rather than a theme token because this sits over a photograph, not over the page — the
   same reason the hero's own copy does. */
.lp-hero__band {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    /* The bottom padding is clearance, not rhythm. The band used to sit on the bottom edge of
       a 92vh hero, which left the last 8vh of the window under it; on a full-screen stage that
       margin is gone and the values were welded to the sill of the browser window. This lifts
       them back off it. */
    padding: 0 24px clamp(14px, 2.4vh, 36px);
}

@media (min-width: 700px) {
    .lp-hero__band { padding: 0 56px clamp(14px, 2.4vh, 36px); }
}

.lp-hero__bandrule {
    display: block;
    height: 1px;
    background: linear-gradient(90deg, color-mix(in srgb, var(--lp-media-ink) 50%, transparent), color-mix(in srgb, var(--lp-media-ink) 6%, transparent));
    /* Draws itself in from the left, then the cells rise — the last beat of the entrance. */
    transform-origin: left center;
}

.js-motion .lp-hero__bandrule.reveal { transform: scaleX(0) translateY(0); }

/* One row of equal cells; whatever facts the listing lacks simply drop their cell and the
   hairline count re-flows. A single fact is a single cell with no dividers — the borders
   below only ever sit between siblings. */
.lp-hero__bandgrid {
    margin: 0;
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
}

.lp-hero__bandcell {
    /* The mask: the inner block rises through this edge. */
    overflow: hidden;
    padding: clamp(16px, 2.2vw, 26px) clamp(16px, 2.4vw, 32px) clamp(18px, 2.4vw, 30px);
}

.lp-hero__bandcell:first-child { padding-left: 0; }

.lp-hero__bandcell + .lp-hero__bandcell {
    border-left: 1px solid color-mix(in srgb, var(--lp-media-ink) 22%, transparent);
}

.lp-hero__bandlabel {
    margin: 0 0 8px;
    font: 400 10px/1 var(--font-sans);
    letter-spacing: .34em;
    text-transform: uppercase;
    color: color-mix(in srgb, var(--lp-media-ink) 62%, transparent);
}

.lp-hero__bandvalue {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 300;
    font-size: calc(clamp(22px, 2.4vw, 40px) * var(--lp-type-scale));
    line-height: 1.05;
    color: color-mix(in srgb, var(--lp-media-ink) 95%, transparent);
    white-space: nowrap;
}

/* A 2×2 grid on a phone — a hairline cross rather than a squeezed row — and a single column
   below 360px, where the borders become rules between stacked cells. */
@media (max-width: 699px) {
    .lp-hero__bandgrid {
        grid-auto-flow: row;
        grid-template-columns: repeat(2, 1fr);
    }

    .lp-hero__bandcell { padding: 14px 16px; }
    .lp-hero__bandcell:nth-child(odd) { padding-left: 0; }
    .lp-hero__bandcell + .lp-hero__bandcell { border-left: 0; }
    .lp-hero__bandcell:nth-child(even) { border-left: 1px solid color-mix(in srgb, var(--lp-media-ink) 22%, transparent); }
    .lp-hero__bandcell:nth-child(n+3) { border-top: 1px solid color-mix(in srgb, var(--lp-media-ink) 22%, transparent); }

    .lp-hero__bandvalue { font-size: calc(clamp(18px, 5vw, 22px) * var(--lp-type-scale)); }
}

@media (max-width: 359px) {
    .lp-hero__bandgrid { grid-template-columns: 1fr; }

    .lp-hero__bandcell { padding-left: 0; }
    .lp-hero__bandcell:nth-child(even) { border-left: 0; }
    .lp-hero__bandcell:nth-child(n+2) { border-top: 1px solid color-mix(in srgb, var(--lp-media-ink) 22%, transparent); }
}

/* ============================================================
   Narrative opening
   ============================================================
   The prose, and optionally the features beside it. Same spread as .lp-film and .lp-plans, with
   one difference that matters: the columns align to their tops rather than their centres. Those
   two sections pair a paragraph with a frame of fixed proportion, where centring reads as
   deliberate; here both columns are type, and type whose first lines do not sit on the same
   baseline reads as a mistake.

   With no features column the spread is a single flex child and lays out exactly as the
   full-width section always did. */

.lp-narrative__spread {
    display: flex;
    flex-direction: column;
    gap: clamp(28px, 4vw, 52px);
}

@media (min-width: 900px) {
    .lp-narrative__spread { flex-direction: row; align-items: flex-start; gap: clamp(40px, 5vw, 88px); }

    .lp-narrative__main { flex: 1 1 56%; }
    .lp-narrative__aside { flex: 1 1 38%; }
}

/* The anchor. The section used to open on a hairline and a paragraph floating in the band,
   which reads as a caption that has lost its picture; the eyebrow and title give the story the
   same footing the film and the feature stories have. Both are optional, so a page written
   before they existed keeps its old composition exactly. */
.lp-narrative__head { margin-bottom: clamp(26px, 3.6vw, 44px); }

.lp-narrative__title { max-width: 18ch; }

/* Set as `features-prose`'s lead-in never was: the page's own eyebrow lockup, tick and all.
   Standing beside prose, a small grey sentence read as a caption belonging to the paragraph on
   the left; a label reads as the head of its own column. */
.lp-narrative__featureshead { margin: 0 0 16px; }

/* One column, always — see the partial for why. The items keep `.lp-features__item`: the
   hairline separators are brand law and do not change because the list moved. */
.lp-narrative__features {
    list-style: none;
    margin: 0;
    padding: 0;
    border-top: 1px solid color-mix(in srgb, var(--ctx-rule) 34%, transparent);
}

/* The rule above the prose is the section's opening gesture and the features head is aligned
   to it, so the paragraph below does not also need the full drop. Under a title it is the
   second gesture rather than the first, and takes its air from the head above instead. */
.lp-narrative__rule { margin-bottom: clamp(28px, 4vw, 44px); }

.lp-narrative__body {
    margin: 0;
    max-width: 22ch;
    font-size: calc(clamp(26px, 3.4vw, 40px) * var(--lp-type-scale));
    line-height: 1.34;
    color: var(--ctx-text);
}

@media (min-width: 700px) {
    .lp-narrative__body { max-width: 30ch; }
}

/* The lede — the first paragraph, set in the serif a size above the rest.

   With a title standing over it, the old 40px display paragraph is a second headline and the
   two argue; brought down to lede size it becomes what it should have been, the opening of the
   prose rather than a rival to the line above it. The rest of the description follows in the
   sans at reading size, which is the magazine's own order: nameplate, standfirst, body.

   `first-of-type`, not `first-child`: a description may open with a heading or a hairline —
   `DescriptionTruncator` keeps whatever the first block is — and the lede is the first
   paragraph after it, not whatever happens to come first. */
.lp-narrative--titled .lp-narrative__body,
.lp-narrative--titled .lp-narrative__prose > p:first-of-type {
    font-family: var(--font-display);
    font-weight: 300;
    font-size: calc(clamp(21px, 2.4vw, 28px) * var(--lp-type-scale));
    line-height: 1.42;
    color: var(--ctx-text);
}

@media (min-width: 700px) {
    .lp-narrative--titled .lp-narrative__body { max-width: 42ch; }
}

.lp-narrative__quote {
    margin: clamp(32px, 5vw, 56px) 0 0;
    padding-left: 26px;
    border-left: 1px solid var(--ctx-rule);
    max-width: 34ch;
    font-size: calc(clamp(17px, 1.9vw, 22px) * var(--lp-type-scale));
    font-style: italic;
    line-height: 1.5;
    color: var(--ctx-text-2);
}

/* Consecutive paragraphs in the truncated variant. The first keeps `.lp-narrative__body`'s
   zero top margin; the rest need air between them, and a `+` selector says that without a
   second class in the markup. */
.lp-narrative__body + .lp-narrative__body { margin-top: clamp(20px, 2.4vw, 32px); }

/* CONTINUE READING. A text link with a hairline under it — the same letterspaced treatment as
   the gallery's filter index, because they are the same kind of thing: a word you press. */
.lp-readmore {
    display: inline-block;
    margin-top: clamp(28px, 4vw, 44px);
    padding: 0 0 8px;
    background: none;
    border: 0;
    border-bottom: 1px solid var(--ctx-rule);
    cursor: pointer;
    font: 400 10px/1 var(--font-sans);
    letter-spacing: .4em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
    transition: color var(--duration-tint) var(--ease-drift),
                border-color var(--duration-tint) var(--ease-drift);
}

.lp-readmore:hover {
    color: var(--ctx-text);
    border-color: var(--ctx-text);
}

/* ============================================================
   Reading panel
   ============================================================
   Shared surface — the description today, a long area essay later. Desktop: a panel from the
   right. Mobile: the same sheet rising from the bottom. One set of markup; the axis is the
   only difference, which is why the controller contains no breakpoint. */

.lp-panel {
    position: fixed;
    inset: 0;
    z-index: 70;
}

.lp-panel[hidden] { display: none; }

.lp-panel__scrim {
    position: absolute;
    inset: 0;
    /* A photographic scrim over the page, not a themed surface — the documented exception to
       the no-literals rule at the top of this file. It must dim a dark register as reliably
       as a light one, and a token that does both is a token that does neither well. */
    background: var(--ctx-scrim);
    opacity: 0;
    transition: opacity .5s var(--ease-drift);
}

.lp-panel[data-open] .lp-panel__scrim { opacity: 1; }

/* The sheet takes the page's panel surface rather than the band's, and that distinction is
   the reason --lp-panel-* is a page token: a panel opened from the film's cinema band is not
   a near-black sheet, it is the page's reading surface arriving over one. */
.lp-panel__sheet {
    position: absolute;
    display: flex;
    flex-direction: column;
    background: var(--lp-panel-surface);
    color: var(--lp-panel-text);
    left: 0;
    right: 0;
    bottom: 0;
    max-height: 88svh;
    padding: clamp(24px, 5vw, 40px) clamp(22px, 6vw, 44px) clamp(32px, 6vw, 48px);
    transform: translateY(100%);
    /* Expo-out: almost all of the distance is covered early and the last few pixels take their
       time, which is what makes a large surface feel weighted rather than thrown. */
    transition: transform .5s cubic-bezier(.16, 1, .3, 1);
}

.lp-panel[data-open] .lp-panel__sheet { transform: none; }

@media (min-width: 900px) {
    .lp-panel__sheet {
        left: auto;
        top: 0;
        bottom: 0;
        width: min(var(--lp-panel-width, 520px), 92vw);
        max-height: none;
        padding: clamp(48px, 5vw, 72px) clamp(40px, 4vw, 60px);
        transform: translateX(100%);
    }

    /* ---- Mode: left. An exact mirror, and nothing more. Same width, same easing, same scrim,
       same focus trap — the only thing reversed is which edge it comes from. */
    .lp-panel--left .lp-panel__sheet {
        left: 0;
        right: auto;
        transform: translateX(-100%);
    }
    .lp-panel--left[data-open] .lp-panel__sheet { transform: none; }

    /* ---- Mode: split. A full-viewport takeover in two halves.
       One gesture, not two: the text descends from the top while the photograph rises through
       its mask 80ms later. The offset is what makes it read as choreography rather than a
       symmetric wipe, and it is small enough that nobody could name it.

       `100svh` rather than `100vh` — on iOS the large viewport unit puts the bottom of the
       image under the URL bar, which is exactly where a full-height photograph should not end. */
    .lp-panel--split {
        display: grid;
        grid-template-columns: 1fr 1fr;
        overflow: hidden;
    }

    .lp-panel--split .lp-panel__sheet {
        position: relative;
        left: auto;
        right: auto;
        top: auto;
        bottom: auto;
        width: auto;
        height: 100svh;
        transform: translateY(-100%);
        transition: transform 1s cubic-bezier(.16, 1, .3, 1);
    }
    .lp-panel--split[data-open] .lp-panel__sheet { transform: none; }

    .lp-panel--split .lp-panel__image {
        position: relative;
        height: 100svh;
        overflow: hidden;
        background: var(--ctx-surface);
        clip-path: inset(100% 0 0 0);
        transition: clip-path 1s cubic-bezier(.16, 1, .3, 1) 80ms;
    }
    .lp-panel--split[data-open] .lp-panel__image { clip-path: inset(0 0 0 0); }

    /* Once settled, the same barely-there drift the still hero has. The image is a held frame,
       not a slide. */
    .lp-panel--split .lp-panel__image img { transition: transform 12s linear 1s; }
    .lp-panel--split[data-open] .lp-panel__image img { transform: scale(1.03); }

    /* The side swap is a column reorder, so the text stays first in the document and the
       reading order never depends on which side it was drawn on. */
    .lp-panel--split.lp-panel--text-right .lp-panel__sheet { order: 2; }
    .lp-panel--split.lp-panel--text-right .lp-panel__image { order: 1; }

    /* Exit reverses faster than it entered — leaving is not a performance. */
    .lp-panel--split:not([data-open]) .lp-panel__sheet,
    .lp-panel--split:not([data-open]) .lp-panel__image { transition-duration: .5s; transition-delay: 0s; }
}

/* Below the desktop breakpoint every mode is the shipped bottom sheet, and the split's image
   column is simply not drawn. Two 50%-wide columns on a phone is two things too small to read
   rather than one composition; the image returning as a static header panel in the sheet is
   noted as future polish. */
@media (max-width: 899px) {
    .lp-panel__image { display: none; }
}

.lp-panel__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 20px;
    margin-bottom: 18px;
}

.lp-panel__title {
    margin: 0;
    font: 400 10px/1.4 var(--font-sans);
    letter-spacing: .4em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
}

.lp-panel__close {
    flex: 0 0 auto;
    background: none;
    border: 0;
    padding: 0 0 0 12px;
    font-size: 20px;
    line-height: 1;
    cursor: pointer;
    color: var(--ctx-text-2);
    transition: color var(--duration-tint) var(--ease-drift);
}

.lp-panel__close:hover { color: var(--ctx-text); }

/* Scrolls independently of the page behind it, which is frozen. `overscroll-behavior` is what
   stops a flick at the end of the description handing the scroll back to the document —
   without it the page moves underneath an overlay that was supposed to have stopped it. */
.lp-panel__body {
    margin-top: clamp(24px, 3vw, 36px);
    overflow-y: auto;
    overscroll-behavior: contain;
}

.lp-panel__body > :last-child { margin-bottom: 0; }

/* The bound description at reading size, larger than the page's body prose because a panel is
   a place someone chose to go and read. `.lp-prose` supplies the rest. */
.lp-panel__body p {
    font-size: calc(clamp(16px, 1.5vw, 19px) * var(--lp-type-scale));
    line-height: 1.85;
}

/* The truncated opening. Set as prose rather than at the narrative display size: it is now
   several paragraphs of the agent's own writing with its own pull quotes and lists, and 40px
   serif over three paragraphs is a headline, not an opening. */
.lp-narrative__prose {
    max-width: 62ch;
    font-size: calc(clamp(16px, 1.6vw, 19px) * var(--lp-type-scale));
}

/* Instant open and close. The panel still appears and still traps focus — only the travel is
   removed, because the travel is the part that causes trouble. */
@media (prefers-reduced-motion: reduce) {
    .lp-panel__scrim,
    .lp-panel__sheet,
    .lp-panel__image,
    .lp-panel__image img { transition: none; }
    .lp-panel__sheet,
    .lp-panel__image img { transform: none; }
    .lp-panel__image { clip-path: none; }
}

/* ============================================================
   Detail grid — the hero fact band's quieter sibling
   ============================================================
   Same anatomy as the band (micro-caps label over a serif value, hairlines between cells),
   a register down: smaller values, theme tokens instead of over-photograph ivory. */

.lp-details__grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0;
    margin: 0;
    border-top: 1px solid color-mix(in srgb, var(--ctx-rule) 42%, transparent);
}

@media (min-width: 700px) { .lp-details__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1000px) { .lp-details__grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

.lp-details__fact {
    padding: clamp(22px, 3vw, 34px) clamp(18px, 2.4vw, 30px) clamp(24px, 3.2vw, 38px);
    border-bottom: 1px solid color-mix(in srgb, var(--ctx-rule) 28%, transparent);
}

/* Vertical hairlines between neighbours only — first column starts clean at the margin, and
   the border set matches however many columns the viewport gives the grid. */
.lp-details__fact { border-left: 1px solid color-mix(in srgb, var(--ctx-rule) 28%, transparent); }
.lp-details__fact:nth-child(2n+1) { border-left: 0; }
.lp-details__fact:nth-child(odd) { padding-left: 0; }

@media (min-width: 700px) {
    .lp-details__fact,
    .lp-details__fact:nth-child(2n+1) { border-left: 1px solid color-mix(in srgb, var(--ctx-rule) 28%, transparent); padding-left: clamp(18px, 2.4vw, 30px); }
    .lp-details__fact:nth-child(3n+1) { border-left: 0; padding-left: 0; }
}

@media (min-width: 1000px) {
    .lp-details__fact,
    .lp-details__fact:nth-child(3n+1) { border-left: 1px solid color-mix(in srgb, var(--ctx-rule) 28%, transparent); padding-left: clamp(18px, 2.4vw, 30px); }
    .lp-details__fact:nth-child(4n+1) { border-left: 0; padding-left: 0; }
}

.lp-details__label {
    margin: 0 0 10px;
    font: 400 9.5px/1 var(--font-sans);
    letter-spacing: .3em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
}

/* Price gets exactly this treatment and no other: same size, same weight, same colour as
   every other fact. Never bold, never accented — brand law, and the single easiest rule on
   this page to break by accident. */
.lp-details__value {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 300;
    font-size: calc(clamp(20px, 2vw, 28px) * var(--lp-type-scale));
    line-height: 1.2;
    color: var(--ctx-text);
}

/* ============================================================
   Editorial gallery — alternating spreads
   ============================================================ */

.lp-gallery--spreads {
    display: flex;
    flex-direction: column;
    gap: clamp(48px, 7vw, 104px);
}

.lp-spread { margin: 0; }

.lp-spread__frame {
    position: relative;
    overflow: hidden;
    aspect-ratio: 4 / 3;
}

@media (min-width: 900px) {
    .lp-spread { width: 62%; }
    /* The offset is what makes this a spread rather than a column: each image hangs from
       its own edge and they never line up. */
    .lp-spread--left { align-self: flex-start; }
    .lp-spread--right { align-self: flex-end; }
    .lp-spread--right .lp-spread__caption { text-align: right; }
    .lp-spread__frame { aspect-ratio: 3 / 2; }
}

.lp-spread__caption {
    margin: 16px 0 0;
    font: 300 12px/1.7 var(--font-sans);
    letter-spacing: .12em;
    color: var(--ctx-text-2);
    max-width: 48ch;
}

/* ============================================================
   Editorial gallery — stacked full-bleed
   ============================================================ */

.lp-gallery--stacked {
    display: flex;
    flex-direction: column;
    gap: clamp(40px, 6vw, 92px);
}

.lp-stacked { margin: 0; }

.lp-stacked__frame {
    position: relative;
    overflow: hidden;
    aspect-ratio: 3 / 2;
}

@media (min-width: 900px) {
    .lp-stacked__frame { aspect-ratio: 21 / 9; }
}

.lp-stacked__caption {
    margin: 16px auto 0;
    font: 300 12px/1.7 var(--font-sans);
    letter-spacing: .12em;
    color: var(--ctx-text-2);
}

/* ============================================================
   Editorial gallery — horizontal chapter
   ============================================================
   Authored as a plain vertical stack. The controller adds pinning on top; it never runs on
   touch or under reduced motion, so this is the real markup rather than a fallback that has
   to be restored. */

.lp-chapter__viewport { overflow: hidden; }

.lp-chapter__track {
    display: flex;
    flex-direction: column;
    gap: clamp(40px, 6vw, 92px);
}

.lp-chapter__panel { margin: 0; }

.lp-chapter__frame {
    position: relative;
    overflow: hidden;
    aspect-ratio: 3 / 2;
}

.lp-chapter__caption {
    margin: 16px 24px 0;
    font: 300 12px/1.7 var(--font-sans);
    letter-spacing: .12em;
    color: var(--ctx-text-2);
}

/* Pinned mode — applied by horizontal_chapter_controller.js on pointer:fine only. */
.lp-chapter[data-pinned] .lp-chapter__viewport { height: 100vh; display: flex; align-items: center; }
.lp-chapter[data-pinned] .lp-chapter__track {
    flex-direction: row;
    gap: clamp(24px, 3vw, 56px);
    padding: 0 clamp(24px, 6vw, 96px);
    will-change: transform;
}
.lp-chapter[data-pinned] .lp-chapter__panel { flex: 0 0 auto; width: min(74vw, 1080px); }
.lp-chapter[data-pinned] .lp-chapter__frame { aspect-ratio: 3 / 2; height: 68vh; width: 100%; }
.lp-chapter[data-pinned] .lp-chapter__caption { margin-left: 0; margin-right: 0; }

/* ============================================================
   Image gallery — the full photographic record
   ============================================================
   A repeating three-image figure: one full width, then a two-up row split 58/38. The point is
   that no two consecutive rows are the same shape, so a set of sixty photographs reads as
   spreads rather than as a contact sheet. The rhythm is assigned in Twig (`loop.index0 % 3`)
   and the widths live here, because this section can be dragged anywhere in the stack and its
   layout must not depend on what happens to sit above it. */

.lp-imagegallery__index {
    display: flex;
    flex-wrap: wrap;
    gap: clamp(18px, 2.4vw, 34px);
    margin: clamp(24px, 3vw, 40px) 0 clamp(32px, 4vw, 56px);
}

/* Typography, not chrome. The hairline under the active item is the only state indicator —
   no fill, no border, no pill. */
.lp-imagegallery__filter {
    position: relative;
    padding: 0 0 8px;
    background: none;
    border: 0;
    cursor: pointer;
    font: 400 10px/1 var(--font-sans);
    letter-spacing: .4em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
    transition: color var(--duration-tint) var(--ease-drift);
}

.lp-imagegallery__filter::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 1px;
    background: var(--ctx-rule);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform var(--duration-move) var(--ease-drift);
}

.lp-imagegallery__filter:hover { color: var(--ctx-text); }
.lp-imagegallery__filter[aria-pressed="true"] { color: var(--ctx-text); }
.lp-imagegallery__filter[aria-pressed="true"]::after { transform: scaleX(1); }

/* The count rides above the baseline at half size — a folio number, not a badge. */
.lp-imagegallery__count {
    margin-left: .6em;
    font-size: 8px;
    letter-spacing: .2em;
    vertical-align: .5em;
    color: var(--ctx-text-2);
}

.lp-imagegallery__grid {
    display: flex;
    flex-wrap: wrap;
    gap: clamp(12px, 1.6vw, 28px);
}

.lp-imagegallery__tile {
    margin: 0;
    flex: 0 0 100%;
}

.lp-imagegallery__open {
    position: relative;
    display: block;
    width: 100%;
    padding: 0;
    border: 0;
    background: none;
    cursor: zoom-in;
    overflow: hidden;
    aspect-ratio: 3 / 2;
}

.lp-imagegallery__open img { transition: transform var(--duration-drift) var(--ease-drift); }
.lp-imagegallery__open:hover img { transform: scale(1.03); }

@media (min-width: 900px) {
    .lp-imagegallery__tile--wide { flex: 0 0 100%; }
    /* 58/38 with the gutter between them — deliberately not 60/40, because two panels that
       nearly halve the row read as a split and two that clearly do not read as a composition. */
    .lp-imagegallery__tile--major { flex: 0 0 calc(58% - clamp(6px, .8vw, 14px)); }
    .lp-imagegallery__tile--minor { flex: 1 1 calc(38% - clamp(6px, .8vw, 14px)); }

    .lp-imagegallery__tile--wide .lp-imagegallery__open { aspect-ratio: 21 / 9; }
    .lp-imagegallery__tile--minor .lp-imagegallery__open { aspect-ratio: 4 / 5; }
}

/* Filtered out. Kept in the DOM rather than removed so the controller can animate the exit
   and so a filter change costs no layout thrash beyond the reflow it actually needs. */
.lp-imagegallery__tile[hidden] { display: none; }

/* Capped — in the current room but past the initial count. Distinct from `hidden` on
   purpose: a capped tile is still in the filtered set (the lightbox reaches it), it is just
   not displayed until VIEW ALL. gallery_filter_controller.js owns the attribute. */
.lp-imagegallery__tile[data-capped] { display: none; }

/* The VIEW ALL control: the section library's one vocabulary for "do something" — a
   letterspaced text link over a hairline that draws in on hover. Never a box. */
.lp-imagegallery__more {
    display: flex;
    justify-content: center;
    margin-top: clamp(36px, 5vw, 64px);
}

.lp-imagegallery__more[hidden] { display: none; }

.lp-imagegallery__viewmore {
    position: relative;
    padding: 0 0 10px;
    background: none;
    border: 0;
    cursor: pointer;
    font: 400 11px/1 var(--font-sans);
    letter-spacing: .38em;
    text-transform: uppercase;
    color: var(--ctx-text);
    transition: color var(--duration-tint) var(--ease-drift);
}

.lp-imagegallery__viewmore::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 1px;
    background: var(--ctx-rule);
    transform: scaleX(.4);
    transform-origin: left center;
    transition: transform var(--duration-move) var(--ease-drift);
}

.lp-imagegallery__viewmore:hover { color: var(--ctx-accent); }
.lp-imagegallery__viewmore:hover::after { transform: scaleX(1); }

/* ---- Layout: classic four column ----------------------------------------------------------
   Grid rather than the editorial flex row, because the whole point is that every tile is the
   same size and nothing has a rhythm. The tiles share `.lp-imagegallery__open`'s 3:2 crop and
   its ≤1.03 hover, so the register changes and the vocabulary does not. No borders and no
   background: the gutters carry the structure, and a boxed tile is a thumbnail. */
.lp-imagegallery__grid--uniform {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
}
.lp-imagegallery__tile--uniform { flex: none; }

@media (min-width: 820px) {
    .lp-imagegallery__grid--uniform { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 1200px) {
    .lp-imagegallery__grid--uniform { grid-template-columns: repeat(4, 1fr); }
}

/* ---- Layout: carousel ---------------------------------------------------------------------
   A native horizontal scroller. `scroll-snap` and the momentum are the platform's; the
   controller only eases the prev/next jumps and keeps the counter honest.

   Height is a share of the *small* viewport unit: `100vh` on iOS puts the bottom of the strip
   under the URL bar, and a gallery you have to scroll to finish looking at one image of is not
   the composition that was drawn. */
.lp-carousel { position: relative; }

.lp-carousel__track {
    display: flex;
    gap: 1px;
    height: calc(var(--lp-carousel-h, 70) * 1svh);
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    background: var(--ctx-rule);
}
.lp-carousel__track::-webkit-scrollbar { display: none; }
.lp-carousel__track:focus-visible { outline: 1px solid var(--ctx-accent); outline-offset: -1px; }

.lp-carousel__slide {
    margin: 0;
    flex: 0 0 auto;
    height: 100%;
    scroll-snap-align: start;
}
.lp-carousel__slide[hidden] { display: none; }

.lp-carousel__open {
    display: block;
    height: 100%;
    padding: 0;
    border: 0;
    background: none;
    cursor: zoom-in;
    overflow: hidden;
}
.lp-carousel__open img { transition: transform var(--duration-drift) var(--ease-drift); }
.lp-carousel__open:hover img { transform: scale(1.03); }

/* Mid-drag the cursor says "I am holding the strip", and a click on a photograph is swallowed
   by the controller rather than opening the lightbox. */
.lp-carousel[data-dragging] .lp-carousel__open { cursor: grabbing; }
.lp-carousel[data-dragging] .lp-carousel__open img { transform: none; }

.lp-carousel__chrome {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 24px;
    padding-top: 18px;
}

/* The folio number, in the same register as the filter index's counts. */
.lp-carousel__counter {
    margin: 0;
    font-size: 10px;
    letter-spacing: .24em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
}

.lp-carousel__nav { display: flex; gap: 28px; }

/* Text links with a hairline that draws in, exactly like the filter index. Never a round
   button, never an icon in a circle — the affordance is the word. */
.lp-carousel__step {
    position: relative;
    padding: 0 0 4px;
    border: 0;
    background: none;
    cursor: pointer;
    font-size: 10px;
    letter-spacing: .24em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
    transition: color var(--duration-drift) var(--ease-drift);
}
.lp-carousel__step::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 1px;
    background: currentColor;
    transform: scaleX(0);
    transform-origin: left;
    transition: transform var(--duration-drift) var(--ease-drift);
}
.lp-carousel__step:hover { color: var(--ctx-text); }
.lp-carousel__step:hover::after { transform: scaleX(1); }
.lp-carousel__step:disabled { opacity: .35; cursor: default; }
.lp-carousel__step:disabled:hover::after { transform: scaleX(0); }

@media (prefers-reduced-motion: reduce) {
    .lp-carousel__track { scroll-behavior: auto; }
    .lp-carousel__open img,
    .lp-carousel__step,
    .lp-carousel__step::after { transition: none; }
}

/* ============================================================
   Lightbox
   ============================================================
   The one place on the page allowed a near-opaque field of its own: a photograph shown at
   size needs the page gone, and the theme's own background is not dark enough to do that on a
   light register. The scrim is a photographic device, which is the documented exception to the
   no-literals rule at the top of this file — the chrome over it is all tokens. */

.lp-lightbox {
    position: fixed;
    inset: 0;
    z-index: 80;
    display: flex;
    align-items: center;
    justify-content: center;
}

.lp-lightbox[hidden] { display: none; }

.lp-lightbox__scrim {
    position: absolute;
    inset: 0;
    background: var(--lp-lightbox-field);
    opacity: 0;
    transition: opacity var(--duration-tint) var(--ease-drift);
}

.lp-lightbox[data-open] .lp-lightbox__scrim { opacity: 1; }

.lp-lightbox__stage {
    position: relative;
    margin: 0;
    max-width: min(92vw, 1680px);
    max-height: 84svh;
    opacity: 0;
    transition: opacity var(--duration-move) var(--ease-drift);
}

.lp-lightbox[data-open] .lp-lightbox__stage { opacity: 1; }

.lp-lightbox__image {
    display: block;
    max-width: min(92vw, 1680px);
    max-height: 84svh;
    width: auto;
    height: auto;
    -o-object-fit: contain;
       object-fit: contain;
}

.lp-lightbox__chrome {
    position: absolute;
    inset: 0;
    pointer-events: none;
}

.lp-lightbox__chrome > * { pointer-events: auto; }

.lp-lightbox__counter {
    position: absolute;
    left: clamp(20px, 4vw, 56px);
    bottom: clamp(20px, 4vw, 48px);
    font: 400 10px/1 var(--font-sans);
    letter-spacing: .4em;
    color: var(--lp-media-ink);
}

.lp-lightbox__nav,
.lp-lightbox__close {
    position: absolute;
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    background: none;
    border: 1px solid color-mix(in srgb, var(--lp-media-ink) 28%, transparent);
    border-radius: 50%;
    color: var(--lp-media-ink);
    font-size: 16px;
    cursor: pointer;
    transition: border-color var(--duration-tint) var(--ease-drift);
}

.lp-lightbox__nav:hover,
.lp-lightbox__close:hover { border-color: color-mix(in srgb, var(--lp-media-ink) 70%, transparent); }

.lp-lightbox__nav--prev { left: clamp(12px, 3vw, 44px); top: 50%; transform: translateY(-50%); }
.lp-lightbox__nav--next { right: clamp(12px, 3vw, 44px); top: 50%; transform: translateY(-50%); }
.lp-lightbox__close { right: clamp(12px, 3vw, 44px); top: clamp(12px, 3vw, 36px); }

@media (prefers-reduced-motion: reduce) {
    .lp-lightbox__scrim,
    .lp-lightbox__stage { transition: none; }
}

/* ============================================================
   Media lightbox
   ============================================================
   The same device as the lightbox above, for a player rather than a photograph — and a
   separate one, because closing this has to destroy an iframe and there is nothing here to
   page through. One surface serves both players a property page can carry, the film and the
   3D walkthrough; they are opened from different sections and never at once.

   Above the photograph lightbox at 80 rather than beside it: they are opened from different
   sections and never at once, but a shared number would leave which one wins to document
   order. Nothing else on this page is higher — the property page renders no nav. */

.lp-mediabox {
    position: fixed;
    inset: 0;
    z-index: 85;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: clamp(20px, 5vw, 64px);
    opacity: 0;
}

.lp-mediabox[hidden] { display: none; }
.lp-mediabox[data-open] { opacity: 1; }

@media (prefers-reduced-motion: no-preference) {
    .lp-mediabox { transition: opacity var(--duration-tint) var(--ease-drift); }
}

.lp-mediabox__scrim {
    position: absolute;
    inset: 0;
    background: var(--lp-lightbox-field);
}

.lp-mediabox__stage {
    position: relative;
    width: min(1400px, 100%);
}

/* The box is 16:9 before anything is put in it, so activation shifts nothing — and the player
   cannot end up in a container with no height, which is exactly what used to happen when the
   film played inside the section. */
.lp-mediabox__player {
    position: relative;
    aspect-ratio: 16 / 9;
    /* stylelint-disable-next-line color-no-hex, declaration-property-value-disallowed-list -- the letterbox behind a film, not a surface in the palette. A player's own bars are black in every register, and a themed tint behind a video grades it. */
    background: #0a0a0a;
}

/* A widescreen letterbox on a portrait phone leaves a walkthrough about as tall as a
   postcard, which is not something anyone can explore — the same correction the section's own
   frame makes, carried into the overlay. A film keeps 16:9 at every width: that is the shape
   it was shot in. */
@media (max-width: 640px) {
    .lp-mediabox[data-key="tour"] .lp-mediabox__player { aspect-ratio: 4 / 3; }
}

.lp-mediabox__frame {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
}

.lp-mediabox__close {
    position: absolute;
    right: clamp(12px, 3vw, 44px);
    top: clamp(12px, 3vw, 36px);
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    background: none;
    border: 1px solid color-mix(in srgb, var(--lp-media-ink) 28%, transparent);
    border-radius: 50%;
    color: var(--lp-media-ink);
    font-size: 16px;
    cursor: pointer;
    transition: border-color var(--duration-tint) var(--ease-drift);
}

.lp-mediabox__close:hover { border-color: color-mix(in srgb, var(--lp-media-ink) 70%, transparent); }
.lp-mediabox__close:focus-visible { outline: 2px solid var(--ctx-accent); outline-offset: 3px; }

/* ============================================================
   Feature story
   ============================================================ */

.lp-story {
    display: flex;
    flex-direction: column;
    gap: clamp(28px, 4vw, 52px);
}

@media (min-width: 900px) {
    .lp-story { flex-direction: row; align-items: center; gap: clamp(40px, 5vw, 88px); }
    .lp-story--right { flex-direction: row-reverse; }
    .lp-story__media { flex: 1 1 52%; }
    .lp-story__copy { flex: 1 1 40%; }
}

.lp-story__frame {
    position: relative;
    overflow: hidden;
    aspect-ratio: 4 / 5;

    /* There is no box-sizing reset in this stylesheet, and a frame is sized by aspect-ratio
       inside a grid cell — so any border or padding one of these ever grows would be added
       *outside* the cell, and that frame alone would render wider and taller than its
       neighbour. Two photographs that should be a matched pair are exactly where that is
       most visible. */
    box-sizing: border-box;
}

/* A supporting image is a detail, not an equal — smaller, inset, and never lined up with the
   anchor. Two frames flush left at the same width read as a grid that lost a column. */
.lp-story__frame--satellite {
    margin-top: 22px;
    margin-left: 18%;
    aspect-ratio: 4 / 3;
}

/* Three images. The anchor keeps the spread; the two satellites share the row beneath it,
   separated by a seam rather than by a gutter — three photographs set a few pixels apart read
   as one composition cut into panels, which is the point; a wide gutter reads as unrelated
   thumbnails that happen to be adjacent. The same seam in both axes, so the cluster is one
   block of photography with two hairlines through it. */
@media (min-width: 900px) {
    .lp-story__media--cluster {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 3px;
    }

    .lp-story__media--cluster .lp-story__frame--anchor {
        grid-column: 1 / -1;
        aspect-ratio: 3 / 2;
    }

    .lp-story__media--cluster .lp-story__frame--satellite {
        margin: 0;
        aspect-ratio: 1 / 1;
    }

    /* Two images: the satellite takes the narrower half and hangs off the right, so the pair
       is still asymmetric rather than a half-and-half split. */
    .lp-story__media--cluster .lp-story__frame--satellite:last-child:nth-child(2) {
        grid-column: 2 / -1;
        margin-left: 22%;
    }
}

/* Type is `.lp-copy__*`; this section keeps only what is its own. */

.lp-story__features {
    list-style: none;
    margin: 28px 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 11px;
}

.lp-story__features li {
    position: relative;
    padding-left: 22px;
    font: 300 13.5px/1.6 var(--font-sans);
    letter-spacing: .06em;
    color: var(--ctx-text-2);
}

/* The tick is a hairline drawn with a border, not an icon and not a list bullet. */
.lp-story__features li::before {
    content: '';
    position: absolute;
    left: 0;
    top: .72em;
    width: 12px;
    height: 1px;
    background: var(--ctx-rule);
}

.lp-story--fullbleed { display: block; }

.lp-story__bleed {
    position: relative;
    overflow: hidden;
    aspect-ratio: 3 / 2;
}

@media (min-width: 900px) {
    .lp-story__bleed { aspect-ratio: 21 / 9; }
}

/* Indented into a measure rather than centred: centring would read as a hero. */
.lp-story__under { padding-top: clamp(32px, 5vw, 56px); }

@media (min-width: 900px) {
    .lp-story__under .lp-story__eyebrow,
    .lp-story__under .lp-story__title,
    .lp-story__under .lp-story__rule,
    .lp-story__under .lp-story__body,
    .lp-story__under .lp-story__features { margin-left: 8%; }
}

/* ============================================================
   Features — the item, which the narrative opening's column and each feature story share.
   The full-width features band was retired on 2026-08-26; the list lives beside the prose.
   ============================================================ */

/* Hairline separators only — no cards, no chips, no icons. */
.lp-features__item {
    padding: 17px 0;
    border-bottom: 1px solid color-mix(in srgb, var(--ctx-rule) 22%, transparent);
    font: 300 14.5px/1.5 var(--font-sans);
    letter-spacing: .05em;
    color: var(--ctx-text);
}

/* ============================================================
   Plans & tour
   ============================================================
   A spread when there is a walkthrough — the copy on one side and the frame on the other,
   laid out exactly as .lp-film is, because they are the same composition and a page carrying
   both should differ in which way round they face rather than in how they are built. The
   floor plans sit below at full width whatever the tour does.

   With no walkthrough the copy has nothing to sit beside, so it goes back to being a heading
   over the sheets. */

.lp-plans__spread {
    display: flex;
    flex-direction: column;
    gap: clamp(28px, 4vw, 52px);
}

@media (min-width: 900px) {
    .lp-plans__spread { flex-direction: row; align-items: center; gap: clamp(40px, 5vw, 88px); }

    /* The copy is first in the document either way; the side swap is a flex reorder, so the
       reading order never depends on which side the tour was drawn on. */
    .lp-plans__spread--left { flex-direction: row-reverse; }

    .lp-plans__spread .lp-tour { flex: 1 1 56%; }
    .lp-plans__spread .lp-plans__copy { flex: 1 1 38%; }
}

/* Type is `.lp-copy__*` — the same lockup the film next door wears. */

/* Standing alone above the sheets, the title is a heading again and needs the space under it
   that the spread's own gap was providing. */
.lp-plans__copy--alone { margin-bottom: clamp(26px, 4vw, 44px); }

/* ---- 3D tour ----------------------------------------------------------------
   The poster and the iframe that replaces it are given identical sizing, which is what makes
   activation cause no layout shift at all: the box is already the right shape before anything
   is injected into it.

   Sized by aspect-ratio rather than a fixed height or the old padding-bottom trick, so it is
   fluid at every width. 16:9 turns into 4:3 on a narrow screen — a widescreen letterbox on a
   portrait phone leaves a walkthrough about as tall as a postcard, which is not something
   anyone can explore. */
.lp-tour {
    position: relative;
    aspect-ratio: 16 / 9;
    background: var(--ctx-surface);
    overflow: hidden;
}

@media (max-width: 640px) {
    .lp-tour { aspect-ratio: 4 / 3; }
}

/* `.lp-tour__embed` used to be here: the walkthrough opened inside this frame. It opens in
   the media lightbox now, so the only things absolutely placed in the frame are the poster
   stage and the button over it. */
.lp-tour__stage,
.lp-tour__poster {
    display: block;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
    padding: 0;
    margin: 0;
}

/* The stage: the still, the teaser loop ambient_video_controller inserts over it, and the
   button that covers both — the same shape as .lp-film__stage, and the deferred embed's poster
   target as a whole, so activation removes the three together. */
.lp-tour__stage { background: var(--ctx-surface); }

.lp-tour__poster {
    background: none;
    cursor: pointer;
    -webkit-appearance: none;
       -moz-appearance: none;
            appearance: none;
}

.lp-tour__still {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    -o-object-fit: cover;
       object-fit: cover;
    transition: transform var(--duration-drift) var(--ease-drift);
}

/* Keyed off the stage, as the film's is: the still is earlier in the DOM than the button that
   covers it, and the button is the only focusable thing in the frame. */
@media (prefers-reduced-motion: no-preference) {
    .lp-tour__stage:hover .lp-tour__still,
    .lp-tour__stage:focus-within .lp-tour__still { transform: scale(1.02); }
}

/* Neutral shadow over the photography rather than a themed tint — the same exception the hero
   and film scrims take, and for the same reason: this is someone's photograph. */
.lp-tour__scrim {
    position: absolute;
    inset: 0;
    /* stylelint-disable-next-line color-no-hex, color-named, declaration-property-value-disallowed-list -- neutral shadow over photography, not palette. A poster frame is darkened toward black in every register; tinting it to the theme would grade the photograph. */
    background: linear-gradient(to top, rgba(0, 0, 0, .46) 0%, rgba(0, 0, 0, .10) 48%, rgba(0, 0, 0, .04) 100%);
}

.lp-tour__cue {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 18px;
}

/* A hairline ring and a letterspaced word. No icon-button chrome — every play glyph on the
   web is borrowed from someone, and this page does not borrow. */
.lp-tour__ring {
    width: clamp(54px, 6vw, 74px);
    aspect-ratio: 1;
    border: 1px solid color-mix(in srgb, var(--lp-media-ink) 78%, transparent);
    border-radius: 50%;
    transition: transform var(--duration-move) var(--ease-drift),
                border-color var(--duration-tint) ease;
}

.lp-tour__poster:hover .lp-tour__ring,
.lp-tour__poster:focus-visible .lp-tour__ring {
    transform: scale(1.06);
    border-color: var(--lp-media-ink);
}

.lp-tour__cue-label {
    font: 300 11px/1 var(--font-sans);
    letter-spacing: .3em;
    text-transform: uppercase;
    color: color-mix(in srgb, var(--lp-media-ink) 92%, transparent);
}

.lp-tour__poster:focus-visible { outline: 2px solid var(--ctx-accent); outline-offset: -3px; }

@media (prefers-reduced-motion: reduce) {
    .lp-tour__still, .lp-tour__ring { transition: none; }
}

/* Always separated from whatever is above it — a spread, or a heading standing alone. */
.lp-plans__sheets {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(24px, 3vw, 44px);
    margin-top: clamp(28px, 4vw, 52px);
}

@media (min-width: 700px) { .lp-plans__sheets { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.lp-plans__sheet { margin: 0; }

/* `contain`, not `cover`: a plan is a document, and cropping one loses a corner of the house. */
.lp-plans__sheet img {
    width: 100%;
    height: auto;
    max-height: 70vh;
    -o-object-fit: contain;
       object-fit: contain;
    background: var(--ctx-surface);
}

.lp-plans__sheet figcaption {
    margin-top: 12px;
    font: 300 11.5px/1.6 var(--font-sans);
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
}

/* ============================================================
   Property film

   The film on one side and the invitation to watch it on the other, laid out like .lp-story
   above and for the same reason: a frame and a column of type are a spread, and the two
   sections of this page that are shaped that way should be shaped that way identically.

   There is no player here at all. A reader who presses play gets .lp-mediabox over the page —
   see the section template for why that is a privacy decision as much as a layout one.

   **The stage owns the box.** The 16:9 lives on .lp-film__stage, not on the button inside it.
   It used to live on the button, and the embed controller removes the button when it starts a
   player — so the frame collapsed to nothing and the film played at zero height. The tour
   section next door has always done it this way; now both do.

   The cue is a hairline ring and a word rather than a play triangle. Every icon button in
   this design language is a borrowed one, and YouTube's is the most borrowed of all.
   ============================================================ */

.lp-film {
    display: flex;
    flex-direction: column;
    gap: clamp(28px, 4vw, 52px);
}

@media (min-width: 900px) {
    .lp-film { flex-direction: row; align-items: center; gap: clamp(40px, 5vw, 88px); }

    /* The side swap is a flex reorder, so the copy stays where it is in the document and the
       reading order never depends on which side the film was drawn on — the same rule
       PageSplitSide states for the reading panel. */
    .lp-film--right { flex-direction: row-reverse; }

    .lp-film__stage { flex: 1 1 56%; }
    .lp-film__copy { flex: 1 1 38%; }
}

/* Type is `.lp-copy__*`; the measure is this section's own — the film's headline is the
   agent's own words and runs longer than a composed title. */
.lp-film__title { max-width: 22ch; }

/* The frame. Everything inside it is absolutely positioned against this box, including the
   teaser loop ambient_video_controller inserts, so the section's height is a property of the
   section rather than of whichever child happens to be present. */
.lp-film__stage {
    position: relative;
    aspect-ratio: 16 / 9;
    background: var(--ctx-surface);
    overflow: hidden;
}

.lp-film__still {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    -o-object-fit: cover;
       object-fit: cover;
    transition: transform var(--duration-drift) var(--ease-drift);
}

/* A real <button>: it is operated by keyboard and announced as an action, which a div with a
   click handler is not. It covers the frame rather than holding it open. Everything below
   strips the browser's own button styling back. */
.lp-film__poster {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    -webkit-appearance: none;
       -moz-appearance: none;
            appearance: none;
}

/* Keyed off the stage rather than the button: the still is earlier in the DOM than the button
   that covers it, so no sibling combinator can reach it from there. The button is the only
   focusable thing in the frame, which is what makes :focus-within exact here. */
@media (prefers-reduced-motion: no-preference) {
    .lp-film__stage:hover .lp-film__still,
    .lp-film__stage:focus-within .lp-film__still { transform: scale(1.02); }
}

/* Neutral shadow over the photography, not a themed tint — the same exception the hero and
   context scrims take, and for the same reason: this is someone's photograph. */
.lp-film__scrim {
    position: absolute;
    inset: 0;
    /* stylelint-disable-next-line color-no-hex, color-named, declaration-property-value-disallowed-list -- neutral shadow over photography, not palette. A poster frame is darkened toward black in every register; tinting it to the theme would grade the photograph. */
    background: linear-gradient(to top, rgba(0, 0, 0, .48) 0%, rgba(0, 0, 0, .12) 46%, rgba(0, 0, 0, .04) 100%);
}

.lp-film__cue {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 18px;
}

.lp-film__ring {
    width: clamp(54px, 6vw, 74px);
    aspect-ratio: 1;
    border: 1px solid color-mix(in srgb, var(--lp-media-ink) 78%, transparent);
    border-radius: 50%;
    transition: transform var(--duration-move) var(--ease-drift),
                border-color var(--duration-tint) ease;
}

.lp-film__poster:hover .lp-film__ring,
.lp-film__poster:focus-visible .lp-film__ring {
    transform: scale(1.06);
    border-color: var(--lp-media-ink);
}

.lp-film__cue-label {
    font: 300 11px/1 var(--font-sans);
    letter-spacing: .3em;
    text-transform: uppercase;
    color: color-mix(in srgb, var(--lp-media-ink) 92%, transparent);
}

.lp-film__poster:focus-visible { outline: 2px solid var(--ctx-accent); outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
    .lp-film__still, .lp-film__ring { transition: none; }
}

/* Locally defined rather than borrowed from a utility layer: this stylesheet is the only one
   guaranteed to be present on a property page. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ============================================================
   Area — the editorial teaser after the map
   ============================================================
   A magazine spread, not a card: the town's name set large on the left against a reading
   column on the right, one hairline between them and nothing else. No border, no panel, no
   background of its own — the band it stands on supplies the ground, which is why every
   colour below is a `--ctx-*` role and never the page's theme.

   The composition inverts the usual weighting on purpose. The heading column is the narrower
   one, so the name sits in a generous field of nothing and the copy has a real measure to be
   read at. A 50/50 split would make the name look like a label for the paragraph beside it.
   ============================================================ */

.lp-area__spread {
    display: flex;
    flex-direction: column;
    gap: clamp(28px, 4vw, 44px);
}

@media (min-width: 900px) {
    .lp-area__spread {
        flex-direction: row;
        align-items: flex-start;
        gap: clamp(48px, 6vw, 104px);
    }

    .lp-area__lede { flex: 0 1 38%; }
    .lp-area__body { flex: 1 1 46%; }

    /* The photograph is the last column and the first to go: below 900px the spread stacks and
       it sits under the copy, which is the right order on a phone — the words say what the
       place is, the picture only agrees with them. */
    .lp-area--imaged .lp-area__lede { flex: 0 1 32%; }
    .lp-area--imaged .lp-area__body { flex: 1 1 40%; }
    .lp-area--imaged .lp-area__figure { flex: 0 1 24%; }

    /* The area's photograph beside the copy: two columns, picture left, words right. Near
       enough to even that the picture is a column and not a margin illustration, but the words
       keep the wider half — a paragraph needs its measure more than a photograph needs width. */
    .lp-area--split .lp-area__figure { flex: 0 1 44%; }
    .lp-area--split .lp-area__words { flex: 1 1 48%; }
    .lp-area--split .lp-area__image--beside { aspect-ratio: 4 / 5; }
}

/* The photograph beside the copy: 3:2 while stacked on a phone — a landscape hero's own shape —
   and a taller 4:5 once it is a column (above), so it stands as high as the words beside it
   rather than as a letterbox above them. Unlike the third-column arrangement on `with-image`
   it comes first in the markup, so on a phone the place is seen before it is named. */
.lp-area__image--beside { aspect-ratio: 3 / 2; }

.lp-area__name {
    margin: 20px 0 0;
    font-size: calc(clamp(34px, 5vw, 68px) * var(--lp-type-scale));
    line-height: 1.02;
    color: var(--ctx-text);
    text-wrap: balance;
}

/* The structural fact, not a statistic. Small caps and faint: it is provenance, and provenance
   read at body weight starts arguing with the description. */
.lp-area__meta {
    margin: 18px 0 0;
    font: 400 10px/1.7 var(--font-sans);
    letter-spacing: .28em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
}

.lp-area__rule { margin-bottom: clamp(22px, 3vw, 32px); }

/* The author's own lead-in, set slightly up from the bound copy so the two read as a
   sentence and its elaboration rather than as two paragraphs of equal weight. */
.lp-area__lead {
    margin: 0 0 18px;
    max-width: 52ch;
    font-family: var(--font-display);
    font-weight: 300;
    font-size: calc(clamp(19px, 1.7vw, 24px) * var(--lp-type-scale));
    line-height: 1.5;
    color: var(--ctx-text);
}

.lp-area__copy {
    margin: 0;
    max-width: 54ch;
    font: 300 15px/1.85 var(--font-sans);
    color: var(--ctx-text-2);
}

/* GOLF · COASTAL — the listing's own lifestyles, as a line of type rather than a row of
   chips. Chips would make them look like filters, which is precisely what they are not. */
.lp-area__lifestyles {
    margin: clamp(26px, 3vw, 34px) 0 0;
    font: 400 10px/2.2 var(--font-sans);
    letter-spacing: .28em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
}

.lp-area__sep { padding: 0 .75em; opacity: .5; }

.lp-area__lifestyle {
    color: var(--ctx-text);
    text-decoration: none;
    border-bottom: 1px solid var(--ctx-rule);
    padding-bottom: 3px;
    transition: border-color var(--duration-tint) var(--ease-drift),
                color var(--duration-tint) var(--ease-drift);
}

.lp-area__lifestyle:hover { border-bottom-color: var(--ctx-accent); color: var(--ctx-accent); }

.lp-area__explore {
    display: inline-flex;
    align-items: baseline;
    gap: 12px;
    margin-top: clamp(28px, 3.5vw, 38px);
    padding-bottom: 6px;
    border-bottom: 1px solid var(--ctx-accent);
    font: 400 10px/1 var(--font-sans);
    letter-spacing: .3em;
    text-transform: uppercase;
    color: var(--ctx-text);
    text-decoration: none;
    transition: color var(--duration-tint) var(--ease-drift);
}

/* Transform only, and four pixels of it. The nudge is the whole hover — it echoes the guide
   index rows, and anything larger turns a considered link into a button pretending to be one. */
.lp-area__arrow {
    display: inline-block;
    transition: transform var(--duration-move) var(--ease-drift);
}

.lp-area__explore:hover { color: var(--ctx-accent); }
.lp-area__explore:hover .lp-area__arrow { transform: translateX(5px); }

.lp-area__figure {
    margin: 0;
    overflow: hidden;
}

.lp-area__image {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: 3 / 4;
    -o-object-fit: cover;
       object-fit: cover;
}

@media (prefers-reduced-motion: reduce) {
    .lp-area__lifestyle,
    .lp-area__explore,
    .lp-area__arrow { transition: none; }
}

/* ============================================================
   Location map
   ============================================================
   The address line is the section; the map is an illustration of it. That ordering is why the
   line sits above the panel and is styled to stand on its own — with no key, no script or no
   coordinates, what remains has to read as finished rather than as a caption missing its
   picture. */

/* Type is `.lp-copy__*`; the short measure is this section's own — an address line wants to
   break early rather than run the width of the column. */

/* The wrapper the controller reveals: the map panel and the commute module, which can only
   draw on a map that painted, so they appear and disappear as one. */
.lp-location__live[hidden] { display: none; }

/* A fixed ratio so the panel occupies its space before Google has drawn anything into it —
   revealing a map into a zero-height box shifts every section below it.

   Full width: the panel sits outside the `.lp-shell` in the markup, so it runs edge to edge
   the way the full-bleed feature story does, and the same 21:9 keeps it a band across the
   page rather than a wall of tiles. The words above and below it stay in the measure. */
.lp-location__map {
    position: relative;
    width: 100%;
    aspect-ratio: 3 / 2;
    overflow: hidden;
}

@media (min-width: 900px) {
    .lp-location__map { aspect-ratio: 21 / 9; }
}

.lp-location__canvas { position: absolute; inset: 0; }

.lp-location__note {
    margin: 18px 0 0;
    font: 300 12px/1.7 var(--font-sans);
    letter-spacing: .12em;
    color: var(--ctx-text-2);
}

/* ============================================================
   Commute module — "From the residence to ___"
   ============================================================
   An editorial query line under the map, not a form-looking form: a micro-caps label, an
   input that is nothing but a hairline, and CALCULATE as a text link in the carousel-step
   voice. Results stack beneath as a quiet ruled list; each line is a button, because
   pressing it re-draws that route. */

.lp-commute {
    margin-top: clamp(24px, 3vw, 36px);
}

.lp-commute__line {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 16px 24px;
}

.lp-commute__label {
    font: 400 10px/1 var(--font-sans);
    letter-spacing: .32em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
    white-space: nowrap;
}

/* The input and its suggestion list share this box: it takes the input's place in the
   line, and the list hangs from its bottom edge — the hairline. */
.lp-commute__field {
    position: relative;
    flex: 1 1 220px;
    min-width: 0;
}

/* Typography, not chrome: no box, no background, one hairline underneath. The focus state
   is the hairline finding its voice, not an outline arriving. */
.lp-commute__input {
    display: block;
    width: 100%;
    padding: 0 0 8px;
    background: none;
    border: 0;
    border-bottom: 1px solid var(--ctx-rule);
    border-radius: 0;
    font: 300 clamp(14px, 1.3vw, 16px)/1.4 var(--font-sans);
    letter-spacing: .06em;
    color: var(--ctx-text);
    transition: border-color var(--duration-tint) var(--ease-drift);
}

.lp-commute__input::-moz-placeholder {
    color: var(--ctx-text-2);
    opacity: .6;
}

.lp-commute__input::placeholder {
    color: var(--ctx-text-2);
    opacity: .6;
}

.lp-commute__input:focus {
    outline: none;
    border-color: var(--ctx-text);
}

/* Suggestions: a short ruled list hanging from the hairline, on the band's own ground so it
   can sit over whatever is beneath it, and in the same two tones as the result lines — the
   place in the primary tone, where it is in the secondary. Not a dropdown: no shadow, no
   radius, no highlight bar; the walked-to line simply reads darker, as a hovered line does. */
.lp-commute__suggestions {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 5;
    margin: 0;
    padding: 0;
    list-style: none;
    background: var(--ctx-bg);
    border-bottom: 1px solid var(--ctx-rule);
}

.lp-commute__suggestions[hidden] { display: none; }

.lp-commute__suggestion {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 2px 12px;
    padding: 11px 0;
    border-top: 1px solid var(--ctx-rule);
    cursor: pointer;
    color: var(--ctx-text-2);
    transition: color var(--duration-tint) var(--ease-drift);
}

.lp-commute__suggestion:first-child { border-top: 0; }

.lp-commute__suggestion:hover,
.lp-commute__suggestion[aria-selected="true"] {
    color: var(--ctx-text);
}

.lp-commute__suggestion-main {
    font: 300 clamp(14px, 1.3vw, 16px)/1.4 var(--font-sans);
    letter-spacing: .06em;
}

.lp-commute__suggestion-detail {
    font: 300 11px/1.6 var(--font-sans);
    letter-spacing: .1em;
    color: var(--ctx-text-2);
    opacity: .8;
}

/* CALCULATE — the carousel-step vocabulary verbatim: a letterspaced word over a hairline
   that draws in. Never a box. */
.lp-commute__go {
    position: relative;
    padding: 0 0 4px;
    border: 0;
    background: none;
    cursor: pointer;
    font: 400 10px/1 var(--font-sans);
    letter-spacing: .32em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
    transition: color var(--duration-drift) var(--ease-drift);
}
.lp-commute__go::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 1px;
    background: currentColor;
    transform: scaleX(0);
    transform-origin: left;
    transition: transform var(--duration-drift) var(--ease-drift);
}
.lp-commute__go:hover { color: var(--ctx-text); }
.lp-commute__go:hover::after { transform: scaleX(1); }
.lp-commute__go:disabled { opacity: .35; cursor: default; }
.lp-commute__go:disabled:hover::after { transform: scaleX(0); }

/* The inline voice for a question that did not resolve — a sentence, never a red box. */
.lp-commute__error {
    margin: 14px 0 0;
    font: 300 12px/1.7 var(--font-sans);
    letter-spacing: .08em;
    color: var(--ctx-text-2);
}

.lp-commute__error[hidden] { display: none; }

.lp-commute__results {
    margin: clamp(18px, 2.4vw, 28px) 0 0;
    padding: 0;
    list-style: none;
}

.lp-commute__results[hidden] { display: none; }

.lp-commute__result {
    border-top: 1px solid var(--ctx-rule);
}

/* `SFO INTERNATIONAL AIRPORT — 24.1 MILES · 36 MIN DRIVE`. The whole line is the press
   target; the one currently drawn on the map reads in the primary tone. */
.lp-commute__result-line {
    display: block;
    width: 100%;
    padding: 12px 0;
    border: 0;
    background: none;
    cursor: pointer;
    text-align: left;
    font: 400 11px/1.8 var(--font-sans);
    letter-spacing: .22em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
    transition: color var(--duration-tint) var(--ease-drift);
}

.lp-commute__result-line:hover,
.lp-commute__result-line[aria-current] {
    color: var(--ctx-text);
}

/* The honesty rule: drive times are typical-traffic, said once, smaller than the numbers. */
.lp-commute__honesty {
    margin: 10px 0 0;
    font: 300 10px/1.6 var(--font-sans);
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
    opacity: .7;
}

.lp-commute__honesty[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
    .lp-commute__input,
    .lp-commute__go,
    .lp-commute__go::after,
    .lp-commute__suggestion,
    .lp-commute__result-line { transition: none; }
}

/* Dev-only: listing_map_controller inserts this when the map throws and APP_ENV is dev, so a
   swallowed code error is impossible to miss during QA. Never rendered in prod. */
.lp-map__error {
    margin: 18px 0 0;
    padding-top: 12px;
    border-top: 1px solid var(--ctx-rule);
    font: 400 11px/1.7 var(--font-sans);
    letter-spacing: .28em;
    text-transform: uppercase;
    color: var(--ctx-accent);
}

/* ============================================================
   Agent
   ============================================================ */

.lp-agent {
    display: flex;
    flex-direction: column;
    gap: clamp(26px, 4vw, 52px);
}

@media (min-width: 700px) {
    .lp-agent { flex-direction: row; align-items: flex-start; gap: clamp(36px, 5vw, 76px); }
    .lp-agent__portrait { flex: 0 0 clamp(200px, 24vw, 300px); }
    .lp-agent__copy { flex: 1 1 auto; padding-top: 10px; }
}

.lp-agent__portrait {
    position: relative;
    width: clamp(160px, 42vw, 260px);
    aspect-ratio: 4 / 5;
    overflow: hidden;
    background: var(--ctx-surface);
    display: flex;
    align-items: center;
    justify-content: center;
}

.lp-agent__portrait img { width: 100%; height: 100%; -o-object-fit: cover; object-fit: cover; }

/* Their initials rather than a stock silhouette: a generic photograph of a person who is
   not this person is worse than no photograph. */
.lp-agent__initials {
    font-family: var(--font-display);
    font-size: clamp(36px, 6vw, 54px);
    font-weight: 300;
    letter-spacing: .06em;
    color: var(--ctx-text-2);
}

.lp-agent__name {
    margin: 18px 0 0;
    font-size: calc(clamp(26px, 3.2vw, 38px) * var(--lp-type-scale));
    line-height: 1.1;
    color: var(--ctx-text);
}

.lp-agent__affiliation {
    margin: 12px 0 0;
    font: 300 11.5px/1.6 var(--font-sans);
    letter-spacing: .22em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
}

.lp-agent__intro {
    margin: 22px 0 0;
    max-width: 48ch;
    font: 300 15px/1.85 var(--font-sans);
    color: var(--ctx-text-2);
}

.lp-agent__contact { margin: 22px 0 0; font: 300 14px/1.8 var(--font-sans); letter-spacing: .06em; }
.lp-agent__contact a {
    color: var(--ctx-accent);
    text-decoration: none;
    border-bottom: 1px solid color-mix(in srgb, var(--ctx-accent) 40%, transparent);
}
.lp-agent__contact a:hover { border-bottom-color: var(--ctx-accent); }

/* The licence is a legal line, set in the caption voice — never a badge. */
.lp-agent__licence {
    margin: 16px 0 0;
    font: 300 10px/1.6 var(--font-sans);
    letter-spacing: .26em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
}

/* ============================================================
   Showing request
   ============================================================ */

.lp-showing {
    display: flex;
    flex-direction: column;
    gap: clamp(32px, 5vw, 64px);
}

@media (min-width: 900px) {
    .lp-showing { flex-direction: row; align-items: flex-start; gap: clamp(48px, 7vw, 112px); }
    .lp-showing__copy { flex: 1 1 44%; }
    .lp-showing__form { flex: 1 1 46%; }
}

.lp-showing__headline {
    margin: 26px 0 0;
    max-width: 14ch;
    font-size: calc(clamp(30px, 4.2vw, 52px) * var(--lp-type-scale));
    line-height: 1.08;
    color: var(--ctx-text);
}

.lp-showing__body {
    margin: 20px 0 0;
    max-width: 40ch;
    font: 300 15px/1.85 var(--font-sans);
    color: var(--ctx-text-2);
}

/* ---- with the agent beside the form -----------------------------------------
   The register inversion is already the page's full stop; this is what stands in it. A
   portrait and a form on one dark band read as a person and a way of reaching them, which is
   the whole content of the last thing a page says.

   The columns are separated by a drawn hairline rather than by whitespace alone. Everywhere
   else on this page a rule is horizontal and marks a beginning; here it is vertical and marks
   a division, which is the one place the ornament earns being turned on its side. Below the
   breakpoint it becomes horizontal again — a vertical rule between stacked blocks would be a
   line pointing at nothing. */

@media (min-width: 900px) {
    .lp-showing--presented { gap: 0; align-items: stretch; }

    .lp-showing--presented .lp-showing__copy { flex: 1 1 40%; padding-right: clamp(40px, 5vw, 88px); }

    .lp-showing--presented .lp-showing__form {
        flex: 1 1 50%;
        border-left: 1px solid var(--ctx-rule);
        padding-left: clamp(40px, 5vw, 88px);
    }
}

@media (max-width: 899px) {
    .lp-showing--presented .lp-showing__form {
        border-top: 1px solid var(--ctx-rule);
        padding-top: clamp(32px, 5vw, 52px);
    }
}

/* Held to the width of the copy beneath it: at the full display scale it would outweigh the
   name in the other column, and the two are meant to read as equals. */
.lp-showing--presented .lp-showing__headline {
    margin: 0;
    max-width: 20ch;
    font-size: calc(clamp(26px, 3.2vw, 40px) * var(--lp-type-scale));
}

.lp-showing__ask { margin-bottom: clamp(30px, 4vw, 48px); }

/* The name sits under a much larger frame than it did, and the old 18px reads as a caption
   stuck to the bottom of the photograph. */
.lp-showing--presented .lp-agent__name { margin-top: 26px; }
.lp-showing__ask .lp-rule { margin-bottom: 26px; }

/* Sized to its column rather than to a number. The standalone section's portrait was a fixed
   width beside a wide-open band, which is what left the old agent section looking like a stamp
   floating in empty ivory; here the frame takes the width it is given and the column decides
   how large that is. The shell caps at 1240px, so it cannot run away on a wide screen.

   Back to the 4:5 of `.lp-agent__portrait`: this is the brand's portrait frame and a squarer
   crop was only ever a way of keeping a small picture from looking lost. A large one does not
   have that problem. */
.lp-showing__portrait {
    width: clamp(180px, 52vw, 300px);
    aspect-ratio: 4 / 5;
    margin-top: 26px;
}

@media (min-width: 900px) {
    .lp-showing__portrait { width: 100%; max-width: 460px; }
}

/* Beside a form, a biography is competing for the decision. It still reads; it does not run. */
.lp-showing__bio {
    max-width: 34ch;
    font-size: 14px;
}

.lp-form__field + .lp-form__field { margin-top: 24px; }

.lp-form label {
    display: block;
    margin-bottom: 9px;
    font: 400 9.5px/1 var(--font-sans);
    letter-spacing: .3em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
}

/* Underline only — a bordered box would be the card this page refuses to have. */
.lp-form input,
.lp-form textarea {
    width: 100%;
    padding: 10px 0;
    border: 0;
    border-bottom: 1px solid var(--ctx-input-line);
    border-radius: 0;
    background: transparent;
    font: 300 16px/1.6 var(--font-sans);
    color: var(--ctx-text);
    transition: border-color var(--duration-tint) ease;
}

.lp-form input:focus,
.lp-form textarea:focus { outline: none; border-bottom-color: var(--ctx-accent); }
.lp-form textarea { resize: vertical; }

.lp-form__error { font: 300 12.5px/1.6 var(--font-sans); color: var(--ctx-accent); }
.lp-form__error ul { list-style: none; margin: 8px 0 0; padding: 0; }
.lp-form__summary { margin: 0 0 22px; font: 300 13px/1.6 var(--font-sans); color: var(--ctx-accent); }

/* Off-screen rather than display:none — some form-fillers skip hidden fields, and a trap
   they skip is not a trap. */
.lp-form__trap {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

.lp-form__submit {
    margin-top: 34px;
    padding: 15px 34px;
    border: 1px solid var(--ctx-accent);
    background: transparent;
    font: 400 10px/1 var(--font-sans);
    letter-spacing: .3em;
    text-transform: uppercase;
    color: var(--ctx-accent);
    cursor: pointer;
    transition: background-color var(--duration-tint) ease, color var(--duration-tint) ease;
}

.lp-form__submit:hover { background: var(--ctx-accent); color: var(--ctx-bg); }

@media (prefers-reduced-motion: reduce) {
    .lp-form input, .lp-form textarea, .lp-form__submit { transition: none; }
}

/* ============================================================
   Compliance footer
   ============================================================
   The one section outside the theme. Brokerage lockups, licence lines and fair-housing
   marks are fixed brand assets that must read identically on every property — a compliance
   mark tinted to match the photography is not a compliance mark. Site tokens only here. */

/* A legal strip, not a footer. It used to be the second: a wordmark on the left, the licence
   lines on the right, and enough padding around the pair to make a band of it — directly above
   the site's own footer, which does the same lockup properly and has the sitemap under it. Two
   endings, eighty pixels apart.

   What is left is the property's own legal text, set quietly across the page at the size of a
   caption. The vertical rhythm is a fraction of what it was for the same reason: this is the
   small print under the page, not a place anyone stops. */
.lp-compliance {
    background: var(--ink);
    color: color-mix(in srgb, var(--cream) 58%, transparent);
    padding: clamp(22px, 2.6vw, 34px) 0;
}

.lp-compliance__legal { max-width: 92ch; }

/* Three short lines on a wide screen — brokerage, disclaimer, housing mark — read as one strip
   rather than as a stack. They wrap to their own lines below it, which is where the gap is. */
@media (min-width: 900px) {
    .lp-compliance__legal {
        display: flex;
        flex-wrap: wrap;
        align-items: baseline;
        gap: 6px 26px;
        max-width: none;
    }
}

.lp-compliance__legal p {
    margin: 0 0 10px;
    font: 300 11px/1.85 var(--font-sans);
    letter-spacing: .1em;
}

.lp-compliance__legal p:last-child { margin-bottom: 0; }

@media (min-width: 900px) {
    .lp-compliance__legal p { margin-bottom: 0; }
}

.lp-compliance__housing { display: flex; align-items: center; gap: 10px; }

/* The fair-housing mark: a roof outline in pure CSS, so there is no image to fail to load
   on a page that is legally required to carry it. */
.lp-compliance__housing-mark {
    width: 13px;
    height: 11px;
    flex: 0 0 auto;
    border: 1px solid currentColor;
    border-top: 0;
    position: relative;
}

.lp-compliance__housing-mark::before {
    content: '';
    position: absolute;
    left: -1px;
    top: -6px;
    width: 0;
    height: 0;
    border-left: 7.5px solid transparent;
    border-right: 7.5px solid transparent;
    border-bottom: 6px solid currentColor;
}

/* ============================================================
   Sale result
   ============================================================ */

/* The order on the page is prose, then origin, then figures — a results section that leads
   with a number is a listing card. Everything below is set at the same weight and colour as
   the rest of the page; nothing here is emphasised, because the restraint is what makes it
   read as a record rather than a boast. */

.lp-result__framing {
    margin: clamp(28px, 4vw, 44px) 0 0;
    max-width: 26ch;
    font-size: calc(clamp(26px, 3.4vw, 40px) * var(--lp-type-scale));
    line-height: 1.34;
    color: var(--ctx-text);
}

@media (min-width: 700px) {
    .lp-result__framing { max-width: 32ch; }
}

.lp-result__origin {
    margin: clamp(26px, 3.5vw, 40px) 0 0;
    max-width: 44ch;
    font: 300 clamp(16px, 1.7vw, 19px)/1.7 var(--font-sans);
    color: var(--ctx-text-2);
}

/* The figures, in exactly the .lp-details__value register: small, letterspaced, never bold
   and never coloured. A sold price that shouted would undo the whole section. */
.lp-result__line {
    margin: clamp(28px, 4vw, 44px) 0 0;
    padding-top: 26px;
    border-top: 1px solid color-mix(in srgb, var(--ctx-rule) 42%, transparent);
    font: 400 clamp(10px, 1.05vw, 12px)/2 var(--font-sans);
    letter-spacing: .34em;
    text-transform: uppercase;
    color: var(--ctx-accent);
}

.lp-result__date {
    margin: 12px 0 0;
    font: 300 13px/1.6 var(--font-sans);
    letter-spacing: .06em;
    color: var(--ctx-text-2);
}

/* ============================================================
   Campaign story
   ============================================================ */

.lp-campaign__title {
    margin: clamp(28px, 4vw, 44px) 0 0;
    max-width: 18ch;
    font-size: calc(clamp(24px, 3vw, 36px) * var(--lp-type-scale));
    line-height: 1.14;
    color: var(--ctx-text);
}

.lp-campaign__body {
    margin: clamp(22px, 3vw, 32px) 0 0;
    max-width: 62ch;
    font: 300 16px/1.85 var(--font-sans);
    color: var(--ctx-text-2);
}

/* The elements as a caption, not a feature matrix. Middots and letterspacing put this in the
   same register as the detail line — scannable for "did they do a film?", and unmistakably
   not a list of ticks. */
.lp-campaign__elements {
    margin: clamp(26px, 3.5vw, 38px) 0 0;
    max-width: 66ch;
    font: 400 clamp(9.5px, 1vw, 11px)/2.2 var(--font-sans);
    letter-spacing: .3em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
}

/* Image-accompanied. Stacks image-first on a phone rather than collapsing the columns: the
   photograph is what earns the next scroll, and prose above it would be a wall. */
.lp-campaign__split {
    display: flex;
    flex-direction: column;
    gap: clamp(32px, 5vw, 64px);
}

@media (min-width: 900px) {
    .lp-campaign__split { flex-direction: row-reverse; align-items: flex-start; gap: clamp(48px, 7vw, 96px); }
    .lp-campaign__copy { flex: 1 1 46%; }
    .lp-campaign__figures { flex: 1 1 42%; }
}

.lp-campaign__figures {
    display: flex;
    flex-direction: column;
    gap: clamp(18px, 2.4vw, 28px);
}

.lp-campaign__figure {
    position: relative;
    margin: 0;
    aspect-ratio: 4 / 3;
    overflow: hidden;
    background: var(--ctx-surface);
}

/* ============================================================
   Testimonial
   ============================================================ */

.lp-quote__lead { margin: clamp(26px, 3.5vw, 40px) 0 0; }

.lp-quote__figure { margin: 0; }

/* Large, and given room. No quotation-mark graphic, no card, no portrait — the restraint is
   the argument. The hanging indent below aligns the glyph rather than the quote mark, and is
   dropped on a phone where there is no room to lose. */
.lp-quote__text {
    margin: clamp(28px, 4vw, 48px) 0 0;
    max-width: 20ch;
    font-size: calc(clamp(28px, 4vw, 46px) * var(--lp-type-scale));
    line-height: 1.24;
    color: var(--ctx-text);
}

@media (min-width: 700px) {
    .lp-quote__text { max-width: 24ch; text-indent: -0.42em; }
}

.lp-quote__attribution {
    margin: clamp(26px, 3.5vw, 38px) 0 0;
    font: 400 10px/1 var(--font-sans);
    letter-spacing: .3em;
    text-transform: uppercase;
    color: var(--ctx-text-2);
}

/* ============================================================
   Ambient clip — a full-bleed band of moving picture between written sections.

   The frame owns the height and everything inside it is absolutely positioned, exactly as the
   film's stage does: the loop ambient_video_controller inserts, the still it dissolves out of
   and the scrim under any copy all fill the box, so the band's height is a property of the band
   rather than of whichever child happens to be present.

   Two heights. `band` is a wide strip — a breath between paragraphs — and `tall` most of the
   viewport, for a clip that has earned a page of its own. An option rather than two variants
   because it is one composition drawn at two sizes.

   The copy, when there is any, sits in the lower-left corner over a neutral scrim: the same
   position and the same exception the hero takes, because this is somebody's photograph and
   grading it to the theme would be the one thing the band must not do. No copy, no scrim.
   ============================================================ */

.lp-section--ambient-clip { padding-block: 0; }

.lp-clip { position: relative; overflow: hidden; }

.lp-clip__frame {
    position: relative;
    height: clamp(320px, 42vw, 560px);
    background: var(--ctx-surface);
    overflow: hidden;
}

.lp-clip--tall .lp-clip__frame { height: min(82vh, 900px); }

.lp-clip__scrim {
    position: absolute;
    inset: 0;
    /* stylelint-disable-next-line color-no-hex, color-named, declaration-property-value-disallowed-list -- neutral shadow over photography, not palette. The caption's legibility is bought with darkness in every register; a themed tint would grade the clip. */
    background: linear-gradient(to top, rgba(0, 0, 0, .52) 0%, rgba(0, 0, 0, .16) 38%, rgba(0, 0, 0, 0) 70%);
}

.lp-clip__copy {
    position: absolute;
    inset-inline: 0;
    bottom: clamp(24px, 4vw, 48px);
    color: var(--lp-media-ink);
}

.lp-clip__eyebrow { margin-bottom: 10px; color: var(--lp-media-ink); }

.lp-clip__caption {
    margin: 0;
    max-width: 44ch;
    font-family: var(--font-serif);
    font-size: calc(clamp(17px, 1.6vw, 21px) * var(--lp-type-scale));
    line-height: 1.4;
}

/* ============================================================
   Custom content sections — section title, blockquote, call to action.

   The three sections on the page that are about nothing but themselves. They are grouped here,
   at the end of the section run, rather than slotted into the page order above, because they
   have no page order: a section title can open the page or close it, and a blockquote goes
   wherever the argument needs one.

   Almost all of their type is borrowed. `.lp-copy__*` is the site's copy column and these wear
   it unchanged; what each block below owns is its measure, its rhythm and the one gesture that
   is its own. That is the same bargain the film, the walkthrough and the narrative opening
   struck when the lockup was extracted — a new section should be a few lines here, not a fifth
   set of numbers that will drift from the other four.
   ============================================================ */

/* ---- Section title ----
   The chapter opening. Nothing but the copy column at a slightly shorter measure, because a
   standfirst under a display line is a dek and a dek that runs the full column reads as the
   opening paragraph of something. Its tight bottom edge is up with the other rhythm exceptions.

   The one section here that is configured rather than fixed, because it is the one used as a
   group header: a title over the gallery, the film and the tour is a rank above the titles inside
   them, and where it sits is a decision the page makes rather than the stylesheet. Three
   modifiers, all of them a line or two, all of them multiplying or nudging what the copy column
   already decided — none of them a second set of numbers.

   Alignment is four properties and not one, which is the only part of this that is not obvious.
   The block takes `text-align`; the eyebrow is a flex row, so it takes `justify-content`; the
   standfirst has a `52ch` measure, so it takes a margin; and the hairline is a fixed 92px with a
   directional gradient and a `scaleX` entrance, so it takes a margin, a mirrored gradient and a
   matching transform origin. Setting `text-align` alone leaves a centred title over a
   left-hugging dek and a rule still pinned to the margin. */

.lp-sectiontitle__standfirst { max-width: 52ch; }

.lp-sectiontitle { --lp-title-step: 1.34; }
.lp-sectiontitle--normal { --lp-title-step: 1; }

/* 500, not 700. `.lp-display` is a light face; at bold it stops reading as the same typeface
   emphasised and starts reading as a different one. */
.lp-sectiontitle--strong .lp-sectiontitle__title { font-weight: 500; }

.lp-sectiontitle--center { text-align: center; }
.lp-sectiontitle--center .lp-sectiontitle__eyebrow { justify-content: center; }
.lp-sectiontitle--center .lp-sectiontitle__standfirst { margin-inline: auto; }

.lp-sectiontitle--center .lp-sectiontitle__rule {
    margin-inline: auto;
    background: linear-gradient(90deg, transparent, var(--ctx-rule), transparent);
    transform-origin: center;
}

.lp-sectiontitle--right { text-align: right; }
.lp-sectiontitle--right .lp-sectiontitle__eyebrow { justify-content: flex-end; }
.lp-sectiontitle--right .lp-sectiontitle__standfirst { margin-inline-start: auto; }

.lp-sectiontitle--right .lp-sectiontitle__rule {
    margin-inline-start: auto;
    background: linear-gradient(90deg, transparent, var(--ctx-rule));
    transform-origin: right center;
}

/* The eyebrow's rule when it follows the words rather than leading them: the same hairline,
   fading away from the text instead of into it. */
.lp-eyebrow__tick--trailing { background: linear-gradient(90deg, var(--ctx-accent), transparent); }

/* ---- Blockquote ----
   The homepage pull quote, moved onto the listing page: display face at display size, the
   marks typed around it, a gold eyebrow underneath. The figure is centred in the shell and the
   text inside it is ragged-left, so the block sits where the eye expects and the line still
   reads like prose rather than a greeting card. No hairline — the marks already say what the
   rule used to say, and a rule beside a quote this size is a second frame around one picture.

   The measure is the homepage's: a 1120px container less its gutters. Wider than the copy
   column on purpose; a quote set at 38px in 62ch would wrap into a paragraph, and this wants
   to be two lines that land. */

.lp-blockquote__figure {
    margin: 0 auto;
    max-width: 1008px;
}

.lp-blockquote__text {
    margin: 0;
    font-family: var(--font-display);
    font-weight: 300;
    font-size: calc(clamp(26px, 3.4vw, 38px) * var(--lp-type-scale));
    line-height: 1.4;
    color: var(--ctx-text);
    text-wrap: pretty;
}

/* Name · Role as one gold eyebrow, the way the homepage runs "Seller · Los Gatos · $3,650,000"
   under its quote. The separator is a glyph, not a gap, so the line survives a wrap intact. */
.lp-blockquote__attribution {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 .6em;
    margin: 22px 0 0;
    font: 400 10px/1.6 var(--font-sans);
    letter-spacing: .24em;
    text-transform: uppercase;
    color: var(--ctx-accent);
}

/* ---- Call to action ----
   A text link with a hairline under it, never a filled button. The page has one filled control
   and it belongs to the private-showing form; a second one here would put two equal asks on a
   page that wants one, and the quieter of the two is the one that is not the conversion.

   The rule under the label is the page's own gesture, drawn at the width of the words rather
   than at a fixed 92px, so it reads as an underline that means something rather than as a
   hairline that happens to be there. It thickens on hover; it does not move. */

.lp-cta__body { margin-bottom: clamp(28px, 3.4vw, 40px); }

.lp-cta__action {
    display: inline-flex;
    align-items: center;
    gap: 14px;
    padding-bottom: 10px;
    border-bottom: 1px solid var(--ctx-rule-strong);
    font: 400 11px/1 var(--font-sans);
    letter-spacing: .3em;
    text-transform: uppercase;
    color: var(--ctx-text);
    text-decoration: none;
}

.lp-cta__arrow {
    font-size: 14px;
    color: var(--ctx-accent);
}

@media (prefers-reduced-motion: no-preference) {
    .lp-cta__action { transition: border-color .4s ease, color .4s ease; }
    .lp-cta__arrow { transition: transform .4s ease; }
    .lp-cta__action:hover .lp-cta__arrow { transform: translateX(5px); }
}

.lp-cta__action:hover { border-bottom-color: var(--ctx-accent); color: var(--ctx-accent); }
.lp-cta__action:focus-visible { outline: 1px solid var(--ctx-accent); outline-offset: 6px; }

