/* ============================================================
   greyd-child/hero-split — the split-panel hero.

   An editorial statement in open space, above two complementary
   photographs: a large one filling the left half, a smaller one held
   inside a coloured panel on the right, with the links sitting in the
   panel's negative space directly beneath it.

   TWO REFERENCES, ONE FOR EACH HALF OF THE BLOCK:
     the statement   Wolff Olins' `p.page-intro` (wolffolins.com/work,
                     measured 2026-07-28) — see THE STATEMENT below.
     everything else Fendi's CoreMedia widgets — see below and the
                     ENTRANCE note at the foot of this file.

   ★ THE LINKS NO LONGER TRAVEL (2026-07-28, owner's call). ★
   They sit under the contained photograph and arrive with it, on load.
   Nothing about this block responds to scroll position any more, and
   that deleted the largest mechanism in it:

     – the two caption layers, painted in complementary `clip-path`s so a
       colour seam could land exactly on the panel's bottom edge;
     – the duplicate, aria-hidden copy of the links each layer needed
       (a clipped region is not hit-testable, so each layer had to carry
       the half it painted);
     – `position: sticky` and the `.__inner → .__pinned → .__rail`
       percentage-height chain that fed it;
     – --tjw-hs-band, and with it every constraint the band imposed: the
       links may now wrap, because no height downstream depends on them;
     – --tjw-hs-rest, the tall-viewport guard that kept the caption's
       opening line below the photograph's bottom edge;
     – --tjw-hs-statement-h and the whole of view.js, whose only job was
       to measure the statement so --tjw-hs-rest could subtract it;
     – --tjw-hs-half, --tjw-hs-panel-h, --tjw-hs-fig-h and the paired
       ratio NUMBERS that existed so those heights could be expressed in
       calc(). Widths are plain percentages of the panel now, which also
       retires the 100vw-includes-the-scrollbar caveat that came with
       --tjw-hs-half.

   An earlier change had already lifted the headline out of the caption
   (see THE STATEMENT); between them, what is left is a composition with
   no moving parts other than its entrance.

   Reverse-engineered from Fendi's CoreMedia cm-half-carousel-evolution
   (measured 2026-07-28 at 1536px, root 14px: two square halves of
   611.2px; panel #F6F6F6, padding 32/0/35, flex column, justify-center;
   contained picture 228.75x305 — 3:4, exactly 50% of the panel height —
   with margin 24px auto; link beneath at 12/18 uppercase). With the
   travel gone this is now the reference's own arrangement exactly: a
   picture centred in a panel, its link 24px below it.

   WHY THIS BLOCK EXISTS ALONGSIDE hero-over-under.
     hero-over-under needs ONE large, dark, high-quality photograph: the
     caption sits over it and the colour flip only reads on a dark plate.
     There isn't one. This block spends the same footprint on two SMALLER
     photographs that complement each other — neither has to carry a
     full-bleed hero on its own.

   THE FOOTPRINT — same as the hero it replaces.
     Not Fendi's contained 10-of-12 column: the owner's call is that this
     occupies the space the hero occupied, i.e. the full page minus the
     gutter. hero-over-under's frame spends gutter/2 on each side, so this
     one does too and the two are interchangeable in place.

     The remaining gutter/2 goes to the STATEMENT, not to the caption:
     the statement sits on the page and must land on the page's grid
     line, while the caption lives inside the right-hand panel, whose own
     left edge is the page's midpoint — it can never align with the page
     grid, so it aligns to the small photograph instead (see
     --tjw-hs-pad-x).
   ============================================================ */

