/**
 * Motion system.
 *
 * No animation library. Two mechanisms, and the split is deliberate:
 *   - Above the fold (the hero): pure CSS keyframes, no JS at all.
 *   - Below the fold (reveals): IntersectionObserver in app.js adds .is-in.
 * Anything the visitor sees before they scroll must not wait on a deferred
 * script.
 *
 * Only transform and opacity are animated — anything touching layout (width,
 * top, margin) causes jank and is off-limits here.
 *
 * The reveals must survive JS never running: the .no-js fallback at the bottom
 * shows them, and inc/setup.php server-renders that class onto <html> so the
 * fallback actually engages. Never hide content behind a state JS is required
 * to remove without checking the class is really there.
 *
 * The easing curve is the reference demo's signature (DESIGN-REFERENCE §3) —
 * one curve used everywhere is most of why its motion reads as cohesive rather
 * than random. That part is free; it's the GSAP/Lenis budget we're not paying.
 */

/* ---------------------------------------------------------------
   Reveal — the general-purpose scroll-in
   --------------------------------------------------------------- */

[data-reveal] {
	opacity: 0;
	transform: translateY(14px);
	transition:
		opacity 0.5s ease-out,
		transform 0.6s var(--wp--custom--ease--out, cubic-bezier(0.22, 1, 0.36, 1));
	will-change: opacity, transform;
}

[data-reveal].is-in {
	opacity: 1;
	transform: none;
}

/* Stagger children. --i is set per child by app.js. */
[data-stagger] > * {
	opacity: 0;
	transform: translateY(14px);
	transition:
		opacity 0.5s ease-out,
		transform 0.6s var(--wp--custom--ease--out, cubic-bezier(0.22, 1, 0.36, 1));
	transition-delay: calc(var(--i, 0) * 80ms);
	will-change: opacity, transform;
}

[data-stagger].is-in > * {
	opacity: 1;
	transform: none;
}

/* ---------------------------------------------------------------
   Hero — raising the roof
   --------------------------------------------------------------- */

/*
 * The hero has no photograph to carry it (PLAN §5b), so the motion is doing
 * work the imagery would otherwise do — this is the moment the page has to
 * earn attention. The roof rises and settles onto its eave line, then the name
 * and rule follow. It reads as a roof going on, which is the business.
 *
 * CSS keyframes, NOT a JS-added class — this is the whole point.
 *
 * The first cut used a .is-ready class added from app.js on a double rAF. It
 * looked fine and was quietly broken: rAF does not fire in a background tab,
 * so a hero opened in one stayed at opacity:0 until the tab was focused, and
 * even in the foreground the site's most important content was gated behind a
 * deferred script. Prism 7's ANIMATION-GUIDE already says CSS keyframes for
 * anything above the fold (PLAN §7) — it's right.
 *
 * animation-fill-mode: both is what makes this safe. The element holds the
 * from-state through the delay and the to-state forever after, so the hero
 * ends up visible with no JS involved at all. If this stylesheet fails to load
 * there is no opacity rule to strip — the content is simply there.
 */

@keyframes pob-rise {
	from {
		opacity: 0;
		transform: translateY(18px);
	}
	to {
		opacity: 1;
		transform: none;
	}
}

/* The roof-raise keyframe that used to lead this sequence is gone with
   .pob-hero__roof itself — see the note in theme.css. The pill is now the
   first beat. */
.pob-hero__pill {
	animation: pob-rise 0.7s var(--wp--custom--ease--out, cubic-bezier(0.22, 1, 0.36, 1)) 0.3s both;
}

.pob-hero__title {
	animation: pob-rise 0.7s var(--wp--custom--ease--out, cubic-bezier(0.22, 1, 0.36, 1)) 0.38s both;
}

.pob-hero__rule {
	animation: pob-rise 0.7s var(--wp--custom--ease--out, cubic-bezier(0.22, 1, 0.36, 1)) 0.48s both;
}

.pob-hero__lead {
	animation: pob-rise 0.7s var(--wp--custom--ease--out, cubic-bezier(0.22, 1, 0.36, 1)) 0.56s both;
}

.pob-hero__meta {
	animation: pob-rise 0.7s var(--wp--custom--ease--out, cubic-bezier(0.22, 1, 0.36, 1)) 0.64s both;
}

.pob-hero__actions {
	animation: pob-rise 0.7s var(--wp--custom--ease--out, cubic-bezier(0.22, 1, 0.36, 1)) 0.72s both;
}

/* The page head is the same shell but repeated on every page — the full
   staggered sequence would get tiresome by the third click. Shorter, flatter. */
