/* ==========================================================================
   Palm Estate - homepage sections
   --------------------------------------------------------------------------
   Enqueued on the front page only (see functions.php), so inner pages never
   download it. Each Phase 03-08 section is its own numbered block.

   01 Hero
   02 Scroll journey
   03 Market edge (the stats band it holds is in sections.css)
   04 Recognition and CTA (the contact block that follows is in sections.css)
   05 FAQ block
   ========================================================================== */


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

/*
 * Full-viewport hero with the image full-bleed behind left-aligned copy.
 *
 * The image is the LCP element. It is preloaded from <head> with
 * fetchpriority=high (inc/hero.php), carries explicit width/height so its box
 * is reserved before it arrives, and is never lazy-loaded. The Forest
 * background paints in the meantime so the first frame is on-brand rather
 * than white.
 */
.hero {
	/* Component-local values. In the render the Palm is dead centre with the
	   trunk running straight down the middle, so the crop pivots on 50%. */
	--hero-focal-x: 50%;
	--hero-focal-y: 50%;

	/* The copy is pinned to the viewport edge, not the page container: in the
	   design it sits about 6.5% of the width in, and the same distance below
	   the nav bar. One value drives both, and anchoring the top to the bar
	   keeps the gap constant as the bar scales. */
	--hero-inset: 5vw;
	--hero-content-top: calc(var(--header-h) + var(--header-offset) + var(--hero-inset));

	position: relative;
	isolation: isolate;
	display: grid;
	/* A full viewport tall. The render is far wider than 16:9, so filling the
	   height means object-fit crops its sides - the journey below is drawn to
	   the same width (see --journey-media-w) so the two still join. */
	min-height: 100vh;
	min-height: 100svh;
	overflow: hidden;
	background-color: var(--pe-bg-dark);
	color: var(--pe-text-on-dark);
}

.hero__media {
	/*
	 * Colour grade. The vertical gradient's middle stop and an optional
	 * left-edge darkening are local values because they are the two knobs
	 * for headline contrast: "Property," sits over the left crescent's
	 * white villa facades, the brightest ground on the page.
	 *   original spec: mid 25%, left 0%  -> worst pixel behind the headline 3.5:1
	 *   then:          mid 40%, wash 45%, top 80% -> headline 6.3:1, lead 6.5:1
	 *   current:       mid 32%, wash 35%, top 70% (one notch lighter on request)
	 *                  -> headline 4.5:1 (needs 3), lead 4.75:1 (needs 4.5)
	 *   The lead is what limits it: another notch puts its worst pixel under
	 *   4.5:1. Lighter than this means moving the lead off the villas or
	 *   bringing --hero-grade-left in.
	 *   alternative:   mid 25% + left 35% -> 4.5:1 with the Palm at the lighter grade
	 */
	--hero-grade-mid:  32%;
	--hero-grade-left: 0%;

	position: absolute;
	inset: 0;
	z-index: -1;
	isolation: isolate; /* the wash blends with the image, nothing behind it */
}

.hero__media img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: var(--hero-focal-x) var(--hero-focal-y);
}

/*
 * Two grade layers over the image. ::before is the flat Forest wash that
 * unifies the colour; its blend mode is a token because mix-blend-mode is
 * per element, which is why the wash and the gradient are not one element.
 * ::after is the vertical Deep gradient: heavy at the top so the nav has
 * ground to sit on, lightest around the headline, back up at the foot so
 * the hero settles into the palette before the next section. Neither takes
 * pointer events.
 */
.hero__media::before,
.hero__media::after {
	content: "";
	position: absolute;
	inset: 0;
	pointer-events: none;
}

.hero__media::before {
	background-color: var(--grade-wash);
	mix-blend-mode: var(--grade-wash-blend);
}

.hero__media::after {
	background-image:
		linear-gradient(
			to right,
			color-mix(in srgb, var(--pe-deep) var(--hero-grade-left), transparent),
			transparent 60%
		),
		linear-gradient(
			to bottom,
			color-mix(in srgb, var(--pe-deep) 70%, transparent) 0%,
			color-mix(in srgb, var(--pe-deep) var(--hero-grade-mid), transparent) 35%,
			color-mix(in srgb, var(--pe-deep) 50%, transparent) 100%
		);
}

.hero__content {
	align-self: start;
	width: 100%;
	padding-top: var(--hero-content-top);
	padding-inline: var(--hero-inset);
	padding-bottom: var(--space-3xl);
}

/* Line two is italic through the <em>; .display already styles it. */
.hero__title {
	max-width: none;
}

.hero__lead {
	margin-top: var(--space-md);
	color: color-mix(in srgb, var(--pe-cream) 85%, transparent);
}

.hero__cta {
	margin-top: var(--space-lg);
}

/* The trust line: in the hero's bottom-right corner, at the same inset as
   the copy on the left, in quiet Cream. */
.hero__trust {
	position: absolute;
	right: var(--hero-inset);
	bottom: clamp(var(--space-2xl), 10vh, var(--space-4xl));
	margin: 0;
	max-width: none;
	color: color-mix(in srgb, var(--pe-cream) 60%, transparent);
	text-align: right;
}

/*
 * Entrance: headline, sub copy and CTA rise in turn. Declared only where
 * motion is welcome, so under prefers-reduced-motion the elements are simply
 * present at full opacity: there is no animation to suppress and nothing that
 * could leave them stuck invisible.
 */
