/**
 * Motion.
 *
 * One primitive: lines that draw themselves, taken from the logo, which traces
 * the monogram as a single continuous stroke.
 *
 * Every rule here is transform or opacity only, everything plays once, and
 * reduced motion is a full static build rather than a gentler one.
 */

/* ---------------------------------------------------------------
 * The primitive: a rule that draws
 * ------------------------------------------------------------- */

.js .fx-rule { transform: scaleX(0); }
.js .fx-rule.is-in { transform: scaleX(1); transition: transform var(--d-rule) var(--ease-out); }

/* Nav links change colour only, no rule and no underline. */
.site-nav a { transition: color var(--d-fast) ease; }

/* ---------------------------------------------------------------
 * Framing marks draw in, then tighten on hover
 * ------------------------------------------------------------- */

.js .fx-frame .frame__mark { opacity: 0; transform: scale(.72); }
.js .fx-frame.is-in .frame__mark {
	opacity: 1; transform: scale(1);
	transition: opacity var(--d-mid) var(--ease-out), transform var(--d-mid) var(--ease-out);
}
.js .fx-frame.is-in .frame__mark--tr { transition-delay: 50ms; }
.js .fx-frame.is-in .frame__mark--bl { transition-delay: 100ms; }
.js .fx-frame.is-in .frame__mark--br { transition-delay: 150ms; }

.film-card .frame__mark,
.frame--hover .frame__mark {
	transition: inset var(--d-mid) var(--ease-out), top var(--d-mid) var(--ease-out),
	            left var(--d-mid) var(--ease-out), right var(--d-mid) var(--ease-out),
	            bottom var(--d-mid) var(--ease-out);
}
.film-card:hover .frame__mark--tl, .film-card:focus-visible .frame__mark--tl { top: 22px; left: 22px; }
.film-card:hover .frame__mark--tr, .film-card:focus-visible .frame__mark--tr { top: 22px; right: 22px; }
.film-card:hover .frame__mark--bl, .film-card:focus-visible .frame__mark--bl { bottom: 22px; left: 22px; }
.film-card:hover .frame__mark--br, .film-card:focus-visible .frame__mark--br { bottom: 22px; right: 22px; }

/* ---------------------------------------------------------------
 * The hero opens through the logo.
 *
 * The footage is masked by the mark itself, which then grows until its edges
 * race off screen. It picks up exactly where the ident leaves off, so the
 * sequence reads as one gesture: the logo draws, then it opens into the film.
 *
 * The mask is anchored at 38%/58% of the mark rather than its centre, because
 * the centre of the monogram is a counter. Anchoring there would end the
 * reveal inside a hole, showing nothing.
 * ------------------------------------------------------------- */

@supports (mask-image: url("#x")) or (-webkit-mask-image: url("#x")) {
	.js .hero--reveal.is-open .hero__media {
		-webkit-mask-image: var(--logo-mask);
		        mask-image: var(--logo-mask);
		-webkit-mask-repeat: no-repeat;
		        mask-repeat: no-repeat;
		-webkit-mask-position: 50% 50%;
		        mask-position: 50% 50%;
		animation: hero-logo-open 1900ms linear forwards;
	}

	@keyframes hero-logo-open {
		0% {
			-webkit-mask-size: var(--mask-start) auto; mask-size: var(--mask-start) auto;
			-webkit-mask-position: 50% 50%; mask-position: 50% 50%;
		}
		30% {
			-webkit-mask-size: min(105vw, 660px) auto; mask-size: min(105vw, 660px) auto;
			-webkit-mask-position: 45% 53%; mask-position: 45% 53%;
		}
		55% {
			-webkit-mask-size: min(250vw, 1550px) auto; mask-size: min(250vw, 1550px) auto;
			-webkit-mask-position: 38% 58%; mask-position: 38% 58%;
		}
		75% {
			-webkit-mask-size: 4200px auto; mask-size: 4200px auto;
			-webkit-mask-position: 38% 58%; mask-position: 38% 58%;
		}
		90% {
			-webkit-mask-size: 11000px auto; mask-size: 11000px auto;
			-webkit-mask-position: 38% 58%; mask-position: 38% 58%;
		}
		100% {
			-webkit-mask-size: 26000px auto; mask-size: 26000px auto;
			-webkit-mask-position: 38% 58%; mask-position: 38% 58%;
		}
	}
}

