/*
 * animations.css — the motion layer.
 *
 * Three registers are mixed here deliberately:
 *   · editorial  — masked letter reveals, band wipes, oversized display motion
 *   · precision  — spring-eased controls, cursor-tracked borders, dot fields
 *   · dimension  — perspective scenes, depth-of-field, shadow travel
 *
 * Everything is driven by assets/js/motion.js adding state classes, and
 * everything is neutralised by prefers-reduced-motion and by the
 * html.motion-off toggle (both handled globally in base.css).
 */

/* ═══════════════════════════════════════════════════════════════
 * 0. No-JS safety net.
 *
 * Every reveal below starts at opacity:0 and waits for motion.js to
 * add a class. If that script fails to load — CDN blocked, JS error,
 * a browser with scripting off — the page would render completely
 * blank. header.php stamps html.js before first paint, so the absence
 * of that class is a reliable "nothing is going to animate" signal
 * and everything is forced to its finished state.
 * ═══════════════════════════════════════════════════════════════ */
html:not(.js) [data-reveal],
html:not(.js) .split-char {
	opacity: 1;
	transform: none;
	filter: none;
	clip-path: none;
}
html:not(.js) .reveal-mask > *   { transform: none; }
html:not(.js) .draw-path         { stroke-dashoffset: 0; }

/* ═══════════════════════════════════════════════════════════════
 * 1. Scroll reveal
 * motion.js adds .is-revealed and writes transition-delay inline
 * from data-reveal-delay / data-reveal-stagger.
 * ═══════════════════════════════════════════════════════════════ */
[data-reveal] {
	opacity: 0;
	transition-property: opacity, transform, filter, clip-path;
	transition-duration: var(--dur-slow);
	transition-timing-function: var(--ease-out);
	will-change: transform, opacity;
}
[data-reveal="up"]    { transform: translateY(34px); }
[data-reveal="down"]  { transform: translateY(-26px); }
[data-reveal="left"]  { transform: translateX(-34px); }
[data-reveal="right"] { transform: translateX(34px); }
[data-reveal="scale"] { transform: scale(.92); }
[data-reveal="blur"]  { filter: blur(14px); }
[data-reveal="clip"]  { opacity: 1; clip-path: inset(0 0 100% 0); }

/* Rise — the workhorse. Slightly rotated so it doesn't read as a
   flat slide; the rotation is small enough to register as weight. */
[data-reveal="rise"] {
	transform: translateY(46px) rotate(.6deg);
	transition-duration: var(--dur-xslow);
}

[data-reveal].is-revealed {
	opacity: 1;
	transform: none;
	filter: blur(0);
	clip-path: inset(0 0 0% 0);
}

/* ═══════════════════════════════════════════════════════════════
 * 2. Masked line reveal — text slides up from behind its own edge.
 * Reads far more considered than a fade, and costs the same.
 * ═══════════════════════════════════════════════════════════════ */
.reveal-mask {
	overflow: hidden;
	display: block;
	/* Tight display line-heights let descenders sit fractionally below
	   the box; without this the mask shaves the tail off a "g" or "y". */
	padding-bottom: .12em;
	margin-bottom: -.12em;
}
.reveal-mask > * {
	display: block;
	transform: translateY(105%);
	transition: transform var(--dur-xslow) var(--ease-out);
}
.reveal-mask.is-revealed > * { transform: none; }

/* ═══════════════════════════════════════════════════════════════
 * 3. Letter-split headline (editorial)
 * motion.js splits [data-split] into .split-line > .split-char.
 * ═══════════════════════════════════════════════════════════════ */
/* The mask sits on each WORD, not each line. A line-level mask breaks
   the moment a headline wraps — characters translating out of one line
   would overlap the next. Word-level boxes never wrap internally, so
   the reveal survives any viewport width. */
.split-word {
	display: inline-block;
	overflow: hidden;
	vertical-align: top;
	/* Room for descenders, taken back so line-height is untouched. */
	padding-bottom: .14em;
	margin-bottom: -.14em;
}
.split-char {
	display: inline-block;
	transform: translateY(108%) rotate(4deg);
	opacity: 0;
	transition:
		transform var(--dur-xslow) var(--ease-out),
		opacity calc(var(--dur-base) * 1.6) ease;
	will-change: transform;
}
.split-char.is-in { transform: none; opacity: 1; }

/* ═══════════════════════════════════════════════════════════════
 * 4. Band wipe — dark sections clip themselves into view instead of
 * simply being there. This is the colour-inversion moment, adapted
 * so it never hijacks scrolling.
 * ═══════════════════════════════════════════════════════════════ */