@media (prefers-reduced-motion: no-preference) {
	.hero__title,
	.hero__lead,
	.hero__cta,
	.hero__trust {
		opacity: 0;
		transform: translateY(var(--space-sm));
		animation: hero-rise var(--dur-slow) var(--ease-out) forwards;
	}

	.hero__trust {
		animation-delay: var(--dur-slow);
	}

	.hero__lead {
		animation-delay: var(--dur-fast);
	}

	.hero__cta {
		animation-delay: calc(var(--dur-fast) * 2);
	}

	/*
	 * Held until the preloader has gone. The preloader takes pe-loading off
	 * the root only once its fade has finished (inc/preloader.php), so the
	 * copy starts rising when it can actually be seen, rather than playing
	 * out unseen beneath the overlay. Paused, not delayed: however long the
	 * page takes to load, the entrance begins at its first frame. Without
	 * JavaScript the class is never set, so it plays straight away; the
	 * preloader's own 6s safety timer lifts it if anything goes wrong.
	 */
	.pe-loading .hero__title,
	.pe-loading .hero__lead,
	.pe-loading .hero__cta,
	.pe-loading .hero__trust {
		animation-play-state: paused;
	}

	@keyframes hero-rise {
		to {
			opacity: 1;
			transform: translateY(0);
		}
	}
}

@media (max-width: 768px) {
	.hero {
		/* A portrait crop of a landscape render loses the sides, which here
		   is only water and the outer fronds: the trunk stays centred. Bias
		   the vertical point down so the Palm wins over the sky. */
		--hero-focal-x: 50%;
		--hero-focal-y: 60%;

		/* 6.5vw is too tight on a phone; fall back to the page gutter, and
		   sit the copy a fixed step under the bar. */
		--hero-inset: var(--gutter);
		--hero-content-top: calc(var(--header-h) + var(--header-offset) + var(--space-xl));
	}

	.hero__content {
		padding-bottom: var(--space-xl);
	}
}


/* 02 Scroll journey ======================================================== */

/*
 * One tall render, panned by scroll.
 *
 * The section is several viewports tall. Inside it a sticky, viewport-high
 * window holds the image, which is much taller than the viewport and
 * translates upward as the section scrolls: the viewport is a window panning
 * down the image. The road SVG, waypoints and labels live in
 * .journey__overlay, a layer above the grade that is panned identically to
 * the image, so they share its coordinate space without being darkened.
 *
 * The pan is a CSS scroll-driven animation on the section's own view
 * timeline. A pinned element's view position never changes, so the tall
 * section is the timeline subject and the animation runs over its `contain`
 * range, which for a subject taller than the scrollport is exactly the
 * pinned period. It runs on the compositor: no scroll listener, no rAF, no
 * layout. Not background-attachment: fixed, which iOS does not honour.
 *
 * The base rules are the fallback. Without animation-timeline support, or
 * under prefers-reduced-motion, the image simply sits in flow at full height
 * and scrolls normally, so nothing is ever hidden. The @supports block adds
 * the window and the pan on top.
 */
.journey {
	/* --journey-ratio (height / width) is set inline from the image file.
	   --journey-zoom is how many viewports tall the image renders: it is
	   what gives the image room to pan, and so what makes the waypoints
	   arrive one after another instead of all at once. A wide render can
	   only gain height by being drawn larger than the window, so raising it
	   crops more off the sides; 1 shows the whole frame and there is no pan
	   at all. The max() keeps the image at least as wide as the window on
	   ultra-wide screens. */
	--journey-zoom:    1;

	/* Which point across the image sits at the centre of the window.
	   
	   Keep this at 50% unless the image is drawn WIDER than the window: the
	   translate is a fraction of the image's own width, so when the two are
	   the same width anything other than 50% slides the image sideways and
	   leaves a bare strip down one edge. It only has room to bite when a
	   zoom above 1 has made the image overflow. */
	--journey-focal-x: 50%;
	/* Drawn at the width the hero is drawn at, so the two line up: the hero
	   covers a viewport-high box, which means its width is whichever is
	   larger of the viewport and the width its own ratio needs. */
	--journey-media-w: max(100vw, calc(100svh / var(--hero-ratio)));
	--journey-media-h: calc(var(--journey-media-w) * var(--journey-ratio) * var(--journey-zoom));
	--journey-pan:     max(0px, calc(var(--journey-media-h) - 100svh));
	/* Scroll distance per unit of pan: 2 means the image moves at half the
	   speed you scroll. Together with --journey-hold this sets how long the
	   road takes to draw - raise either to slow it down. */
	--journey-pace:    2.2;

	/* Pinned scroll on top of the pan. Also the whole of the drawing time on
	   a screen where the image does not pan at all. */
	--journey-hold:    40svh;

	/* The line starts drawing before the section pins: this far into the
	   section's approach, so the road is not sitting bare while it slides up.

	   --journey-span is the scroll the WHOLE sequence occupies - the road
	   drawing and every waypoint's marker, leader and text. One range for all
	   of them, so nothing can drift out of step; the staging lives in the
	   keyframe percentages instead, generated per waypoint in journey.php.
	   The road takes the first 90% of it, leaving the last waypoint - which
	   sits on the line's very end - room to play out. */
	/* Label placement, read by journey.js. --journey-label-reach is how far
	   out from its marker a label sits, as a percentage of the image width,
	   and it is one value for all four so every leader is the same length.
	   --journey-label-inset is the floor: the closest a label may come to the
	   edge of the visible crop, as a percentage of that crop, which is what
	   catches a phone's narrow view. --journey-label-rise lifts the label
	   above its marker, as a percentage of the image HEIGHT - an absolute
	   value flattens the curve the moment a taller render is dropped in. */
	--journey-label-reach: 28;
	--journey-label-inset: 5;
	--journey-label-rise:  3;
	/* A waypoint's paragraph, opposite its label: its inset from the visible
	   edge, and the gap it keeps from the road's centreline alongside it
	   (the road itself and its hedges take up part of that), both as
	   percentages of the visible width, read by journey.js. */
	--journey-body-inset: 8;
	--journey-body-gap:   9;
	/* How far below the label's line the paragraph is centred, as a
	   percentage of the image height, so it holds its place at any size. */
	--journey-body-drop:  15%;
	/* A paragraph set beneath its own label (.journey__body--under): how far
	   below the label's line its first line starts, clear of the descriptor. */
	--journey-body-under: 4.5rem;

	--journey-lead: 40svh;
	--journey-span: calc(100svh - var(--journey-lead) + var(--journey-pan) * var(--journey-pace) + var(--journey-hold));

	position: relative;
	background-color: var(--pe-bg-darkest);
	color: var(--pe-text-on-dark);
}