/* The frame pushes in gently behind the opening mark. */
.js .hero--reveal .hero__media > * { transform: scale(1.12); }
.js .hero--reveal.is-open .hero__media > * {
	animation: hero-push 2500ms var(--ease-out) forwards;
}
@keyframes hero-push { to { transform: scale(1); } }

.js .hero--reveal .hero__scrim { opacity: 0; }
.js .hero--reveal.is-open .hero__scrim {
	animation: hero-scrim 900ms ease forwards;
	animation-delay: 1350ms;
}
@keyframes hero-scrim { to { opacity: 1; } }

/* The marks arrive once the mark itself has opened out. */
.js .hero--reveal .frame__mark { opacity: 0; }
.js .hero--reveal.is-open .frame__mark {
	animation: hero-mark 620ms var(--ease-out) forwards;
	animation-delay: 2000ms;
}
@keyframes hero-mark { from { opacity: 0; transform: scale(.55); } to { opacity: 1; transform: scale(1); } }

/* ---------------------------------------------------------------
 * The hero copy, after the logo has opened.
 *
 * Each of these uses `both` fill with a delay rather than a separate hidden
 * base state. The delay holds the `from` keyframe, so the copy waits its turn,
 * and if .is-open never arrives there is no animation and the copy is simply
 * visible. Nothing here can leave the headline hidden.
 * ------------------------------------------------------------- */

.js .hero--reveal.is-open .hero__heading .line > b {
	animation: hero-wipe 820ms var(--ease-out) 1820ms both;
}
/* Tighter than the old 150ms step, so a four line headline does not stretch
   the whole introduction out. */
.js .hero--reveal.is-open .hero__heading .line:nth-child(2) > b { animation-delay: 1930ms; }
.js .hero--reveal.is-open .hero__heading .line:nth-child(3) > b { animation-delay: 2040ms; }
.js .hero--reveal.is-open .hero__heading .line:nth-child(4) > b { animation-delay: 2150ms; }

@keyframes hero-wipe {
	from { clip-path: inset(-0.4em 100% -0.4em 0); }
	to   { clip-path: inset(-0.4em 0 -0.4em 0); }
}

/* The capability line takes the headline's own wipe, not the copy fade: it
   is set in the display face and belongs to that block. It waits for the
   headline to have gone by. */
.js .hero--reveal.is-open .hero__standfirst {
	animation: hero-copy 620ms var(--ease-out) 2500ms both;
}

/* The phrases ride in with the button now, so the row reveals as one piece
   and nothing animates the capability line separately. */
.js .hero--reveal.is-open .hero__actions {
	animation: hero-copy 620ms var(--ease-out) 2450ms both;
}

/* 3070ms is when the row has finished arriving and the phrases start
   cycling, so the beat keeps their period without JavaScript restarting it.
   One declaration, because this selector outranks the one in components.css
   and a shorthand here would otherwise cancel the beat outright. */
.js .hero--reveal.is-open .capline__dot {
	animation: capline-beat 2500ms var(--ease-out) 3070ms infinite;
}
@keyframes hero-copy {
	from { opacity: 0; transform: translateY(10px); }
	to   { opacity: 1; transform: none; }
}