.pob-pagehead .pob-hero__title,
.pob-pagehead .pob-hero__meta {
	animation-delay: 0.08s;
	animation-duration: 0.5s;
}

/*
 * ⚠ HOME HERO OPT-OUT — added 2026-07-17 with the GSAP rebuild.
 *
 * .pob-hero--home's entrance is now owned ENTIRELY by
 * assets/js/motion-gsap.js's timeline (line-mask reveal, card, aside — see
 * that file). Every OTHER hero variant (.pob-hero--about, --roofing,
 * --service, etc.) keeps the CSS-keyframe entrance above completely
 * untouched — this rule is scoped to --home only.
 *
 * Without this, BOTH systems would animate the same elements: the CSS
 * keyframes fire immediately on paint (finishing behind the still-opaque
 * preloader, harmlessly) and hold their end state via animation-fill-mode:
 * both; GSAP's .from() then sets an inline transform on top of that held
 * state and animates from it. That likely still LOOKS fine — inline style
 * wins over a held animation value — but "likely fine" across browsers is
 * not a foundation to build on when a clean fix costs four lines. Two motion
 * systems driving one element is also just a maintenance trap waiting to
 * happen the next time either one changes.
 */
.pob-hero--home .pob-hero__pill,
.pob-hero--home .pob-hero__title,
.pob-hero--home .pob-hero__rule,
.pob-hero--home .pob-hero__lead,
.pob-hero--home .pob-hero__meta,
.pob-hero--home .pob-hero__actions {
	animation: none;
}

/* ---------------------------------------------------------------
   .pob-motion — the pre-paint hidden state for the GSAP hero
   ---------------------------------------------------------------
   ⚠ THESE SELECTORS MUST MATCH motion-gsap.js's HERO TIMELINE EXACTLY.
   If a target is added to that timeline and not added here, it flashes; if
   one is listed here and not animated there, it stays invisible until the
   3s backstop. Keep the two lists in step.

   Why this is needed at all: `gsap.from()` captures the element's current
   state as the END value, so the element must be painted and visible before
   GSAP can read it — which means the jump to the start state always happens
   after first paint. Toby saw exactly that: "all the content is loaded, then
   it disappears and then animates in." Pre-hiding here removes the visible
   state there was to flash from. inc/setup.php's pre-paint script adds the
   class and documents the three ways it can never stick.

   ⚠ THE HOLD ONLY HIDES. IT MUST NEVER SET A transform. THIS BROKE ONCE.
   These rules originally mirrored the timeline's start values as transforms
   too, on the reasoning that the first painted frame should look exactly like
   frame zero. That is wrong, because GSAP does not REPLACE an existing CSS
   transform — it parses it and composes with it:

     - For the px targets it parsed `translateY(-40px)` into its own `y`
       channel and then set y:-40, which happened to be the same channel and
       the same number, so it looked fine.
     - For the line-inners it parsed `translateY(115%)` into `y` (91.407px)
       and then set `yPercent: 115`, which is a SEPARATE channel. It wrote
       BOTH: `translate(0%, 115%) translate(0px, 91.407px)` — 230% of travel
       instead of 115%. The H1 sat 182px below an 83px mask, and when the
       tween finished at yPercent:0 the parsed 91.407px REMAINED, so the
       heading never appeared at all, at any scroll position. Measured on
       staging; this is what Toby saw as "the main hero H1 heading isn't
       appearing."

   So: opacity only. GSAP sets the start positions itself at build time via
   immediateRender, which is both correct and the single source of those
   numbers — there is nothing here left to keep in sync with the timeline. */
.pob-motion .pob-header,
.pob-motion .pob-hero--home .pob-hero__pill,
.pob-motion .pob-hero--home .pob-hero__title .pob-line-inner,
.pob-motion .pob-hero--home .pob-hero__card,
.pob-motion .pob-hero--home .pob-hero__aside {
	opacity: 0;
}

/*
 * Reduced motion: motion-gsap.js bails before building the timeline, so
 * nothing would ever animate these back. Un-hide them here as well as in JS —
 * this rule lands before the script does, so there is no hidden frame at all
 * rather than a brief one.
 */
@media (prefers-reduced-motion: reduce) {
	.pob-motion .pob-header,
	.pob-motion .pob-hero--home .pob-hero__pill,
	.pob-motion .pob-hero--home .pob-hero__title .pob-line-inner,
	.pob-motion .pob-hero--home .pob-hero__card,
	.pob-motion .pob-hero--home .pob-hero__aside {
		opacity: 1;
		transform: none;
	}
}

/* ---------------------------------------------------------------
   Service icons — the hero's gesture, at 48px
   --------------------------------------------------------------- */