/* A portrait screen already sees only a slice of a wide render, so it fills
   the height and crops to the centre with no further zoom. */
@media (orientation: portrait) {
	.journey {
		--journey-zoom: 1;
		--journey-hold: 120svh;
	}

	/* Three columns of a phone's width would be slivers. */
	.journey__grid {
		--journey-grid-cols: 2;
	}
}

.journey__sticky {
	position: relative;
	isolation: isolate; /* the wash blends with the image, not the page */
	overflow: hidden;
	contain: paint;     /* nothing inside can invalidate paint outside it */
}

.journey__media {
	position: relative;
	width: 100%;
}

.journey__image {
	display: block;
	width: 100%;
	height: auto;
}

/* Road, markers and labels: a layer above the grade, sized and panned
   exactly like the media (see the @supports block), so it shares the image's
   coordinate space and nothing in it is darkened by the vignette. */
.journey__overlay {
	position: absolute;
	inset: 0;
	pointer-events: none;
}

/*
 * Grid. Hairline Bronze rules at low opacity, as in the design: broad
 * landscape cells, three of them across the window. First in the overlay, so
 * it sits beneath the road and labels and pans with the image.
 */
.journey__grid {
	/*
	 * Sized from the WINDOW, not the image. The image is drawn wider than the
	 * viewport so that it can be a viewport tall, and by how much depends on
	 * the window's shape - so a cell measured as a fraction of the image is a
	 * different number of columns on screen on every machine. Against 100vw
	 * it is exactly --journey-grid-cols columns wherever you look.
	 *
	 * The cells are rectangles, not squares: --journey-grid-aspect is how many
	 * times wider than tall.
	 */
	--journey-grid-cols:   3;
	--journey-grid-aspect: 1.6;
	--journey-grid-w: calc(100vw / var(--journey-grid-cols));
	--journey-grid-h: calc(var(--journey-grid-w) / var(--journey-grid-aspect));
	--journey-grid-rule: color-mix(in srgb, var(--pe-bronze) 18%, transparent);

	position: absolute;
	inset: 0;
	background-image:
		linear-gradient(to right, var(--journey-grid-rule) var(--hairline), transparent var(--hairline)),
		linear-gradient(to bottom, var(--journey-grid-rule) var(--hairline), transparent var(--hairline));
	background-size:
		var(--journey-grid-w) 100%,
		100% var(--journey-grid-h);

	/*
	 * Faded in from the top edge and out at the foot. The hero and this
	 * section are one continuous picture, and the grid's first row line lands
	 * exactly on the join - a hard rule straight across it, which is the one
	 * thing that gives the seam away. Fading means nothing is drawn at the
	 * edge at all.
	 */
	-webkit-mask-image: linear-gradient(to bottom, transparent, black 14%, black 88%, transparent);
	mask-image: linear-gradient(to bottom, transparent, black 14%, black 88%, transparent);
}

/*
 * The road. The SVG fills exactly the image's box and its viewBox is the
 * image's pixel size, so path coordinates are image pixels and the line sits
 * on the road at any rendered size. Visible in Bronze for the alignment check;
 * step 3 turns it into a scroll-driven draw.
 */
.journey__road {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	overflow: visible;
	pointer-events: none;
}


/*
 * The line is revealed by a WINDOW sliding down the image. The road never
 * doubles back upward, so a vertical sweep looks the same as drawing along the
 * path; what matters is how the sweep is made, because it runs on every frame
 * of the scroll.
 *
 * It is two nested boxes, both the size of the image, moved in opposite
 * directions by the same amount. .journey__reveal clips to its own box and is
 * pulled UP, so its bottom edge - the edge of what is shown - sits that far up
 * the image. .journey__reveal-track, inside it, is pushed back DOWN by exactly
 * as much, so the line itself never moves. Only the clip edge travels.
 *
 * Both are `translate`, which the compositor animates from the scroll position
 * directly: the line is rasterised once and never repainted. This replaced an
 * animated clip-path, which looks identical but cannot run on the compositor -
 * every frame it repainted the line, and because the line shares a layer with
 * the grid that meant re-rasterising the whole ~2000x2250 overlay (four times
 * that on a 2x display). Measured over 600 frames of scroll it was 86% of all
 * painting on the page, and it is what made the journey stutter. (A mask,
 * before that, was worse still.) Do not swap it back for clip-path.
 *
 * The percentages are of each box's own height, which is the image's, so the
 * two cancel exactly. Base state is the FINISHED line - down to the last label
 * and no further - which is the fallback for no scroll-timeline support and for
 * reduced motion. Both cuts are set inline from the tracing tool:
 * --journey-road-cut is how much of the image sits below the road's first
 * point, and --journey-road-end how much sits below the last waypoint, where
 * the drawn line stops. The traced path carries on beneath that, never shown:
 * past the last label there is nothing left to point at.
 */