/* The inner page heroes, on the home page hero's keyframes, curve and steps.
 *
 * Declared unconditionally, not gated on a class script adds. Stylesheets are
 * render blocking, so this applies before the first paint and `both` fill
 * holds the closed keyframe from that frame on: the headline is never briefly
 * shown finished and then snapped shut, which is what read as choppy. The
 * animation clock also starts at that same moment, so a 320ms delay reliably
 * puts the wipe just after the page appears rather than before it.
 *
 * Gating it on a class instead was tried twice and fails in both directions:
 * added after paint it causes the snap, and shortening a fallback delay once
 * that delay has already elapsed makes the headline jump straight to open with
 * no wipe at all, which is what happens on a slow connection. Pausing is the
 * only intervention that cannot produce either, which is all the ident does.
 *
 * Numbers are the home page's: 820ms a line, 110ms between lines, the copy
 * 680ms after the first line starts. */
.js .page-hero [data-timed-reveal] .line > b {
	/* Inline, exactly as the home page's is, and this is the whole difference
	   between the two. hero-wipe insets by a PERCENTAGE of this element's box.
	   Left as display:block the box is the full 1312px column while the words
	   are 501-756px of it, so the clip passes the last letter early and spends
	   the rest of its run revealing empty space: measured, the last letter of
	   "Contact" appeared 68ms into an 820ms animation and the remaining 599ms
	   was blank. The ease-out was all still there, it was just happening where
	   there was nothing to see, which is why it read as instant and flat.
	   Inline makes the box the words, so every millisecond of the curve, and
	   all of its deceleration, lands on letters.
	   Stated rather than inherited: these headlines also carry .fx-reveal from
	   the markup, and that rule sets display:block for the scroll reveal. */
	display: inline; font-weight: inherit;
	/* The closed state written out as well as relied on from the animation's
	   backwards fill. They are the same value, so this changes nothing about
	   how it looks, but it no longer depends on a browser applying backwards
	   fill before its first paint of the element. A running animation outranks
	   a normal declaration, so the wipe still wins the moment it starts. */
	clip-path: inset(-0.4em 100% -0.4em 0);
	animation: hero-wipe 820ms var(--ease-out) 320ms both;
}
.js .page-hero [data-timed-reveal] .line:nth-child(2) > b { animation-delay: 430ms; }
.js .page-hero [data-timed-reveal] .line:nth-child(3) > b { animation-delay: 540ms; }
.js .page-hero [data-timed-reveal] .line:nth-child(4) > b { animation-delay: 650ms; }

.js .page-hero [data-timed-after] {
	opacity: 0;
	animation: hero-copy 620ms var(--ease-out) 1000ms both;
}
.js .page-hero [data-timed-reveal]:has(.line:nth-child(2)) ~ [data-timed-after] { animation-delay: 1110ms; }
.js .page-hero [data-timed-reveal]:has(.line:nth-child(3)) ~ [data-timed-after] { animation-delay: 1220ms; }
.js .page-hero [data-timed-reveal]:has(.line:nth-child(4)) ~ [data-timed-after] { animation-delay: 1330ms; }

/* A second paragraph trails the first, not the headline. */
.js .page-hero [data-timed-after] + [data-timed-after] { animation-delay: 1160ms; }

/* Held, not withheld, while the ident is up: the delays run from page load, so
   otherwise the whole hero plays behind the loading screen and is over before
   it lifts. Pausing cannot jump or flash if the class never arrives or never
   leaves; the animation simply runs on its own clock. */
.ident-up .page-hero [data-timed-reveal] .line > b,
.ident-up .page-hero [data-timed-after] { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
	.js .page-hero [data-timed-reveal] .line > b { animation: none; clip-path: none; }
	.js .page-hero [data-timed-after] { animation: none; opacity: 1; transform: none; }
}

/* ---------------------------------------------------------------
 * Masked type reveals.
 *
 * Driven by scroll position rather than a timer: --reveal runs 0 to 1 as the
 * heading crosses the viewport, so the wipe is as slow as the reader's own
 * scrolling and you can actually watch it happen.
 *
 * A wipe, like a broadcast lower third, not the fade-up every template uses.
 * ------------------------------------------------------------- */