/*
 * The same idea as the hero roof: things being INSTALLED. Courses land on their
 * eave line, the gutter drops onto the fascia, the membrane rolls out, the box
 * profile rises. One gesture at two scales rather than two unrelated animations
 * — which is most of why the hero and the service row read as the same site.
 *
 * NO NEW JAVASCRIPT. These hang off the [data-stagger] -> .is-in class that
 * app.js already sets on the services grid. The observer, the threshold-0 fix,
 * the reduced-motion branch and the no-JS fallback are all inherited as they
 * stand. Adding an icon animation cost zero bytes of JS.
 *
 * transform + opacity only. transform-box: fill-box is what makes transforms on
 * SVG children behave — without it the origin is the SVG's user-space origin,
 * not the shape's own box, and every one of these would swing from the corner
 * of the viewBox instead of settling on itself.
 *
 * Deliberately NOT a stroke-dashoffset line-draw, which is the reflex for icons
 * like these. dashoffset is a paint-level property, so it repaints the element
 * every frame rather than riding the compositor, and this file's rule is
 * transform/opacity only. It would also be the wrong idea: a line drawing itself
 * says "being drawn". These say "being fitted", which is the business.
 */

.pob-ico__p {
	transform-box: fill-box;
	transform-origin: 50% 100%;
}

/*
 * Default part: rises and settles, offset by --n so multi-part icons build in
 * the order the real thing is built (deck first, then felts; bottom course
 * first, then up the pitch). --n is set per path in inc/icons.php, NOT by JS —
 * app.js's --i only indexes a stagger group's direct children, and these are
 * paths inside one child.
 */
[data-stagger] .pob-ico__p {
	opacity: 0;
	transform: translateY(22%);
	transition:
		opacity 0.4s ease-out,
		transform 0.55s var(--wp--custom--ease--pop, cubic-bezier(0.34, 1.56, 0.64, 1));
	transition-delay: calc((var(--i, 0) * 80ms) + (var(--n, 0) * 70ms) + 120ms);
}

[data-stagger].is-in .pob-ico__p {
	opacity: 1;
	transform: none;
}

/* Lead sheet + single-ply membrane: they unroll, so they scale from the roll
   end rather than rising. */
[data-stagger] .pob-ico__p--unroll {
	transform-origin: 0% 50%;
	transform: scaleX(0.15);
}

/* The gutter is fixed on last and from above. */
[data-stagger] .pob-ico__p--drop {
	transform-origin: 50% 0%;
	transform: translateY(-40%);
	transition-delay: calc((var(--i, 0) * 80ms) + 340ms);
}

/* The deck is the thing everything else lands on, so it doesn't move — it just
   arrives, and the felts stack onto it. */
[data-stagger] .pob-ico__p--deck {
	transform: none;
}

/* Box profile ribs rise into the sheet. */
[data-stagger] .pob-ico__p--rise {
	transform: translateY(30%);
}

/* ---------------------------------------------------------------
   Header — transparent over the hero, solid once past it
   --------------------------------------------------------------- */

.pob-header {
	position: fixed;
	inset-block-start: 0;
	inset-inline: 0;
	z-index: 100;
	transition:
		background-color 0.3s ease,
		box-shadow 0.3s ease;
}

/*
 * GLASS, NOT SOLID. Toby, 2026-07-18: "give it some transparency... give it
 * like a glass effect so it's like blurred and transparent, because that
 * looks really nice."
 *
 * ⚠ THE OPAQUE FALLBACK IS LOAD-BEARING, NOT BELT-AND-BRACES.
 * backdrop-filter is what makes the translucent background readable — it
 * blurs whatever scrolls under it into a flat wash. Without it, a 72%-alpha
 * background over a photograph or a run of body copy is just tinted text on
 * text. So the base rule below is the OPAQUE surface colour, and the
 * translucency is only applied inside `@supports (backdrop-filter)`. A
 * browser that cannot blur gets a solid header, which is legible; it never
 * gets the translucency without the blur that makes it work.
 */
.pob-header {
	background: var(--wp--preset--color--surface);
	box-shadow: 0 1px 0 var(--wp--preset--color--line);
}

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
	.pob-header.is-stuck {
		background: color-mix(in srgb, var(--wp--preset--color--surface) 72%, transparent);
		-webkit-backdrop-filter: blur(18px) saturate(150%);
		backdrop-filter: blur(18px) saturate(150%);
		box-shadow: 0 1px 0 var(--wp--custom--line-strong);
	}
}

/* Over a hero, before any scroll: fully transparent, no blur — there is a
   photograph immediately behind it and nothing to separate it from yet. */