/*
 * The band only surrenders its own background once motion.js has
 * confirmed it is actually going to animate it, by adding .wipe-ready.
 *
 * That ordering is the whole point. Making the section transparent by
 * default means any failure to add .is-in — a JS error, an observer
 * that never fires, printing, reader mode — leaves white text on a
 * white ground. Gating on .wipe-ready inverts the failure mode: if
 * anything goes wrong the band just renders normally.
 */
.band-wipe {
	position: relative;
	isolation: isolate;
}
.band-wipe.wipe-ready { background-color: transparent; }
/* This deliberately uses ::after, not ::before. Bands are frequently
   also .spotlight elements, and .spotlight::before is declared further
   down this file — sharing the pseudo-element meant the spotlight's
   gradient silently replaced the band colour and the section rendered
   white-on-transparent. Two layers, two pseudo-elements:
     -2  band colour (here)
     -1  spotlight glow
      0  content                                                     */
.band-wipe.wipe-ready::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -2;
	border-radius: inherit;
	background-color: var(--band-bg, var(--navy));
	clip-path: inset(100% 0 0 0);
	transition: clip-path var(--dur-xslow) var(--ease-wipe);
	pointer-events: none;
}
.band-wipe.wipe-ready.is-in::after { clip-path: inset(0 0 0 0); }

/* Printing never fires a scroll observer, so any band the reader hadn't
   scrolled to yet would come out as white text on white paper. */
@media print {
	.band-wipe.wipe-ready::after { clip-path: inset(0 0 0 0); }
}

/* ═══════════════════════════════════════════════════════════════
 * 5. Cursor-tracked border gradient (precision)
 * A 1px gradient ring that follows the pointer around a card. Built
 * with a mask so it only ever paints the border, never the fill.
 * ═══════════════════════════════════════════════════════════════ */
[data-glow] { position: relative; }
[data-glow]::after {
	content: "";
	position: absolute;
	inset: -1px;
	border-radius: inherit;
	padding: 1px;
	pointer-events: none;
	opacity: 0;
	transition: opacity var(--dur-base) var(--ease);
	background: radial-gradient(
		240px circle at var(--gx, 50%) var(--gy, 50%),
		rgba(var(--brand-rgb), .85),
		transparent 62%
	);
	-webkit-mask: linear-gradient(#000, #000) content-box, linear-gradient(#000, #000);
	mask: linear-gradient(#000, #000) content-box, linear-gradient(#000, #000);
	-webkit-mask-composite: xor;
	mask-composite: exclude;
}
[data-glow]:hover::after,
[data-glow]:focus-within::after { opacity: 1; }

/* ═══════════════════════════════════════════════════════════════
 * 6. Perspective scenes (dimension)
 * motion.js writes --rx/--ry from pointer position on [data-scene].
 * ═══════════════════════════════════════════════════════════════ */
[data-scene] { perspective: 1400px; perspective-origin: 50% 45%; }
[data-scene] .scene__stage {
	position: relative;
	width: 100%;
	height: 100%;
	transform-style: preserve-3d;
	transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
	transition: transform var(--dur-slow) var(--ease);
}
.scene__layer {
	position: absolute;
	transform-style: preserve-3d;
	will-change: transform;
}
/* Layers behind the focal plane lose focus, exactly like a lens */
.scene__layer--back {
	filter: blur(2.5px);
	opacity: .62;
}

/* ═══════════════════════════════════════════════════════════════
 * 7. Pointer tilt — cards lift toward the cursor with travelling shadow
 * ═══════════════════════════════════════════════════════════════ */
[data-tilt] {
	transform-style: preserve-3d;
	will-change: transform;
	transition: transform var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease);
}
[data-tilt]:hover { box-shadow: var(--shadow-l); }

/* ═══════════════════════════════════════════════════════════════
 * 8. Magnetic buttons — motion.js sets transform on move, clears on
 * leave; this only guarantees a smooth release.
 * ═══════════════════════════════════════════════════════════════ */
[data-magnetic] { transition: transform 340ms var(--ease-spring); }

/* ═══════════════════════════════════════════════════════════════
 * 9. Spring indicator (precision) — segmented controls / filter pills
 * ═══════════════════════════════════════════════════════════════ */
.spring-ind {
	position: absolute;
	z-index: 0;
	top: 4px;
	bottom: 4px;
	left: 4px;
	width: 0;
	border-radius: var(--radius-full);
	background: var(--ink);
	transition:
		transform var(--dur-slow) var(--ease-spring),
		width var(--dur-slow) var(--ease-spring);
}

/* ═══════════════════════════════════════════════════════════════
 * 10. Cursor spotlight — dark bands only
 * ═══════════════════════════════════════════════════════════════ */
/* The glow sits on ::before at z-index -1 inside an isolated stacking
   context, so it paints above the section background but below every
   child. Do NOT lift children with `.spotlight > * { position: relative }`
   — that blanket rule silently overrides absolutely-positioned
   backdrop layers (it collapsed the hero canvas to 0×0). */
.spotlight { position: relative; isolation: isolate; }
.spotlight::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background: radial-gradient(620px circle at var(--x, 50%) var(--y, 50%), rgba(255,255,255,.075), transparent 45%);
	opacity: 0;
	transition: opacity var(--dur-base) var(--ease);
	pointer-events: none;
}
.spotlight:hover::before { opacity: 1; }