.journey__reveal,
.journey__reveal-track {
	position: absolute;
	inset: 0;
	pointer-events: none;
}

.journey__reveal {
	overflow: hidden;
	translate: 0 calc(-1 * var(--journey-road-end));
}

.journey__reveal-track {
	translate: 0 var(--journey-road-end);
}

/* Dashed, Cream as in the design; a constant 2px on screen whatever the image
   scales to. Dash and gap are in path units and tunable here. */
.journey__path {
	--journey-dash: 6;
	--journey-gap: 8;

	fill: none;
	stroke: var(--pe-text-on-dark);
	stroke-width: 2;
	stroke-linecap: round;
	stroke-linejoin: round;
	stroke-dasharray: var(--journey-dash) var(--journey-gap);
	vector-effect: non-scaling-stroke;
}

/*
 * The traveller: the light at the leading edge of the line. Shown only where
 * the scroll-driven journey runs and journey.js has written its keyframes
 * (.has-traveller): everywhere else the line is already finished and there
 * is nothing to lead. Like a waypoint it is a box the size of the overlay,
 * moved by a percentage translate that is therefore a percentage of the
 * image, with the light drawn at its corner - so the whole ride is one
 * compositor animation and nothing is ever laid out or painted per frame.
 */
.journey__traveller {
	display: none;
}

.journey__traveller::before,
.journey__traveller::after {
	content: "";
	position: absolute;
	top: 0;
	left: 0;
	border-radius: var(--radius-pill);
	translate: -50% -50%;
}

/* The light itself: a Cream point with a Bronze bloom close around it. */
.journey__traveller::before {
	width: 0.5rem;
	height: 0.5rem;
	background-color: var(--pe-text-on-dark);
	box-shadow:
		0 0 0 var(--space-3xs) color-mix(in srgb, var(--pe-deep) 60%, transparent),
		0 0 0.75rem 0.125rem color-mix(in srgb, var(--pe-accent) 80%, transparent);
}

/* The halo, breathing on its own clock so the light never sits dead still
   between scrolls. */
.journey__traveller::after {
	width: 3.5rem;
	height: 3.5rem;
	background: radial-gradient(circle, color-mix(in srgb, var(--pe-accent) 40%, transparent), transparent 70%);
	animation: journey-breathe 2.8s ease-in-out infinite;
}

/*
 * Waypoints. Each sits at a fraction of the road's length; journey.js asks
 * the path for that point and writes it as --journey-x / --journey-y (a
 * percentage of the image box), and derives the label anchor as
 * --journey-lx / --journey-ly. The list item is not positioned itself: the
 * marker and the label each position from the overlay using those inherited
 * values, so the item can carry the fade for both. Hidden until placed.
 */
.journey__waypoints {
	margin: 0;
	padding: 0;
	list-style: none;
}

/* Each item is a full-size box over the overlay, not a zero-height line, so
   the marker's and label's percentages resolve against the image whatever
   the item itself is doing (the fade animates it, and an animated element
   becomes its children's containing block). */
.journey__waypoint {
	position: absolute;
	inset: 0;
	visibility: hidden;
}

.journey__waypoint.is-placed {
	visibility: visible;
}

/* The numbered circle, centred on its point. A Deep halo lifts it off the
   road surface. */
.journey__marker {
	position: absolute;
	left: var(--journey-x, 50%);
	top: var(--journey-y, 50%);
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 1.75rem;
	height: 1.75rem;
	translate: -50% -50%;
	color: var(--pe-text-on-dark);
	font-family: var(--font-body);
	font-size: var(--fs-3xs);
	font-weight: 500;
	letter-spacing: var(--ls-wide);
	line-height: 1;
	background-color: var(--pe-bg-dark);
	border: var(--hairline) solid var(--pe-accent);
	border-radius: var(--radius-pill);
	box-shadow: 0 0 0 var(--space-3xs) color-mix(in srgb, var(--pe-deep) 60%, transparent);
}

/*
 * Labels. Anchored near the image edge on their side, a little above the
 * marker: the anchor is the outer end of the leader, and the word sits just
 * above that line with the descriptor just below it, aligned to the outer
 * edge. The leader itself is an SVG curve (journey__leader) drawn from this
 * anchor to the marker, so it runs under the word and then bends down to
 * the circle, however far away that is.
 */
.journey__label {
	position: absolute;
	top: var(--journey-ly, 50%);
	white-space: nowrap;
}