.has-hero .pob-header:not(.is-stuck) {
	background: transparent;
	box-shadow: none;
	-webkit-backdrop-filter: none;
	backdrop-filter: none;
}

/*
 * Nav colour over the hero.
 *
 * ⚠ DARK REBUILD, 2026-07-17 — there used to be TWO rules here: this one
 * (ink, for a plain hero) and a `.has-hero-media` one that forced `surface`
 * (white, pre-flip) for a photo hero, on the reasoning that a white hero
 * needed dark nav text but a dark photo scrim needed light nav text. `surface`
 * is now near-black, so the media rule had inverted into forcing the nav
 * dark-on-dark specifically on photo-hero pages — Toby: "the logo is still
 * dark on a dark background", same root cause as the header logo and the
 * mega bar (theme.css). Every hero is dark now regardless of `.has-media`
 * (see `.pob-hero`/`.pob-pagehead` in theme.css), so this one rule, using
 * ink, already covers both cases — the media-specific override is gone.
 *
 * :where(...:not(.is-menu-open)) — this is the fix for the invisible mobile
 * menu, not tidying. See the long note in theme.css.
 *
 * This rule means "the header is floating over the hero, so colour the nav
 * for what's behind it". That reasoning does not survive the overlay menu
 * opening: the overlay is position:fixed, full-screen, and painted on core's
 * own WHITE panel — there is no hero behind it. But the anchors are still
 * descendants of .pob-header__links, so the selector kept matching and kept
 * making them white. White on white. The menu was invisible on every
 * photo-hero page at scroll-top.
 *
 * :where() contributes ZERO specificity, so the rule stays exactly (0,4,1)
 * and nothing else about the cascade moves — it just stops claiming the modal.
 */
.has-hero .pob-header:not(.is-stuck) .pob-header__links :where(.wp-block-navigation__responsive-container:not(.is-menu-open)) a {
	color: var(--wp--preset--color--ink);
}

/* ---------------------------------------------------------------
   Reduced motion — hard stop
   --------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
	[data-reveal],
	[data-stagger] > * {
		opacity: 1;
		transform: none;
		transition: none;
		transition-delay: 0s;
	}

	/* The icon parts are NOT direct children of a [data-stagger] group — they
	   are paths inside one — so `[data-stagger] > *` above does not reach them.
	   Without this rule every icon would sit permanently at opacity:0 for a
	   reduced-motion user: six blank boxes, on the one setting that is supposed
	   to make the page MORE usable. Same shape of mistake as hiding content
	   behind a JS-added class. */
	[data-stagger] .pob-ico__p {
		opacity: 1;
		transform: none;
		transition: none;
		transition-delay: 0s;
	}

	/* The hero is keyframe-driven, so it needs animation: none — killing
	   transitions would do nothing here. Removing the animation leaves each
	   element at its natural state, which is already visible. */
	.pob-hero__pill,
	.pob-hero__title,
	.pob-hero__rule,
	.pob-hero__lead,
	.pob-hero__meta,
	.pob-hero__actions {
		animation: none;
	}

	.pob-header {
		transition: none;
	}
}

/* ---------------------------------------------------------------
   No JS — content must still be visible
   --------------------------------------------------------------- */

/* The hero is pure CSS keyframes with fill-mode both, so it never depended on
   JS in the first place. The scroll reveals do. */
.no-js [data-reveal],
.no-js [data-stagger] > *,
.no-js [data-stagger] .pob-ico__p {
	opacity: 1;
	transform: none;
	transition: none;
}

/*
 * Without JS the header must be solid.
 *
 * `is-stuck` is added by app.js, so with no JS the header can never learn it
 * has left the hero and :not(.is-stuck) stays true forever — a permanently
 * transparent header. Worse on the pages with a photo hero: `has-hero-media`
 * is also JS-added, so the nav never flips to white and sits ink-on-dark-
 * photograph. Caught by simulating a JS failure on the live page rather than
 * by reading the code — the reveals passed, the header did not.
 *
 * Solid is the correct fallback in both directions: it is legible over white
 * content AND over a photo, which is the whole reason the scroll state exists.
 * The float-over-hero behaviour is enhancement, and this is what it degrades to.
 *
 * :not(.is-stuck) is carried here only to match the specificity of the
 * `.has-hero .pob-header:not(.is-stuck)` rule above; source order then settles
 * it. Without it that rule wins and the header stays transparent.
 */
.no-js .pob-header:not(.is-stuck) {
	background: var(--wp--preset--color--surface);
	box-shadow: 0 1px 0 var(--wp--preset--color--line);
}

.no-js .pob-header:not(.is-stuck) .pob-header__links a {
	color: var(--wp--preset--color--ink);
}