/* ═══════════════════════════════════════════════════════════════
 * 11. Ambient float — decorative panels drifting on their own
 * ═══════════════════════════════════════════════════════════════ */
@keyframes floatY {
	0%, 100% { translate: 0 0; }
	50%      { translate: 0 -14px; }
}
@keyframes floatSlow {
	0%, 100% { translate: 0 0; rotate: 0deg; }
	50%      { translate: 0 -20px; rotate: 1.2deg; }
}
.float     { animation: floatY 6s ease-in-out infinite; }
.float--sl { animation: floatSlow 9s ease-in-out infinite; }

/* Morphing blob behind perspective scenes */
@keyframes blobMorph {
	0%, 100% { border-radius: 47% 53% 44% 56% / 58% 42% 58% 42%; rotate: 0deg; }
	50%      { border-radius: 58% 42% 62% 38% / 42% 62% 38% 58%; rotate: 14deg; }
}
.blob { animation: blobMorph 16s ease-in-out infinite; }

/* ═══════════════════════════════════════════════════════════════
 * 12. Page enter
 * ═══════════════════════════════════════════════════════════════ */
body { animation: pageEnter 520ms var(--ease-out); }
@keyframes pageEnter {
	from { opacity: 0; }
	to   { opacity: 1; }
}

/* ═══════════════════════════════════════════════════════════════
 * 13. Spinner (form submit state)
 * ═══════════════════════════════════════════════════════════════ */
.spinner {
	width: 18px;
	height: 18px;
	border: 2.5px solid rgba(255,255,255,.4);
	border-top-color: #fff;
	border-radius: 50%;
	animation: spin 700ms linear infinite;
	display: none;
}
.is-loading .spinner { display: inline-block; }
.is-loading .btn__label, .is-loading .btn__icon { display: none; }
@keyframes spin { to { rotate: 360deg; } }

/* ═══════════════════════════════════════════════════════════════
 * 14. Counters & path draw
 * ═══════════════════════════════════════════════════════════════ */
[data-counter] { font-variant-numeric: tabular-nums; }

.draw-path {
	stroke-dasharray: var(--path-length, 1000);
	stroke-dashoffset: var(--path-length, 1000);
	transition: stroke-dashoffset 1400ms var(--ease-out);
}
.draw-path.is-drawn { stroke-dashoffset: 0; }

/* ═══════════════════════════════════════════════════════════════
 * 15. Sticky-scroll feature block (precision)
 * The visual pins while the copy column scrolls past it.
 * ═══════════════════════════════════════════════════════════════ */
.sticky-scroll { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-xl); }
.sticky-scroll__visual {
	position: sticky;
	top: calc(var(--header-h) + var(--space-l));
	height: fit-content;
}
.sticky-scroll__step { min-height: 62vh; display: flex; flex-direction: column; justify-content: center; }
.sticky-scroll__step > * { opacity: .34; transition: opacity var(--dur-slow) var(--ease); }
.sticky-scroll__step.is-active > * { opacity: 1; }
@media (max-width: 900px) {
	.sticky-scroll { grid-template-columns: 1fr; }
	.sticky-scroll__visual { position: static; }
	.sticky-scroll__step { min-height: 0; }
	.sticky-scroll__step > * { opacity: 1; }
}

/* ═══════════════════════════════════════════════════════════════
 * 16. Reduced motion — belt and braces on top of base.css.
 * Anything that hides content until animated must be forced visible,
 * or the page reads as broken rather than as calm.
 * ═══════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
	[data-reveal],
	[data-reveal].is-revealed { opacity: 1; transform: none; filter: none; clip-path: none; }
	.reveal-mask > *          { transform: none; }
	.split-char               { transform: none; opacity: 1; }
	.draw-path                { stroke-dashoffset: 0; }
	.float, .float--sl, .blob { animation: none; }
	[data-scene] .scene__stage { transform: none; }
	body                      { animation: none; }
}
html.motion-off [data-reveal],
html.motion-off [data-reveal].is-revealed { opacity: 1; transform: none; filter: none; clip-path: none; }
html.motion-off .reveal-mask > *   { transform: none; }
html.motion-off .split-char        { transform: none; opacity: 1; }
html.motion-off .draw-path         { stroke-dashoffset: 0; }
html.motion-off .float,
html.motion-off .float--sl,
html.motion-off .blob              { animation: none; }
html.motion-off [data-scene] .scene__stage { transform: none; }