/*
 * Flush waypoints (Buy, Sell and Stay): placed by the frame, not by the road.
 * Each label sits --journey-label-extent of the way from its marker to the
 * edge of the visible image on its side - 1 would be against the edge, 0.9
 * stops a tenth of the line short - so its line runs most of the way across
 * whatever the window's width, instead of a set distance that means something
 * different on every screen.
 *
 * The far end is the inset floor, 2% of the visible width (about 38px on a
 * 1920px window), so a flush label can never be pushed off screen.
 */
.journey__waypoint--flush {
	--journey-label-extent: 0.9;
	--journey-label-inset: 2;
}

/* 2% of a phone is under 10px: the text would all but touch the glass, so
   flush labels keep the ordinary gutter there. After the rule above on
   purpose - same specificity, so source order decides. */
@media (orientation: portrait) {
	.journey__waypoint--flush {
		--journey-label-inset: 5;
	}
}

.journey__waypoint--left .journey__label {
	left: var(--journey-lx, 10%);
	text-align: left;
}

.journey__waypoint--right .journey__label {
	right: calc(100% - var(--journey-lx, 90%));
	text-align: right;
}

.journey__word,
.journey__desc {
	position: absolute;
	display: block;
}

.journey__waypoint--left .journey__word,
.journey__waypoint--left .journey__desc {
	left: 0;
}

.journey__waypoint--right .journey__word,
.journey__waypoint--right .journey__desc {
	right: 0;
}

.journey__word {
	bottom: var(--space-2xs);
	color: var(--pe-text-on-dark);
	font-family: var(--font-display);
	font-size: var(--fs-xl);
	font-weight: 400;
	line-height: 1;
	letter-spacing: var(--ls-wide);
	text-transform: uppercase;
}

.journey__desc {
	top: var(--space-xs);
}

/*
 * The leader curve, in the road's own units; a hairline on screen. It is
 * drawn from the marker outward, using its own length (measured once by
 * journey.js and written to --journey-leader-len) as a single dash: offset
 * that dash by the full length and the line is hidden, wind it to zero and
 * the line draws on.
 *
 * Base state is drawn, which is the fallback for no scroll-timeline support
 * and for reduced motion.
 */
/*
 * A waypoint's paragraph, on the other side of the road from its label,
 * centred --journey-body-drop below the label's line. journey.js sets where
 * it starts and how much room there is before the road (--journey-bx,
 * --journey-bw); the measure is capped so a wide window does not stretch it
 * into long lines. Bronze, like the descriptor beneath the label.
 */
.journey__body {
	position: absolute;
	top: calc(var(--journey-ly, 50%) + var(--journey-body-drop));
	left: var(--journey-bx, 5%);
	width: min(var(--journey-body-max, 34rem), var(--journey-bw, 30%));
	max-width: none;
	margin: 0;
	translate: 0 -50%;
	/* Cream at 75%, through the colour, not opacity: opacity is what its
	   fade-in animates, and would be overwritten. Jost Light. */
	color: color-mix(in srgb, var(--pe-cream) 75%, transparent);
	font-weight: 300;
	font-size: var(--fs-lg);
	line-height: var(--lh-loose);
	text-wrap: pretty;
	/* A soft Deep halo, invisible on the dark grass, that keeps the words
	   legible where they cross a lit tree or hedge in the render. */
	text-shadow:
		0 0 0.75rem color-mix(in srgb, var(--pe-deep) 90%, transparent),
		0 0 0.25rem color-mix(in srgb, var(--pe-deep) 80%, transparent);
}

/* Set against the frame's edge on its side instead of starting at the road:
   its outer edge sits at the inset, and it widens back towards the road. Used
   on the right, where journey.js's --journey-be is that outer edge. */
.journey__body--edge {
	left: auto;
	right: calc(100% - var(--journey-be, 95%));
}

/* Beneath its own label instead of across the road: top-aligned a fixed step
   under the label's line, so it always clears the descriptor. */
.journey__body--under {
	top: calc(var(--journey-ly, 50%) + var(--journey-body-under));
	translate: none;
}

.journey__leader {
	fill: none;
	stroke: var(--pe-accent);
	stroke-width: 1;
	stroke-linecap: round;
	vector-effect: non-scaling-stroke;
	stroke-dasharray: var(--journey-leader-len, none);
	stroke-dashoffset: 0;
}

@media (max-width: 768px) {
	/* On a phone the image is cropped to its centre and the road takes most
	   of the width, so each label keeps to a narrow band at its side: the
	   word drops a size and the descriptor wraps. The limits go on the word
	   and descriptor themselves, since the label box has no size of its own. */
	.journey__word,
	.journey__desc {
		max-width: 22vw;
		white-space: normal;
	}

	.journey__word {
		font-size: var(--fs-lg);
	}

	/* A phone sees a narrow centre crop with the road across most of it:
	   there is no room beside the road for a paragraph. */
	.journey__body {
		display: none;
	}
}

/*
 * Colour grade on the window rather than the image, so the edge darkening
 * stays at the viewport edges while the image pans beneath it. Same two
 * layers as the hero with a radial fall-off: clear through the centre so the
 * road stays open, Deep at the sides where the labels sit.
 *
 * The edge was Deep 65%; now 45%, lightened on request. The render is a
 * night scene, so the shared wash alone barely shows here - the edge is the
 * knob that does. The seam with the hero is set by the wash and the hero's
 * bottom stop, not by this, so it can move without the join showing.
 */
.journey__vignette {
	position: absolute;
	inset: 0;
	pointer-events: none;
}

.journey__vignette::before,
.journey__vignette::after {
	content: "";
	position: absolute;
	inset: 0;
}

