/* ==========================================================================
   Palm Estate - motion
   --------------------------------------------------------------------------
   How things arrive and respond, in one place. Nothing here sets layout,
   spacing, type or colour: it animates opacity and transform only (the
   individual translate and scale properties, so a scroll reveal and a hover
   transform on the same element compose instead of fighting).

   One easing curve for everything: --ease in style.css, a gentle ease-out.

   Arrivals are scroll-driven (animation-timeline: view()), so they run on
   the compositor with no scroll listener. They sit inside
   @supports (animation-timeline: view()): a browser without it (Firefox,
   today) reads none of it and shows every element in its final state.
   They also sit inside prefers-reduced-motion: no-preference, so a visitor
   who has asked for less motion gets the same: everything visible, still.

   Scroll-driven animations have no clock, so a millisecond animation-delay
   does nothing on them. Stagger and "the heading, then its second line, then
   the body" are therefore offsets along the scroll, measured in beats: one
   beat is a small slice of viewport height, which at a normal scrolling pace
   lands at roughly 60 to 80ms. The offsets come from the index property --i
   set in the markup, or from a role (--order) set below, never from
   nth-child.

   01 Beats and keyframes
   02 Arrivals
   03 Headlines
   04 Staggered groups
   05 Images
   06 Footer
   07 Nav leaving the hero
   08 Links and buttons
   09 Property card hover
   10 Accordion
   ========================================================================== */


/* 01 Beats and keyframes =================================================== */

:root {
	/* How far an element scrolls into view while it arrives. */
	--reveal-run: 20vh;
	/* One step of stagger. */
	--beat: 3vh;
}

@keyframes pe-rise {
	from {
		opacity: 0;
		translate: 0 20px;
	}
	to {
		opacity: 1;
		translate: 0 0;
	}
}

@keyframes pe-fade {
	from { opacity: 0; }
	to   { opacity: 1; }
}

@keyframes pe-settle {
	from { scale: 1.06; }
	to   { scale: 1; }
}