/* ---- the hero opens the page, so it takes the header's edge -------
   Same rule, same reason, as hero-over-under: <main> carries a global
   --wp--style--block-gap below the header which reads as dead space above
   a hero. Removed only when this block genuinely opens the page, so any
   other page keeps its gap. Both blocks' rules can coexist — each only
   matches when its own block is first.

   TWO selectors, because there are two shapes and hero-over-under only
   carries the first. The site's convention is to wrap a hero in
   `<div class="wp-block-group alignfull tjw">` (that group is what
   supplies the .tjw token scope), which is the descendant case. A block
   dropped straight into the post content with no wrapper is the direct
   case — it is the shape you get from the inserter before anyone adds
   the group, and without this second selector the hero silently keeps a
   32px gap above it and every derived position moves down by that much.
   Measured: the caption opened 32px lower and was already pinned at
   scroll 0. */
main:has(> .entry-content > *:first-child .tjw-hs),
main:has(> .entry-content > .tjw-hs:first-child) {
	margin-block-start: 0;
}

.tjw-hs {
	/* ---- the page grid ----
	   The --wp--custom--* fallback is the one that resolves outside the
	   .tjw scope — theme.json emits it globally, and in the editor. */
	--tjw-hs-gutter: var(--gutter, var(--wp--custom--tjw--gutter, clamp(18px, 5vw, 64px)));
	--tjw-hs-frame: calc(var(--tjw-hs-gutter) / 2);

	--tjw-hs-ratio: 1 / 1;        /* left photograph — render.php overrides */
	--tjw-hs-fig-ratio: 3 / 4;    /* contained photograph — ditto */

	/* ⚠ TWO PROPERTIES, AND THEY MUST STAY TWO. render.php writes the
	   authored fraction into an inline `style` attribute, and an inline
	   declaration beats every rule in this stylesheet including one inside
	   a media query — so if the phone branch below and the author wrote
	   the SAME property, the phone branch could never win and the
	   photograph would render at the desktop fraction of a full-width
	   panel, i.e. half the size intended. Measured at 390px before the
	   split: 139px wide, which forced the headline onto three lines.

	   So: `-a` is the authored value and nothing but render.php touches
	   it; the property everything else reads is derived from it here,
	   where a media query can reach it. */
	--tjw-hs-fig-share-a: 0.375;  /* its width as a fraction of the panel's */
	--tjw-hs-fig-share: var(--tjw-hs-fig-share-a);

	/* THE CAPTION TAKES THE PHOTOGRAPH'S OWN MEASURE. The figure is
	   centred in the panel, so its edges sit at (panel − figure) / 2 —
	   which as a percentage of the panel is simply 50% − share×50%. Used
	   as padding on a full-width box inside the panel, so it puts the
	   links exactly on the photograph's edges: the two share a vertical
	   line, which is the only alignment available inside a panel whose
	   left edge is the page's midpoint. Both alignments take it; they
	   differ only in text-align.

	   Percentages, not the old calc chain off a --tjw-hs-half derived
	   from 100vw. The panel IS the box these resolve against, so the
	   scrollbar can no longer make them a few pixels optimistic and there
	   is one fewer token to keep in step with the layout. */
	--tjw-hs-pad-x: calc(50% - calc(var(--tjw-hs-fig-share) * 50%));

	--tjw-hs-pad-block: var(--sp-8, 32px);   /* panel's minimum breathing room */
	--tjw-hs-cta-top: 24px;      /* photograph → links, the reference's own */
	--tjw-hs-cta-lh: 18px;       /* CTA line box (14px link)             */
	--tjw-hs-cta-gap: 24px;      /* between two links, when there are two */

	--tjw-hs-items: center;                  /* cross-axis alignment of the caption */
	--tjw-hs-text: center;

	/* ---- type ------------------------------------------------------
	   The links are the house UI ramp, identical to hero-over-under.

	   ★ THE STATEMENT IS A STEP ABOVE ANYTHING THE SITE HAS. ★
	   The house H1 is --fd-1, clamp(34px, 4vw, 48px). This is
	   deliberately larger: the owner's brief is a headline set in open
	   space, referencing Wolff Olins' `p.page-intro` (wolffolins.com/work,
	   measured 2026-07-28) — a 64px serif on three lines with 96px of
	   nothing above it and 160px below.

	   Their ramp is four stepped breakpoints, 35 → 45 → 55 → 64px. Ported
	   as a clamp rather than as their steps, because that is how this site
	   expresses a ramp and because the intermediate widths matter more
	   here: TJW's measure is narrower, so a size that steps late spends
	   too long at the small end. Tuned to pass through their endpoints —
	   35px at 400px of viewport, 64px from 1100px up.

	   ⚠ THE `+` LIVES IN A CUSTOM PROPERTY, WHICH IS THE ONLY PLACE IT IS
	   SAFE. WP Rocket's minifier strips the space BEFORE a `+`, and
	   calc() requires whitespace on both sides of `+` and `-`, so
	   `a +b` is valid and `a+b` silently voids the whole declaration. A
	   CUSTOM property's value is preserved as an untouched token stream,
	   whitespace and all, which is why this one survives. Written straight
	   into `font-size` it is dropped and the statement silently falls back
	   to the 35px floor — in production only, and `?nowprocket` will never
	   show you the bug. THE RULE FOR THIS FILE: sums go in custom
	   properties; real properties stay subtraction-only.

	   ★ WEIGHT: THE SITE'S OWN 300. ★ Tried at 400 and reverted the same
	   day (owner, 2026-07-28) — at this size 300 is light rather than
	   thin, and the hero is not a reason to leave the agreed serif weight.
	   So it reads --serif-w and inherits any future change to it, rather
	   than pinning a literal here. (/bespoke/ raised its H2 to 400; this
	   is a separate decision and did not follow.)

	   The reference's other three decisions, and what each became here:
	     line-height  1 → 1.05. Theirs is a 400-weight face with shallow
	                  descenders; Newsreader 300 at 64px collides on
	                  consecutive lines at a flat 1.
	     tracking     −0.0275em → −0.02em. Heavy negative tracking closes
	                  the counters of a light serif.
	     measure      32ch, kept exactly. On the ELEMENT, not a wrapper —
	                  `ch` resolves against the font it is declared on, so
	                  a wrapper would measure it in the body face and hand
	                  the statement a different line length than intended.
	*/
	--tjw-hs-cta-size: var(--fs-ui, 14px);
	--tjw-hs-cta-font: var(--sans, var(--wp--preset--font-family--body, sans-serif));

	--tjw-hs-statement-font: var(--serif, var(--wp--preset--font-family--heading, Georgia, serif));
	--tjw-hs-statement-weight: var(--serif-w, var(--wp--custom--tjw--serif-weight, 300));
	--tjw-hs-statement-size: clamp(35px, calc(1.15rem + 4.14vw), 64px);
	--tjw-hs-statement-lh: 1.05;
	--tjw-hs-statement-track: -0.02em;
	--tjw-hs-statement-measure: 32ch;

	/* ---- the space around it ---------------------------------------
	   ★ ONE VALUE, THE SAME ABOVE AND BELOW: 100px at desktop (owner's
	   call, 2026-07-28). ★

	   The reference's own is 96px above and 160px below — a 1:1.7 ratio,
	   weighted downwards so the line reads as belonging to what follows
	   rather than floating between two equal gaps — and that was ported
	   here as --band at 1:1.5, i.e. 120/180. Both the asymmetry and the
	   size are overridden: the owner's call is less space, evenly. It is
	   ONE token now rather than two, so "symmetric" is a property of the
	   code and not something that has to keep being true by coincidence.

	   Deliberately NOT --band any more. --band tops out at 120px and this
	   has to top out at 100, so reading the site token and scaling it
	   would be a fiction — the number is authored. It still scales the
	   way every other measure on this site does: 7vw is the same slope
	   --band uses (8vw), so it holds the desktop value from ~1430px down
	   and reaches its floor at 800px, below which it stays 56px. */
	--tjw-hs-space: clamp(56px, 7vw, 100px);

	/* ---- THE LAG — how far the statement falls behind the page -------
	   ★ DERIVED FROM THE SPACE BELOW IT, AND THAT IS THE WHOLE SAFETY
	   ARGUMENT. ★ Full note at the foot of this file. --tjw-hs-space IS
	   the gap between the statement and the photographs, so a lag of half
	   it can close at most half that gap: the two can never meet. The
	   guarantee is structural rather than a number that happened to work
	   at the width it was checked on, and it survives any future change
	   to the spacing automatically. 50px desktop, 28px phone. */
	--tjw-hs-lag: calc(var(--tjw-hs-space) * 0.5);
	--tjw-hs-lag-range: 200px;

	/* author-chosen; render.php overrides these inline */
	--tjw-hs-statement-color: var(--ink, #161616);
	--tjw-hs-panel-bg: var(--bg-2, #eae9e6);
	/* ONE link colour now. There were two — one for the links while they
	   were on the panel and one for after they had travelled off it — and
	   with the travel gone they only ever sit on the panel. */
	--tjw-hs-link: var(--ink, #161616);

	display: flex;
	flex-direction: column;
	position: relative;
	/* L/R only. Top stays 0 for two reasons: <main>'s block gap above a
	   hero reads as the hero sitting too low, so it is removed (see the
	   rule above this one); and the space above the statement is the
	   STATEMENT's, not the block's — one owner, one place to change it,
	   and it is visible as padding on the element it belongs to rather
	   than as a gap nobody can attribute. Bottom stays 0 because the
	   block ends where the panel does; whatever follows spaces itself. */
	padding: 0 var(--tjw-hs-frame);
}

.tjw-hs * {
	box-sizing: border-box;
}

/* ---- THE STATEMENT ---------------------------------------------
   Prose in open space. It is simply given room, which is the entire
   idea.

   ★ CENTRED ON THE PAGE (owner's call, 2026-07-28). ★ It used to be
   left-aligned on the page grid, with the block's frame spending
   gutter/2 and this element spending the other half so the first
   character landed on the same vertical line as every heading below it,
   and with the 32ch measure holding the right-hand edge so the negative
   space sat to one side. Centring replaces both jobs: the measure now
   decides the line length and the two equal margins decide where it
   sits.

   THE FULL-GUTTER PADDING STAYS ANYWAY, and it is not decorative. Once
   the viewport is narrow enough that the measure exceeds the page,
   centring buys nothing and the text would otherwise run to the block's
   own frame — half a gutter — and sit inboard of every heading below it.
   This keeps it on the page's own inset at exactly the point where the
   centring stops being visible — the rule this site is strict about is
   that nothing sits off the page's left edge. */
.tjw-hs__statement {
	padding-block: var(--tjw-hs-space);
	padding-inline: var(--tjw-hs-frame);
}

/* Two classes deep on purpose. The theme sets headings through
   `:root :where(h1)`, whose specificity is (0,1,0) — the same as a single
   class — so a one-class rule here would be decided by stylesheet order,
   which is not something a block should depend on. */
.tjw-hs .tjw-hs__statement-text {
	/* `auto` centres the MEASURE in the page; `text-align` centres the
	   lines within it. Both are needed and they do different jobs — with
	   the margins alone a two-line statement would be a centred block of
	   ragged-right text, which is not what centred means here. */
	margin: 0 auto;
	max-width: var(--tjw-hs-statement-measure);
	text-align: center;
	font-family: var(--tjw-hs-statement-font);
	font-size: var(--tjw-hs-statement-size);
	line-height: var(--tjw-hs-statement-lh);
	font-weight: var(--tjw-hs-statement-weight);
	letter-spacing: var(--tjw-hs-statement-track);
	color: var(--tjw-hs-statement-color);
	/* The reference's own. Chrome and Firefox even the ragged edge across
	   the lines; everywhere else it is ignored and the measure alone
	   decides, which is a perfectly good result rather than a fallback. */
	text-wrap: balance;
}

/* Start-aligned caption. The box is already the photograph's (see
   --tjw-hs-pad-x), so this only decides where the text sits inside it —
   flush to the photograph's left edge rather than centred under it. */
.tjw-hs--start {
	--tjw-hs-items: flex-start;
	--tjw-hs-text: start;
}

/* ---- the two halves -------------------------------------------- */
/* No gap: the halves meet, as they do on the reference — the panel's
   colour reads as the photograph's continuation rather than as a card. */
.tjw-hs__item {
	position: relative;
	z-index: 1;
	display: grid;
	grid-template-columns: 1fr 1fr;
}

/* explicit width alongside aspect-ratio: with width:auto a browser is
   free to narrow the BOX to satisfy the ratio rather than grow it. */
.tjw-hs__media {
	position: relative;
	z-index: 2;
	width: 100%;
	aspect-ratio: var(--tjw-hs-ratio);
	overflow: hidden;
	background: var(--bg-2, #eae9e6);
}

.tjw-hs__media img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* The panel takes its height from the photograph beside it (grid rows
   stretch by default), which is what makes the two halves square off.
   Its contents — the contained photograph and the links beneath it — are
   centred in whatever height that leaves, exactly as the reference
   centres its own. */
.tjw-hs__panel {
	position: relative;
	z-index: 2;
	min-width: 0;
	display: flex;
	flex-direction: column;
	justify-content: center;
	padding-block: var(--tjw-hs-pad-block);
	background: var(--tjw-hs-panel-bg);
}

.tjw-hs__figure {
	position: relative;
	z-index: 1;
	width: calc(var(--tjw-hs-fig-share) * 100%);
	aspect-ratio: var(--tjw-hs-fig-ratio);
	margin-inline: auto;
	overflow: hidden;
	background: var(--bg, #f9f8f6);
}

.tjw-hs__figure img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* ---- the links -------------------------------------------------
   In the panel's flow, directly beneath the photograph, boxed to its
   measure. Nothing is positioned, clipped or pinned. */
.tjw-hs__caption {
	width: 100%;
	padding-inline: var(--tjw-hs-pad-x);
	padding-block-start: var(--tjw-hs-cta-top);
	color: var(--tjw-hs-link);
	text-align: var(--tjw-hs-text);
}

/* THEY MAY WRAP NOW, and that is a real dividend of the travel going.
   The row used to be `nowrap` because its height was the band the colour
   seam was derived from, so a second line slid the seam off the block's
   edge and two long labels on a narrow phone would do it silently. No
   height downstream depends on this row any more, so long labels simply
   take a second line. */
.tjw-hs__cta-row {
	display: flex;
	flex-wrap: wrap;
	gap: 8px var(--tjw-hs-cta-gap);
	justify-content: var(--tjw-hs-items);
}

/* The house link, restated rather than borrowed — a block that carries
   its own stylesheet should not depend on an ancestor class the editor
   can remove. Underline present at rest, retracting left-to-right on
   hover; the ::after is used rather than text-decoration precisely
   because a transform can be transitioned and a text-decoration-color
   swap reads as a fade rather than a wipe. */
.tjw-hs__cta {
	position: relative;
	display: inline-block;
	font-family: var(--tjw-hs-cta-font);
	font-size: var(--tjw-hs-cta-size);
	line-height: var(--tjw-hs-cta-lh);
	font-weight: 500;
	letter-spacing: var(--tr-snug, -0.01em);
	color: inherit;
	text-decoration: none;
}

.tjw-hs__cta::after {
	content: "";
	position: absolute;
	left: 0;
	bottom: -2px;
	height: 1px;
	width: 100%;
	background: currentColor;
	transform: scaleX(1);
	transform-origin: left;
	transition: transform 350ms var(--ease-big, cubic-bezier(0.16, 1, 0.3, 1));
}

/* WHERE THE HOVER LIVES DEPENDS ON WHAT THE LINK IS.

   Single link: on the frame, not the words. A transparent overlay spans
   the whole block and sits above them, so the words themselves never
   receive a hover.

   Two links: on each link, because the whole point of there being two is
   that they lead somewhere different, and an underline that retracts on
   both says the opposite. `a` and `span` are mutually exclusive here, so
   the two rules can never both apply to the same element. */
.tjw-hs:hover span.tjw-hs__cta::after {
	transform: scaleX(0);
	transform-origin: right;
}

a.tjw-hs__cta:hover::after,
a.tjw-hs__cta:focus-visible::after {
	transform: scaleX(0);
	transform-origin: right;
}

a.tjw-hs__cta:focus-visible {
	outline: 2px solid currentColor;
	outline-offset: 4px;
}

/* ---- the single interactive element -----------------------------
   With one call to action the block IS that link: a transparent overlay
   spans both halves, above everything, so the hero is clickable
   anywhere and there is a single tab stop. A second call to action makes
   that impossible — the photographs can no longer stand for one
   destination — so render.php omits this and the words become the links
   instead. */
.tjw-hs__link {
	position: absolute;
	inset: 0;
	z-index: 5;
	display: block;
}

.tjw-hs__link:focus-visible {
	outline: 2px solid var(--tjw-hs-link);
	outline-offset: 2px;
}

/* ---- phone ------------------------------------------------------
   The halves stack, and only two things follow.

   1. --tjw-hs-fig-share is authored against a HALF-width panel. Left
      alone on a full-width one it draws the same fraction of twice the
      width, i.e. a photograph twice as large. Scaling it restores
      roughly the optical size the desktop composition has, which is the
      intent the author expressed — not the number they typed. Nothing
      else needs a branch: the figure's width and --tjw-hs-pad-x are both
      percentages of the panel, so they follow it.

   2. --tjw-hs-pad-x is replaced anyway, because aligning the links to
      the photograph's edge is not an alignment anyone can see on a
      phone and the row needs the room.

   The panel needs no branch at all now. Its height is its own contents
   at this width, `justify-content: center` on a box that fits its
   content is a no-op, and there is no travel to find space for — which
   is what the old --tjw-hs-travel / --tjw-hs-pad-bottom pair existed to
   buy. */
@media (max-width: 768px) {
	.tjw-hs__item {
		grid-template-columns: 1fr;
	}

	.tjw-hs {
		/* Scaled, not replaced, so the author's choice still carries: the
		   phone panel is twice the width the fraction was authored
		   against, and x1.5 lands the default 0.375 on 0.5625 — about the
		   same optical size as the desktop composition. The min() is the
		   guard, because an author who already pushed the slider to 0.7
		   would otherwise ask for 1.05 of the panel. */
		--tjw-hs-fig-share: min(0.75, calc(var(--tjw-hs-fig-share-a) * 1.5));

		--tjw-hs-pad-x: var(--tjw-hs-gutter);
		--tjw-hs-pad-block: var(--sp-10, 40px);
	}
}

/* ---- THE ENTRANCE ----------------------------------------------
   The only thing in this block that moves, and now the only thing that
   ever did.

   Fendi's own, measured off the live widget (2026-07-28) rather than
   guessed. Their block carries `data-aos="fade-up"` and is driven by
   AOS (Animate On Scroll); the values that library resolves to are:

     pre-state    transform: translateY(100px); opacity: 0
     post-state   transform: none;              opacity: 1
     transition   opacity .9s ease, transform .9s ease
     delay        per element, `data-aos-delay` — 350ms on this widget
     once         `data-aos-once="false"` — it replays on every entry

   THE THING WORTH KNOWING: there are ZERO nested [data-aos] elements
   inside any one of their widgets. Within a widget the photograph and
   the caption do not travel separately — it moves as ONE element, and
   what reads as two things arriving is simply the block arriving. That
   is why the links here have no beat of their own: they are part of the
   panel's composition and arrive with it.

   TWO BEATS HERE, AND THE REFERENCE IS STILL WHAT DECIDES IT. The
   statement is not inside the photographs' composition; it is a band of
   its own with a screen's worth of space around it, which on the
   reference site is what a separate widget is. And separate widgets DO
   arrive separately — `data-aos-delay` is per widget, and 350ms is the
   value theirs carries. So the statement rises, then the photographs and
   their links 350ms behind it, and each still moves as one undivided
   thing.

   --tjw-hs-intro-stagger is authored, and 0 collapses the two beats back
   into one for anyone who wants the whole block to arrive together.

   ⚠ THIS IS A CSS ANIMATION, NOT A SCRIPTED ONE, AND THAT IS NOT A
   STYLISTIC CHOICE. The obvious build is AOS's own shape — render the
   pre-state, then have an IntersectionObserver add a class to release it
   — and on this site that produces an INVISIBLE HERO. WP Rocket's "Delay
   JavaScript Execution" rewrites every enqueued script to
   `type="text/rocketlazyloadscript"` with `data-rocket-src`, so it does
   not execute until the visitor first interacts with the page. Verified
   in the generated production HTML: this block's own script came through
   delayed, along with 29 other scripts. A hero whose reveal depends on
   that script sits at opacity 0 and translateY(100px) until someone
   moves the mouse.

   And `?nowprocket` shows you none of it — that URL is precisely the one
   where the script is NOT delayed. Same class of trap as the minifier
   note above.

   A keyframe animation has no such dependency: it runs off the
   stylesheet, on load, delayed by nothing. It is also the reason this
   block now ships no front-end JavaScript at all.

   THE TRADE, stated plainly: a CSS animation fires on load rather than
   on entering the viewport, so an instance placed far down a page will
   have played before it is seen. That is the right trade HERE — this is
   a hero, it opens the page, and "on load" is what was asked for — but
   it is the thing to revisit if this block is ever reused mid-page.

   ⚠ THE OPACITY HALF COSTS LCP. This block is the page's opening
   element, so the large photograph is the LCP candidate — and an element
   at opacity 0 is not painted, so `fade-up` pushes Largest Contentful
   Paint out by delay + duration (900ms here, 1250ms at Fendi's 350ms
   delay) on a site whose LCP was deliberately tuned to ~1250ms total.
   The `travel` variant exists for exactly this: it keeps the 100px/900ms
   arrival and drops only the fade, so the photograph is painted at once
   and LCP is untouched. Transform never affects LCP. */
.tjw-hs {
	--tjw-hs-intro-dist: 100px;
	--tjw-hs-intro-dur: 900ms;
	--tjw-hs-intro-delay: 0ms;      /* render.php overrides */
	--tjw-hs-intro-stagger: 350ms;  /* ditto — the reference's own value */

	/* The second beat. A CUSTOM property, so the `+` survives the
	   minifier — written into `animation-delay` directly it would be
	   dropped in production and both beats would fire together. See the
	   minifier note above. */
	--tjw-hs-intro-delay-2: calc(
		var(--tjw-hs-intro-delay)
		+ var(--tjw-hs-intro-stagger)
	);
}

@keyframes tjw-hs-rise {
	from { transform: translateY(var(--tjw-hs-intro-dist)); }
	to   { transform: none; }
}

@keyframes tjw-hs-fade-rise {
	from { transform: translateY(var(--tjw-hs-intro-dist)); opacity: 0; }
	to   { transform: none; opacity: 1; }
}

/* `backwards`, NOT `both`. `backwards` holds the `from` state through
   the delay — without any fill the block would paint in place and then
   jump down to start travelling — but it does NOT retain the final
   frame, so the item goes back to its ordinary computed style once it
   has arrived and carries no transform at all afterwards. (`both` would
   retain a `transform: none` that still computes to the identity
   matrix — measured: `matrix(1, 0, 0, 1, 0, 0)`. Harmless here now that
   nothing inside is positioned against the viewport, but there is no
   reason to leave a transform on a hero.) */
.tjw-hs--anim .tjw-hs__statement {
	animation: tjw-hs-rise var(--tjw-hs-intro-dur) ease var(--tjw-hs-intro-delay) backwards;
}

.tjw-hs--anim .tjw-hs__item {
	animation: tjw-hs-rise var(--tjw-hs-intro-dur) ease var(--tjw-hs-intro-delay-2) backwards;
}

.tjw-hs--fade .tjw-hs__statement,
.tjw-hs--fade .tjw-hs__item {
	animation-name: tjw-hs-fade-rise;
}

/* ---- THE LAG ----------------------------------------------------
   The statement tracks the page slightly as the reader scrolls: it
   drifts DOWN relative to the document, so in the viewport it falls
   behind at about three-quarters of the page's speed for the first
   200px and then travels with it normally. Owner's call, 2026-07-28 —
   *"can the h1 track the page slightly as i scroll down the page but
   not cover the hero below."*

   ★ IT CANNOT REACH THE PHOTOGRAPHS, BY CONSTRUCTION. ★ The statement's
   own bottom padding IS the gap between it and them, and the lag is
   half of that same token — so at full lag exactly half the gap is
   spent and half remains. Nothing here is a measured constant that a
   later change to the spacing could invalidate. There is a second,
   independent guarantee behind it: .tjw-hs__item carries `z-index: 1`
   and the statement does not, so even a lag that somehow overshot would
   pass BEHIND the photographs rather than over them.

   ★ WHY THIS IS A SCROLL-DRIVEN CSS ANIMATION AND NOT A SCROLL HANDLER.
   Same reason the entrance is a keyframe animation — WP Rocket's Delay
   JS holds every enqueued script until the visitor first interacts, so
   a scripted parallax would simply not run for the readers most likely
   to see it, and `?nowprocket` would show you none of that. This block
   ships no front-end JavaScript and this effect does not change that.

   THE DEGRADATION IS THE POINT. `animation-timeline` is not universal;
   where it is missing the @supports block never applies and the
   statement scrolls with the page exactly as it did before. Nothing is
   revealed by this animation and no layout depends on it — it is
   decoration on top of a composition that is already correct — which is
   what makes a progressive enhancement legitimate here where it was not
   for the entrance.

   ⚠ TWO ORDERING TRAPS, both of which fail silently:
     1. The `animation` SHORTHAND resets `animation-timeline` to `auto`.
        Longhands are used throughout below so there is no shorthand to
        get the order wrong with.
     2. The entrance animates `.tjw-hs__statement`; this animates
        `.tjw-hs__statement-text` INSIDE it. That separation is
        deliberate and load-bearing: two animations on one element both
        targeting `transform` do not compose — the later one in the list
        wins outright, and the entrance would be silently overridden for
        its whole 900ms. On two elements the transforms multiply, which
        is what we actually want.

   The range is 200px, not the full height of the block: the statement
   passes behind the sticky header at roughly 95px of scroll, so a
   longer range would spend most of the lag after it is out of sight and
   the visible part of the effect would be a fraction of what was
   authored. */
@keyframes tjw-hs-lag {
	to { transform: translateY(var(--tjw-hs-lag)); }
}

@supports (animation-timeline: scroll()) {
	.tjw-hs .tjw-hs__statement-text {
		animation-name: tjw-hs-lag;
		animation-timing-function: linear;
		animation-fill-mode: both;
		/* `scroll()`, not `scroll(root block)` — the bare form has no
		   space inside its parentheses. A minifier that normalises
		   whitespace inside a function would turn the second into
		   `scroll(rootblock)`, which is invalid, and the effect would be
		   missing in production only. Same family of trap as the calc()
		   `+` rule above. The default is `nearest block`, and the nearest
		   scrollport for a hero is the document. */
		animation-timeline: scroll();
		animation-range: 0px var(--tjw-hs-lag-range);
	}
}

@media (prefers-reduced-motion: reduce) {
	.tjw-hs__cta::after {
		transition: none;
	}

	.tjw-hs--anim .tjw-hs__statement,
	.tjw-hs--anim .tjw-hs__item {
		animation: none;
	}

	/* Scroll-linked motion is exactly what this preference is asking
	   about, so the statement simply travels with the page. */
	.tjw-hs .tjw-hs__statement-text {
		animation-name: none;
	}
}