.fx-reveal { --reveal: 1; }

/* Multi-line headings: each line wipes on its own offset. The block display
   lives on .line itself so deliberate line breaks survive even when the reveal
   is switched off.

   No overflow clip here, and the clip-path insets are negative top and bottom.
   These headlines run a line-height below 1, so the glyphs sit outside their
   own box; anything clipping at the box edge cuts the tails off p, g, y and q.
   The wipe only ever needed to clip horizontally. */
.line { display: block; }
.js .fx-reveal .line > b {
	display: block; font-weight: inherit;
	clip-path: inset(-0.4em calc((1 - var(--line-reveal, var(--reveal))) * 100%) -0.4em 0);
}

/* Single-block headings get the same treatment on the element itself. */
.js .fx-reveal--block {
	clip-path: inset(-0.4em calc((1 - var(--reveal)) * 100%) -0.4em 0);
}

/* The default is revealed. JavaScript sets the progress explicitly on the
   headings it drives, so nothing is ever hidden by a script that did not run. */

/* ---------------------------------------------------------------
 * The bridging image.
 *
 * Same wipe vocabulary as the headlines, scroll-linked so it opens as you
 * arrive at it, with the picture settling out of a slight push-in. No
 * negative inset here: unlike type, nothing overflows the box.
 * ------------------------------------------------------------- */

/* Each of the three opens from a different edge, so the row arrives as three
   movements rather than one long wipe: the left panel in from the left, the
   centre up from the bottom, the right panel in from the right. */
.js .straddle.fx-reveal--block {
	clip-path: inset(0 calc((1 - var(--reveal, 1)) * 100%) 0 0);
}
.js .straddle--main.fx-reveal--block {
	clip-path: inset(calc((1 - var(--reveal, 1)) * 100%) 0 0 0);
}
.js .straddle--right.fx-reveal--block {
	clip-path: inset(0 0 0 calc((1 - var(--reveal, 1)) * 100%));
}
.js .straddle img {
	transform: scale(calc(1 + (1 - var(--reveal, 1)) * 0.14));
	transform-origin: center;
}

/* ---------------------------------------------------------------
 * The traced ident
 * ------------------------------------------------------------- */

.ident {
	position: fixed; inset: 0; z-index: var(--z-ident);
	background: var(--green-brand);
}
/* Hidden unless the head script says this visit gets one. Default-hidden
   means no-JS and repeat visits never see it, and because the decision is
   made before first paint there is no flash of the page underneath. */
.ident { display: none; }
.ident-pending .ident { display: block; }

/* The ident hands straight over to the hero mask, so it sits exactly where
   that mask starts: the mark's 38%/58% point, at 38% across and 58% down the
   hero. Same size, same place, no jump. */
.ident__mark {
	position: absolute;
	width: var(--mask-start);
	height: auto;
	left: 50%;
	top: calc(var(--header-h, 79px) + 0.5 * min(88vh, 940px));
	transform: translate(-50%, -50%);
}

/* The mark is bone on the brand green, which is how the logo is actually
   used. The trace draws along the monogram's own centreline, clipped to the
   real outline, so the edges are the logo at every frame of the animation. */
.ident__trace {
	stroke: var(--ink-dk);
	stroke-dasharray: var(--trace-len, 1200);
	stroke-dashoffset: var(--trace-len, 1200);
}
.ident.is-running .ident__trace {
	animation: speed-trace var(--d-trace) var(--ease-trace) forwards;
}
@keyframes speed-trace { to { stroke-dashoffset: 0; } }

/* The dot is its own shape in the artwork, so it lands once the stroke has
   arrived rather than being swept into it. */
.ident__dot {
	fill: var(--ink-dk);
	transform-origin: -10.8px 21.8px;
	transform: scale(0);
}
.ident.is-running .ident__dot {
	animation: speed-dot 320ms var(--ease-out) forwards;
	animation-delay: calc(var(--d-trace) * .78);
}
@keyframes speed-dot { to { transform: scale(1); } }