@keyframes pe-nav-tighten {
	to { scale: 0.97; }
}


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

		/* 02 Arrivals ====================================================== */

		/*
		 * Every revealed element: rises 20px and fades in over the first
		 * stretch of scroll after its top edge enters the viewport, shifted
		 * later by its order in beats. An element already on screen when the
		 * page opens is past its range, so it is simply there.
		 */
		:where(
			/* Section openers and body copy, by role (order set in 03). */
			.page-band__eyebrow, .property__back, .page-band__title, .page-band__lead, .property__price,
			.listings-band__facts, .finder, .listings__count, .pagination,
			.edge__eyebrow, .edge__title, .edge__body, .edge__connector,
			.team__eyebrow, .team__title, .team__note,
			.faq-home__title, .faq-home__body, .faq-home__more,
			.recognition__eyebrow,
			.cta__title, .cta__body, .cta__actions,
			.contact__eyebrow, .contact__title, .contact__body, .contact__details, .contact__form,
			.story__eyebrow, .story__title, .story__body,
			.split__eyebrow, .split__title,
			.faq__group-eyebrow, .faq__group-title, .faq__more,
			.property-gallery, .property__facts, .property__section, .agent-card,
			.contact-map__header, .contact-map__frame, .contact-map__link,
			/* Repeated children, by index (04). */
			.stats__item, .team__member, .property-card, .accordion__item,
			.recognition__outlet, .recognition__award, .split__half, .property__fact
		) {
			animation: pe-rise var(--ease) both;
			animation-timeline: view();
			animation-range:
				entry calc(var(--order, 0) * var(--beat))
				entry calc(var(--reveal-run) + var(--order, 0) * var(--beat));
		}


		/* 03 Headlines ===================================================== */

		/*
		 * The serif heading (and the eyebrow above it) leads; its body copy
		 * follows two beats behind and any actions three. Where a heading is
		 * two lines with a second, italic line, that line fades in just
		 * after the first: opacity only, since it is an inline run inside
		 * the rising heading, on the heading's own view timeline.
		 */
		:where(
			.page-band__lead, .property__price, .edge__body, .team__note, .faq-home__body,
			.cta__body, .contact__body, .story__body, .contact-map__frame
		) {
			--order: 2;
		}

		:where(
			.listings-band__facts, .finder, .edge__connector, .faq-home__more, .cta__actions,
			.contact__details, .contact-map__link
		) {
			--order: 3;
		}

		/* The form sits beside the intro, so it arrives with the heading's
		   body rather than after the details below it. */
		:where(.contact__form, .agent-card) {
			--order: 1;
		}

		:where(
			.edge__title, .team__title, .faq-home__title, .cta__title,
			.story__title, .split__title, .contact__title
		) {
			view-timeline: --pe-heading block;
		}

		:where(
			.edge__title, .team__title, .faq-home__title, .cta__title,
			.story__title, .split__title
		) em,
		:where(.contact__title) .contact__title-bold {
			animation: pe-fade var(--ease) both;
			animation-timeline: --pe-heading;
			animation-range:
				entry calc(1.4 * var(--beat))
				entry calc(var(--reveal-run) + 1.4 * var(--beat));
		}


		/* 04 Staggered groups ============================================== */

		/*
		 * Stat columns, team portraits, property cards, FAQ items, press and
		 * awards rows, About's two halves, a property's facts: each child's
		 * --i (set in the template, and by listings.js as filters change)
		 * is its order. Team portraits and property cards carry their
		 * column, so every row arrives left to right.
		 */
		:where(
			.stats__item, .team__member, .property-card, .accordion__item,
			.recognition__outlet, .recognition__award, .split__half, .property__fact
		) {
			--order: var(--i, 0);
		}


		/* 05 Images ======================================================== */

		/*
		 * Photographs settle from 1.06 to 1 as they come into view, inside
		 * the frames that already clip them, alongside their frame's fade.
		 * Slower than the rise: over the whole approach to mid-screen.
		 *
		 * The frames clip with overflow: hidden, which makes each one a
		 * scroll container, so view() on the photograph would track the
		 * frame (which never scrolls) and read as arrived. The nearest
		 * element whose own scroller is the page names the timeline instead:
		 * the frame, or for a property card the card itself, which clips too.
		 */
		:where(.property-card, .property-gallery__main, .agent-card__portrait, .team__portrait) {
			view-timeline: --pe-frame block;
		}

		:where(.property-card__image, .property-gallery__image, .agent-card__image, .team__image) {
			animation: pe-settle var(--ease) both;
			animation-timeline: --pe-frame;
			animation-range: entry 0% cover 50%;
		}


		/* 06 Footer ======================================================== */

		/*
		 * The footer ends the page, so its last rows can never scroll far
		 * past the bottom edge. Its ranges are fractions of each element's
		 * own entry, which always completes by the time the page is at its
		 * foot: mark, then the four columns in turn, then the legal row.
		 */
		:where(.site-footer__mark, .site-footer__col, .site-footer__legal) {
			animation: pe-rise var(--ease) both;
			animation-timeline: view();
			animation-range:
				entry calc(var(--i, 0) * 7%)
				entry calc(60% + var(--i, 0) * 7%);
		}

	}
}


/* 07 Nav leaving the hero ================================================== */

/*
 * On the homepage the pill starts as a tinted glass over the hero. As the
 * hero scrolls away it tightens slightly and its Forest fill fades up, both
 * tied to the scroll rather than switched at a threshold. The fill is a
 * layer under the pill's contents, so it can be faded with opacity; the
 * is-solid class navigation.js sets still drops the blur once the fill is
 * opaque and has nothing left to show through.
 *
 * Without scroll timelines, or with reduced motion, the class swap stays as
 * it was: the fill changes colour with a short transition, instantly when
 * reduced.
 */