.journey__vignette::before {
	background-color: var(--grade-wash);
	mix-blend-mode: var(--grade-wash-blend);
}

.journey__vignette::after {
	background-image: radial-gradient(
		ellipse at center,
		transparent 30%,
		color-mix(in srgb, var(--pe-deep) 45%, transparent) 100%
	);
}

@supports (animation-timeline: view()) {
	@media (prefers-reduced-motion: no-preference) {
		.journey {
			height: calc(100svh + var(--journey-pan) * var(--journey-pace) + var(--journey-hold));
			view-timeline-name: --journey;
			view-timeline-axis: block;
			/* By default a view timeline insets the scrollport by the root's
			   scroll-padding - here the header's clearance - so the pan would
			   begin 115px before the sticky window actually pins. Zero: the
			   timeline sees the real viewport, as the sticky does. */
			view-timeline-inset: 0;
		}

		.journey__sticky {
			position: sticky;
			top: 0;
			height: 100svh;
		}

		/* Placed horizontally by the translate's first value, which pulls the
		   image left by --journey-focal-x of its own width so that point
		   lands at the centre of the window; the pan animates the second.
		   Width follows from height and ratio so the image is never
		   distorted. The overlay gets the identical box and animation, so
		   road and labels stay on the image. */
		.journey__media,
		.journey__overlay {
			position: absolute;
			top: 0;
			left: 50%;
			width: calc(var(--journey-media-h) / var(--journey-ratio));
			height: var(--journey-media-h);
			translate: calc(-1 * var(--journey-focal-x)) 0;
			animation: journey-pan linear both;
			animation-timeline: --journey;
			animation-range: contain 0% contain 100%;
		}

		.journey__image {
			height: 100%;
		}

		/*
		 * Every scroll-driven piece of the journey shares this one range, so
		 * the road and its waypoints cannot drift apart. What differs is only
		 * the keyframes: the road's finish at 90%, and each waypoint names its
		 * own set (generated in journey.php) whose stages begin at the point
		 * the road reaches it. Nothing about a waypoint shows before then.
		 */
		.journey__reveal,
		.journey__reveal-track,
		.journey__marker,
		.journey__leader,
		.journey__label,
		.journey__body {
			animation-fill-mode: both;
			animation-timing-function: linear;
			animation-timeline: --journey;
			animation-range: cover var(--journey-lead) cover calc(var(--journey-lead) + var(--journey-span));
		}

		.journey__reveal {
			animation-name: journey-reveal-window;
		}

		/* Its keyframes (journey-traveller) are written by journey.js; it
		   shares the road's range and timing above. */
		.journey.has-traveller .journey__traveller {
			display: block;
			position: absolute;
			inset: 0;
			pointer-events: none;
			opacity: 0;
			animation-name: journey-traveller;
			animation-fill-mode: both;
			animation-timing-function: linear;
			animation-timeline: --journey;
			animation-range: cover var(--journey-lead) cover calc(var(--journey-lead) + var(--journey-span));
		}

		.journey__reveal-track {
			animation-name: journey-reveal-track;
		}

		/* The waypoint parts take their animation-name inline, per waypoint;
		   a paragraph shares its label's, so the two arrive together. */
		.journey__marker,
		.journey__label,
		.journey__body {
			opacity: 0;
		}

		/* The grid fades in as the section enters, before the line starts. */
		.journey__grid {
			opacity: 0;
			animation: journey-appear var(--ease-out) both;
			animation-timeline: --journey;
			animation-range: entry 10% entry 45%;
		}

		/*
		 * Arrivals. Once journey.js is watching the arrival points it adds
		 * .has-arrivals, and a waypoint's leader, label and paragraph stop
		 * following the scroll: they wait, hidden, until its marker has
		 * landed (.is-arrived), then play in on their own - the leader draws
		 * out while the text fades in just behind it - whether or not the
		 * visitor keeps scrolling. Scrolling back above the marker hides them
		 * again, so the sequence replays on the way down.
		 *
		 * !important only to overrule the per-waypoint animation-name set
		 * inline in the markup, which stays for the no-script fallback.
		 */
		.journey.has-arrivals .journey__leader,
		.journey.has-arrivals .journey__label,
		.journey.has-arrivals .journey__body {
			animation-name: none !important;
		}

		.journey.has-arrivals .journey__leader {
			stroke-dashoffset: var(--journey-leader-len, 0);
			transition: stroke-dashoffset 900ms var(--ease);
		}

		.journey.has-arrivals .journey__leader.is-arrived {
			stroke-dashoffset: 0;
		}

		.journey.has-arrivals .journey__label,
		.journey.has-arrivals .journey__body {
			opacity: 0;
			transition: opacity 700ms var(--ease) 250ms;
		}

		.journey.has-arrivals .journey__waypoint.is-arrived .journey__label,
		.journey.has-arrivals .journey__waypoint.is-arrived .journey__body {
			opacity: 1;
		}
	}
}

/*
 * An arrival point: from the scroll position where a waypoint's marker has
 * landed down to the foot of the section. The waypoint timeline runs from
 * --journey-lead into the section over --journey-span, so the marker lands at
 * that fraction of the span, and the strip's top edge enters the viewport at
 * exactly that scroll. It runs on to the foot rather than being a single
 * pixel so that a fast scroll or a jump past the point still registers as a
 * change the observer reports. Never seen, never in the way.
 */