.ident.is-leaving { animation: speed-ident-out 620ms var(--ease-out) forwards; }
@keyframes speed-ident-out { to { transform: translateY(-100%); } }

.ident__skip {
	position: absolute; bottom: var(--s6); right: var(--s6);
	background: none; border: 1px solid var(--green-700); border-radius: 2px;
	color: var(--mute-dk); cursor: pointer; padding: 8px 14px;
	font-family: var(--serif); font-size: 10.5px;
}

/* ---------------------------------------------------------------
 * Scrub and magnetic
 * ------------------------------------------------------------- */

.film-card__proxy { transition: opacity var(--d-fast) ease; }

.is-magnetic { transition: transform 180ms var(--ease-out); }

/* ---------------------------------------------------------------
 * Anchored scrolling
 *
 * The jump rail moves down the page rather than snapping to it. The
 * reduced-motion block below already forces scroll-behavior back to auto, so
 * anyone who asks for less movement gets the instant jump instead.
 * ------------------------------------------------------------- */

html { scroll-behavior: smooth; }

/* A target has to clear the site header AND the sticky rail, or it lands
   underneath them. Both heights are published from JavaScript and re-measured
   on resize, since the rail wraps to two rows on narrow screens. The fallbacks
   match a single row. */
section.band[id] {
	scroll-margin-top: calc(var(--header-h, 78px) + var(--rail-h, 83px));
}

/* ---------------------------------------------------------------
 * The camera reticle. Sitewide, and it reads its colour from
 * whatever ground it is currently over.
 * ------------------------------------------------------------- */

/* Only hide the native cursor once the reticle is actually running, so a
   browser that never fires the script is never left with no cursor at all. */
html.has-reticle,
html.has-reticle body { cursor: none; }

/* The showreel is a modal dialog, drawn in the top layer where the reticle
   cannot follow. Give the real cursor back for as long as it is open. */
html.is-reel-open,
html.is-reel-open body { cursor: auto; }
html.is-reel-open .reticle { opacity: 0; }

/* Text entry and text selection keep their real cursors: a reticle over a
   form field hides where the caret will land. */
html.has-reticle input,
html.has-reticle textarea,
html.has-reticle select,
html.has-reticle [contenteditable] { cursor: auto; }

.reticle {
	position: fixed; top: 0; left: 0;
	z-index: var(--z-reticle);
	width: 34px; height: 34px;
	margin: -17px 0 0 -17px;
	pointer-events: none;
	opacity: 0;
	color: var(--accent);
	transition: opacity var(--d-fast) ease, width var(--d-mid) var(--ease-out),
	            height var(--d-mid) var(--ease-out), margin var(--d-mid) var(--ease-out),
	            color var(--d-mid) ease;
}
.reticle.is-on { opacity: 1; }

/* Four crop marks, drawn from the same gesture as the framing marks. */
.reticle i {
	position: absolute; display: block;
	width: 11px; height: 11px;
	border: 3px solid currentColor;
	transition: inset var(--d-mid) var(--ease-out), width var(--d-mid) var(--ease-out),
	            height var(--d-mid) var(--ease-out), border-width var(--d-mid) var(--ease-out);
}
.reticle i:nth-child(1) { top: 0; left: 0;  border-right: 0; border-bottom: 0; }
.reticle i:nth-child(2) { top: 0; right: 0; border-left: 0;  border-bottom: 0; }
.reticle i:nth-child(3) { bottom: 0; left: 0;  border-right: 0; border-top: 0; }
.reticle i:nth-child(4) { bottom: 0; right: 0; border-left: 0;  border-top: 0; }

/* A centre dot, so the exact pointer position is never ambiguous. */
.reticle::after {
	content: ""; position: absolute; top: 50%; left: 50%;
	width: 4px; height: 4px; margin: -2px 0 0 -2px;
	background: currentColor; border-radius: 50%;
	transition: opacity var(--d-fast) ease;
}