@media screen and (prefers-reduced-motion: no-preference) {
	@supports (animation-timeline: scroll()) {
		.site-header--overlay .site-header__inner {
			position: relative;
			isolation: isolate;
			animation: pe-nav-tighten var(--ease) both;
			animation-timeline: scroll(root);
			animation-range: 40svh 100svh;
		}

		.site-header--overlay .site-header__inner::before {
			content: "";
			position: absolute;
			inset: 0;
			z-index: -1;
			border-radius: inherit;
			background-color: var(--pe-bg-dark);
			pointer-events: none;
			animation: pe-fade var(--ease) both;
			animation-timeline: scroll(root);
			animation-range: 40svh 100svh;
		}

		/* The fill is the layer now: the pill keeps its glass tint underneath. */
		.site-header--overlay.is-solid .site-header__inner {
			background-color: color-mix(in srgb, var(--pe-deep) 35%, transparent);
		}
	}
}


/* 08 Links and buttons ===================================================== */

/*
 * Text links that carry no underline at rest draw a Bronze one from the left
 * on hover and focus: the header menu, the footer's links, the contact
 * details, the FAQ block's "More FAQs". A transform on a hairline, so nothing
 * reflows. Links underlined at rest (in running text) keep their underline.
 */
.nav-menu a::after {
	left: 0;
	width: 100%;
	transform: scaleX(0);
	transform-origin: left center;
	transition: transform var(--dur-fast) var(--ease);
}

.nav-menu a:hover::after,
.nav-menu a:focus-visible::after,
.nav-menu .current-menu-item > a::after {
	transform: scaleX(1);
}

.site-footer__menu a,
.site-footer__legal-menu a,
.contact__details a,
.faq-home__more-text {
	position: relative;
}

.site-footer__menu a::after,
.site-footer__legal-menu a::after,
.contact__details a::after,
.faq-home__more-text::after {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	bottom: -0.2em;
	height: var(--hairline);
	background-color: var(--pe-accent);
	transform: scaleX(0);
	transform-origin: left center;
	transition: transform var(--dur-fast) var(--ease);
	pointer-events: none;
}

.site-footer__menu a:hover::after,
.site-footer__menu a:focus-visible::after,
.site-footer__legal-menu a:hover::after,
.site-footer__legal-menu a:focus-visible::after,
.contact__details a:hover::after,
.contact__details a:focus-visible::after,
.faq-home__more:hover .faq-home__more-text::after,
.faq-home__more:focus-visible .faq-home__more-text::after {
	transform: scaleX(1);
}

/* The drawn line replaces the contact details' switched-on underline. */
.contact__details a:hover {
	text-decoration: none;
}

/*
 * Outlined buttons fill by fading a Bronze layer in behind the label, rather
 * than switching the background colour: the same fill and label colour as
 * before, arriving over the shared duration. Arrows already shift right on
 * hover (components.css); they now share the duration and curve.
 */
.btn--outline,
.btn--outline-ink {
	position: relative;
	isolation: isolate;
}

.btn--outline::before,
.btn--outline-ink::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	border-radius: inherit;
	background-color: var(--pe-accent);
	opacity: 0;
	transition: opacity var(--dur-fast) var(--ease);
	pointer-events: none;
}

.btn--outline:hover,
.btn--outline-ink:hover {
	background-color: transparent;
}

.btn--outline:hover::before,
.btn--outline-ink:hover::before {
	opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
	.btn:hover .btn__arrow,
	.faq-home__more:hover .faq-home__more-arrow,
	.faq-home__more:focus-visible .faq-home__more-arrow {
		transform: none;
	}
}


/* 09 Property card hover =================================================== */

/* The card's lift (properties.css) on the shared duration, and the photograph
   leaning in a little inside its frame, which already clips it. */
.property-card {
	transition:
		transform var(--dur-fast) var(--ease),
		box-shadow var(--dur-fast) var(--ease);
}

@media (prefers-reduced-motion: no-preference) {
	.property-card__image {
		transition: transform var(--dur-fast) var(--ease);
	}

	.property-card:hover .property-card__image,
	.property-card:has(.property-card__link:focus-visible) .property-card__image {
		transform: scale(1.04);
	}
}


/* 10 Accordion ============================================================= */

/*
 * The plus is two hairlines, and opening turns the upright one a quarter
 * turn onto the crossbar (components.css): a rotation, not a swapped glyph.
 * Here it takes the shared curve and the buttons' duration.
 */
.accordion__icon::before,
.accordion__icon::after {
	transition: transform var(--dur-fast) var(--ease);
}