.journey__arrive {
	position: absolute;
	top: calc(var(--journey-lead) + var(--journey-span) * var(--journey-arrive, 0) / 100);
	bottom: 0;
	left: 0;
	width: 1px;
	opacity: 0;
	pointer-events: none;
}

@keyframes journey-pan {
	from {
		translate: calc(-1 * var(--journey-focal-x)) 0;
	}

	to {
		translate: calc(-1 * var(--journey-focal-x)) calc(-1 * var(--journey-pan));
	}
}

/* The two halves of the reveal: equal and opposite, so the line stays put and
   only the window's edge moves. Finished at 90%: the remaining tenth of the
   range is what lets the last waypoint, sitting on the line's end, run its own
   sequence. */
@keyframes journey-reveal-window {
	from {
		translate: 0 calc(-1 * var(--journey-road-cut));
	}

	90%, 100% {
		translate: 0 calc(-1 * var(--journey-road-end));
	}
}

@keyframes journey-reveal-track {
	from {
		translate: 0 var(--journey-road-cut);
	}

	90%, 100% {
		translate: 0 var(--journey-road-end);
	}
}

@keyframes journey-breathe {
	0%, 100% {
		opacity: 0.55;
		scale: 1;
	}

	50% {
		opacity: 1;
		scale: 1.3;
	}
}

/* Opacity only: the item spans the whole overlay, so a scale here would move
   the marker, not pop it. */
@keyframes journey-appear {
	from {
		opacity: 0;
	}

	to {
		opacity: 1;
	}
}


/* 03 Market edge ================================================= */

/*
 * Cream arcs up over the journey, carries the headline and body, and a dashed
 * connector leads down into the Forest stats band; Cream then arcs back up
 * over that band to become the ground for the team section. The band and the
 * team are shared with About, so their styles live in sections.css.
 *
 * The section paints no background of its own: each block carries its own, so
 * the arcs (pulled above their block by the divider rule in components.css)
 * overlay the darker section behind rather than their own ground.
 */
.edge {
	position: relative;
	z-index: 1; /* the top arc overlays the journey's sticky window */
}

.edge__intro,
.edge__outro {
	background-color: var(--pe-bg);
	color: var(--pe-text);
}

.edge__copy {
	position: relative;
	z-index: 2; /* the connector's dot drops past the band's Cream dome */
	padding-block: var(--space-2xl) 0;
}

/* Centred on the title: the paragraph measure would otherwise leave it a
   left-anchored box with its text centred inside it. The left padding
   balances the tracking after the last letter. */
.edge__eyebrow {
	max-width: none;
	padding-left: var(--ls-caps);
	color: var(--pe-accent-on-light);
}

/* Line one in Ink, line two italic in the accent. Bronze itself is 2:1 on
   Cream, so accent text on light uses the darkened mix (style.css). */
.edge__title {
	margin-top: var(--space-lg);
	font-size: var(--fs-2xl);
	line-height: var(--lh-snug);
	text-wrap: balance;
}

.edge__title em {
	color: var(--pe-accent-on-light);
	font-style: italic;
}

/* Two lines on a wide measure, in Ink, close under the title, as the design
   sets it. */
.edge__body {
	margin-top: var(--space-md);
	margin-inline: auto;
	max-width: 40rem;
	color: var(--pe-text);
	font-size: 0.9375rem;
	line-height: var(--lh-base);
	text-wrap: balance;
}

/*
 * Connector: a dashed rule dropping from the copy into the stats band, ending
 * in a Bronze dot. Decoration, so it can be pure Bronze without a contrast
 * obligation. The dot is drawn on the pseudo-element rather than as a second
 * node.
 */
.edge__connector {
	/* The band's Cream dome below (its arc's depth, sections.css) and the
	   distance the dot lands beneath it, in the Forest. */
	--edge-dome: clamp(1.25rem, 1.75vw, 2.75rem);
	--edge-drop: 0.75rem;

	position: relative;
	display: block;
	width: 0;
	height: calc(var(--space-2xl) + var(--edge-dome) + var(--edge-drop));
	margin: var(--space-2xl) auto calc(-1 * (var(--edge-dome) + var(--edge-drop)));
	border-left: var(--hairline) dashed var(--pe-accent-on-light);
}

.edge__connector::after {
	content: "";
	position: absolute;
	bottom: 0;
	left: 50%;
	width: 0.4375rem;
	height: 0.4375rem;
	background-color: var(--pe-accent);
	border-radius: var(--radius-pill);
	translate: -50% 50%;
}

/*
 * The section's arcs are shallow, matching the stats band's (sections.css):
 * a wide, gentle sweep, and shallow enough that the top one grazes the
 * journey rather than covering the lower villa.
 */
.edge__arc {
	--divider-h: clamp(1.25rem, 1.75vw, 2.75rem);
}

/* The outro carries only the arc: the team section below supplies the Cream
   ground it hands over to. */
.edge__outro {
	padding-bottom: 0;
}


/* 04 Recognition and CTA =================================================== */

/*
 * The press and awards rows continue the Cream the team sits on, then Cream
 * arcs down into the Forest band that closes the page.
 */
.recognition {
	background-color: var(--pe-bg);
	color: var(--pe-text);
	padding-bottom: var(--space-3xl);
}