/* Ground awareness. The class is set from JS by reading the band under the
   pointer, so the reticle stays legible on paper, on the accent band and on
   footage without any pixel sampling. */
.reticle.on-paper  { color: var(--accent-deep); }
.reticle.on-accent { color: var(--green-950); }
.reticle.on-media  { color: var(--bone); }

/* Over anything clickable it opens up and becomes a target. */
.reticle.is-active { width: 50px; height: 50px; margin: -25px 0 0 -25px; }
.reticle.is-active i { width: 15px; height: 15px; }

/* Over a film it becomes a record mark. */
.reticle.is-recording { color: var(--signal); }
.reticle.is-recording::after { width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px; }

/* Over text it thins down so it does not fight the words. The corner marks
   have to come down with it: at 11px each, four of them exactly span a 22px
   box and the reticle closes into a plain square. Six keeps a 10px gap on
   every edge, so it still reads as a camera frame at half the size. */
.reticle.is-text { width: 22px; height: 22px; margin: -11px 0 0 -11px; opacity: .55; }
.reticle.is-text i { width: 6px; height: 6px; border-width: 2px; }
.reticle.is-text::after { width: 3px; height: 3px; margin: -1.5px 0 0 -1.5px; }

/* The label rides under the reticle and only appears where there is something
   to watch. Positioned off the reticle's own box so it stays centred as the
   reticle resizes between states. */
.reticle__label {
	position: absolute; top: 100%; left: 50%;
	transform: translate(-50%, 10px);
	margin-top: 8px;
	font-family: var(--serif); font-weight: 500;
	font-size: 15px; line-height: 1;
	white-space: nowrap;
	opacity: 0; pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
	.reticle__label { transition: opacity var(--d-fast) ease, transform var(--d-mid) var(--ease-out); }
}
.reticle.is-watch .reticle__label { opacity: 1; transform: translate(-50%, 0); }
.reticle.is-watch { width: 62px; height: 62px; margin: -31px 0 0 -31px; }
.reticle.is-watch i { width: 17px; height: 17px; }

/* ---------------------------------------------------------------
 * Reduced motion: a full static build, not a gentler one.
 * ------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
	.js .fx-rule,
	.js .fx-rule.is-in { transform: scaleX(1); transition: none; }

	.js .fx-frame .frame__mark,
	.js .fx-frame.is-in .frame__mark { opacity: 1; transform: none; transition: none; }

	.film-card .frame__mark,
	.frame--hover .frame__mark { transition: none; }

	.js .fx-reveal .line > b,
	.js .fx-reveal--block,
	.js .straddle.fx-reveal--block,
	.js .straddle--main.fx-reveal--block,
	.js .straddle--right.fx-reveal--block { clip-path: none; --reveal: 1; }
	.js .straddle img { transform: none; }

	.ident { display: none !important; }

	.js .hero--reveal .hero__media {
		-webkit-mask-image: none; mask-image: none;
		clip-path: none; animation: none;
	}
	.js .hero--reveal .hero__media > * { transform: none; animation: none; }
	.js .hero--reveal .hero__scrim,
	.js .hero--reveal .hero__standfirst,
	.js .hero--reveal .hero__actions { opacity: 1; transform: none; animation: none; }
	.js .hero--reveal .hero__heading .line > b { clip-path: none; animation: none; }
	.js .hero--reveal .frame__mark { opacity: 1; animation: none; }

	.is-magnetic { transition: none; transform: none !important; }
	html.has-reticle, html.has-reticle body { cursor: auto; }
	.reticle { display: none; }

	*, *::before, *::after {
		animation-duration: .001ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: .001ms !important;
		scroll-behavior: auto !important;
	}
}

/* A coarse pointer never gets the reticle, and never loses its cursor. */
@media (pointer: coarse) {
	html.has-reticle, html.has-reticle body { cursor: auto; }
	.reticle { display: none; }
}