/* A short Bronze rule leads into the eyebrow, matching the team's header. */
.recognition__eyebrow {
	display: flex;
	align-items: center;
	gap: var(--space-2xs);
	margin-bottom: var(--space-xl);
	color: var(--pe-accent-on-light);
	font-size: var(--fs-2xs);
}

.recognition__eyebrow::before {
	content: "";
	width: var(--space-md);
	height: var(--hairline);
	background-color: var(--pe-accent);
}

/* Press row ---------------------------------------------------------------- */

/*
 * Four outlets, then three awards. Both rows are hairline-ruled above, so the
 * page reads as a stack of records rather than free-floating text.
 */
.recognition__press,
.recognition__awards {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--space-lg);
	margin: 0;
	padding-top: var(--space-xl);
	list-style: none;
	border-top: var(--hairline) solid var(--pe-rule);
}

.recognition__awards {
	margin-top: var(--space-2xl);
}

.recognition__outlet,
.recognition__award {
	display: flex;
	flex-direction: column;
	gap: var(--space-3xs);
}

/* Outlet in serif, what they covered beneath it in accent caps. */
.recognition__name {
	font-family: var(--font-display);
	font-size: var(--fs-md);
	line-height: var(--lh-snug);
}

.recognition__subject {
	color: var(--pe-accent-on-light);
}

/* Awards row --------------------------------------------------------------- */

.recognition__year {
	color: var(--pe-accent-on-light);
}

.recognition__title {
	font-family: var(--font-display);
	font-size: var(--fs-md);
	line-height: var(--lh-snug);
	text-wrap: balance;
}

.recognition__note {
	color: var(--pe-text-muted);
	font-size: var(--fs-sm);
	line-height: var(--lh-base);
}

@media (min-width: 640px) {
	.recognition__press {
		grid-template-columns: repeat(2, 1fr);
	}
}

@media (min-width: 1024px) {
	.recognition__press {
		grid-template-columns: repeat(4, 1fr);
	}

	.recognition__awards {
		grid-template-columns: repeat(3, 1fr);
		gap: var(--space-xl);
	}
}

/* CTA band ----------------------------------------------------------------- */

.cta {
	background-color: var(--pe-bg-dark);
	color: var(--pe-text-on-dark);
	/* No top padding: the arc inside the band supplies the space above. */
	padding-bottom: var(--space-4xl);
}

.cta__inner {
	padding-top: var(--space-3xl);
}

.cta__title {
	font-size: var(--fs-3xl);
	line-height: var(--lh-snug);
	text-wrap: balance;
}

.cta__title em {
	font-style: italic;
}

.cta__body {
	margin-top: var(--space-lg);
	margin-inline: auto;
	max-width: 46ch;
	color: color-mix(in srgb, var(--pe-cream) 80%, transparent);
	line-height: var(--lh-loose);
	text-wrap: pretty;
}

/* Filled Cream and outlined Bronze, side by side; they stack on a phone
   rather than shrinking below a comfortable tap target. */
.cta__actions {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--space-sm);
	margin-top: var(--space-xl);
}

@media (min-width: 480px) {
	.cta__actions {
		flex-direction: row;
		justify-content: center;
	}
}


/* 05 FAQ block ============================================================= */

/*
 * Six questions between the team and the recognition rows, on Cream between
 * Cream: a hairline rule across the content width opens it, no arcs. Two
 * columns from 1280px, 40 / 60, the intro centred on the accordion's height.
 * The accordion itself is the shared component (components.css).
 */
.faq-home {
	position: relative;
	background-color: var(--pe-bg);
	color: var(--pe-text);
	padding-bottom: var(--space-section);
}

.faq-home__inner {
	display: grid;
	gap: var(--space-2xl);
	padding-top: var(--space-lg);
}

/* The rule that opens the block, on the content width rather than the
   container's outer edge: a first grid row across both columns. */
.faq-home__inner::before {
	content: "";
	grid-column: 1 / -1;
	height: var(--hairline);
	background-color: var(--pe-rule);
}

/* Two columns only once the 40% column can hold the heading's two lines. */
@media (min-width: 1280px) {
	.faq-home__inner {
		grid-template-columns: 2fr 3fr;
		align-items: center;
		gap: var(--space-4xl);
	}
}

/* A step under the section titles, so each line holds within the 40%
   column: "Any questions?" on one, "We've got you." on the next. */
.faq-home__title {
	font-size: clamp(2rem, 1.1rem + 2.3vw, 3.1rem);
	line-height: var(--lh-tight);
}

.faq-home__title em {
	font-style: italic;
	color: var(--pe-accent-on-light);
}

.faq-home__body {
	max-width: 34ch;
	margin-top: var(--space-md);
	color: var(--pe-text-muted);
	line-height: var(--lh-loose);
}

/* The link: letter-spaced Bronze caps with the arrow on the line beneath,
   which slides right on hover. */
.faq-home__more {
	display: inline-flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--space-2xs);
	margin-top: var(--space-xl);
	color: var(--pe-accent-on-light);
	text-decoration: none;
}

.faq-home__more-arrow {
	font-size: var(--fs-lg);
	line-height: 1;
	transition: transform var(--dur-fast) var(--ease);
}

.faq-home__more:hover .faq-home__more-arrow,
.faq-home__more:focus-visible .faq-home__more-arrow {
	transform: translateX(0.25rem);
}

.faq-home__more:focus-visible {
	outline: 2px solid var(--pe-accent-on-light);
	outline-offset: 4px;
}

.faq-home__accordion {
	margin-top: 0;
}
