/* ==========================================================================
   MOTION SYSTEM — tyler-kung.com
   --------------------------------------------------------------------------
   Single source of truth for loading, reveal, and transition behavior.
   Plain CSS. The old LESS sources had drifted from the compiled styles.css and
   could not be recompiled, so they were deleted — styles.css and this file are
   now hand-authored and are the only stylesheets.

   Load order: styles.css  ->  motion.css
   Docs: /MOTION.md
   ========================================================================== */


/* --------------------------------------------------------------------------
   1. TOKENS
   Every animated property on this site pulls from these. Do not hardcode
   durations or easings in page CSS — reach for a token or add one here.
   -------------------------------------------------------------------------- */

:root {
	/* Duration — pick by how much surface area is moving.
	   instant : color / opacity on a small element (icon, link, badge)
	   fast    : hover states, small transforms, anything under the cursor
	   base    : a single element entering or leaving
	   slow    : a whole section, an overlay, a large surface
	   stately : loader beats only — never for interaction feedback */
	--mo-instant:  120ms;
	--mo-fast:     200ms;
	--mo-base:     320ms;
	--mo-slow:     500ms;
	--mo-stately:  800ms;

	/* Easing — entrances decelerate, exits accelerate, moves do both. */
	--mo-ease-out:    cubic-bezier(0.22, 1, 0.36, 1);      /* entering  */
	--mo-ease-in:     cubic-bezier(0.55, 0, 1, 0.45);      /* leaving   */
	--mo-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);      /* moving    */
	--mo-ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);    /* accents, sparingly */
	--mo-ease-linear: linear;                              /* continuous loops  */

	/* Travel — how far an element moves on entrance. Short. Always short. */
	--mo-travel-sm:  8px;
	--mo-travel-md: 16px;
	--mo-travel-lg: 24px;

	/* Stagger — delay between siblings in a sequence. */
	--mo-stagger: 60ms;

	/* Loader */
	--mo-loader-draw:  900ms;   /* stroke draw-in */
	--mo-loader-min:  1100ms;   /* minimum on-screen time, prevents a flash */
	--mo-loader-max:  4000ms;   /* hard failsafe — dismiss no matter what */

	/* Loader scale, as a multiple of the 64px header tile.
	   The mark enters oversized and settles to `scale` while it draws. */
	--mo-loader-scale:     2.2;
	--mo-loader-scale-in:  6;

	/* HDR logo bloom — the one-shot SVG -> HDR handoff. Long on purpose: this
	   is a luminance ramp, not a swap, and it only ever plays once. */
	--mo-hdr-bloom:       1600ms;
	--mo-hdr-bloom-delay:  240ms;
	/* Fallback curve for engines without linear(). */
	--mo-hdr-ease: cubic-bezier(0.45, 0, 0.75, 0.7);
}

/* A squared opacity ramp. HDR peak luminance runs several times SDR white, so
   a linear ramp reads as "dark, then suddenly lit" — most of the perceived
   brightness change lands in the first third. Weighting the curve toward the
   back spreads the glow evenly across the whole duration. */
@supports (transition-timing-function: linear(0, 1)) {
	:root {
		--mo-hdr-ease: linear(
			0,
			0.0156 12.5%,
			0.0625 25%,
			0.1406 37.5%,
			0.25 50%,
			0.3906 62.5%,
			0.5625 75%,
			0.7656 87.5%,
			1
		);
	}
}

/* At 6x the mark box is 384px and its artwork ~319px. That clears a 390px
   phone comfortably but runs to the edge on a 320px one, so narrow screens
   enter slightly smaller. Desktop keeps the full 6x. */
@media (max-width: 480px) {
	:root {
		--mo-loader-scale-in: 4.4;
	}
}


/* --------------------------------------------------------------------------
   2. LOADING
   The inline <head> snippet stamps html.tk-loading before first paint, so
   the panel below is up before any content can flash. If JS is disabled the
   class is never added and the page simply renders — no dependency on the
   loader ever running.
   -------------------------------------------------------------------------- */

/* Pre-paint panel. Identical color to the loader overlay, so the handoff
   from "CSS painted this" to "JS injected the real loader" is invisible. */
html.tk-loading::before {
	content: '';
	position: fixed;
	inset: 0;
	background-color: #212121;
	z-index: 10050;
	pointer-events: none;
}

html.tk-loading {
	overflow: hidden;
}

/* Page content is held back while loading, then handed to the reveal system. */
html.tk-loading body > .wrapper,
html.tk-loading body > .gate,
html.tk-loading body > #content-mount {
	opacity: 0;
}

.tk-loader {
	position: fixed;
	inset: 0;
	z-index: 10051;
	background-color: #212121;
	transition: opacity var(--mo-slow) var(--mo-ease-in);
	/* Swallows clicks while up — the content beneath is only opacity:0 and
	   would otherwise still be clickable. */
	pointer-events: auto;
}

.tk-loader.is-dismissing {
	opacity: 0;
	pointer-events: none;
}

/* The mark is fixed and viewport-centered so the flight to the header slot
   is a pure transform — no layout thrash mid-animation. */
.tk-loader__mark {
	position: fixed;
	top: 50%;
	left: 50%;
	width: 64px;
	height: 64px;
	margin: -32px 0 0 -32px;
	border-radius: 8px;
	background-color: #212121;
	outline: 2px solid rgba(191, 234, 252, 0);
	/* Entry size. Settles to --mo-loader-scale over the draw, then the inline
	   transform JS writes for the flight overrides both. */
	transform: scale(var(--mo-loader-scale-in));
	transform-origin: 50% 50%;
	transition:
		transform var(--mo-loader-draw) var(--mo-ease-out),
		outline-color var(--mo-base) var(--mo-ease-out);
	will-change: transform;
}

/* Beat 1 — the mark scales down as it draws, sharing the draw's duration. */
.tk-loader.is-drawing .tk-loader__mark {
	transform: scale(var(--mo-loader-scale));
}

/* Beat 4 — the flight to the header runs on its own, slower curve. */
.tk-loader.is-dismissing .tk-loader__mark {
	transition:
		transform var(--mo-slow) var(--mo-ease-in-out),
		outline-color var(--mo-base) var(--mo-ease-out);
}

/* Beat 2 — the tile chrome arrives once the drawing has resolved. */
.tk-loader__mark.is-settled {
	outline-color: rgba(191, 234, 252, 1);
}

.tk-loader__svg {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
}

/* Beat 1 — stroke draws itself in. pathLength="1" on the paths normalizes
   the geometry so a single dash value works for both. */
.tk-loader__svg path {
	fill: #BFEAFC;
	fill-opacity: 0;
	stroke: #BFEAFC;
	stroke-width: 1.25;
	stroke-dasharray: 1;
	stroke-dashoffset: 1;
	transition:
		fill-opacity var(--mo-base) var(--mo-ease-out),
		stroke-opacity var(--mo-base) var(--mo-ease-out);
}

.tk-loader.is-drawing .tk-loader__svg path {
	animation: tk-draw var(--mo-loader-draw) var(--mo-ease-in-out) forwards;
}

/* Second path is shorter — it starts slightly later so the mark resolves
   as one gesture rather than two paths racing. */
.tk-loader.is-drawing .tk-loader__svg path:nth-child(2) {
	animation-delay: 180ms;
	animation-duration: calc(var(--mo-loader-draw) - 180ms);
}

/* Beat 2 — ink fills, wireframe recedes. */
.tk-loader.is-filled .tk-loader__svg path {
	fill-opacity: 1;
	stroke-opacity: 0;
}

@keyframes tk-draw {
	from { stroke-dashoffset: 1; }
	to   { stroke-dashoffset: 0; }
}

/* The real header logo is held invisible until the flying mark lands on it,
   so there is never a moment with two logos on screen. */
html.tk-loading .top-header-logo .box-logo {
	opacity: 0;
}

.top-header-logo .box-logo {
	transition: opacity var(--mo-fast) var(--mo-ease-out),
	            outline-color var(--mo-fast) var(--mo-ease-out),
	            box-shadow var(--mo-fast) var(--mo-ease-out);
}


/* --------------------------------------------------------------------------
   2b. HEADER LOGO — HDR IDLE STATE
   The mark that draws and flies in is the SVG. Once the whole sequence has
   settled, the HDR artwork crossfades over it and becomes the resting state.
   Hover fades it back out to the SVG, which is what carries the highlight
   colour — so the hover affordance still reads.

   assets/img/tk-hdr-glow.png is Rec.2100 PQ HDR — cICP primaries=9,
   transfer=16 (BT.2020 / PQ ST2084), plus a matching ICC profile. The cICP
   chunk is the only thing telling browsers to render it as HDR.

   It is committed byte-for-byte from the source. DO NOT run it through any
   optimiser, resizer, or export step: those strip unrecognised chunks by
   default, and losing cICP silently demotes it to SDR. Note this is the
   format that works — a PQ-tagged JPEG does NOT, because browser HDR for
   JPEG is gain-map driven rather than ICC driven.
   -------------------------------------------------------------------------- */

.top-header-logo .box-logo .logo-hdr {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	border-radius: 8px;
	object-fit: cover;
	z-index: 2;
	opacity: 0;
	pointer-events: none;
	/* Resting timing — this is what hover in/out uses. */
	transition: opacity var(--mo-fast) var(--mo-ease-out);
}

.top-header-logo .box-logo .logo {
	z-index: 1;
}

/* Logo hover tilt. Replaces a jQuery + GSAP handler that existed only to
   rotate this one element — dropping both CDN libraries.

   The rotation is written into the `transform` shorthand alongside the
   existing centring translate, NOT as the standalone `rotate` property:
   individual transform properties apply before `transform`, so `rotate` would
   have spun the -50%/-50% offset with it and thrown the mark off centre.

   Scoped to tiles carrying the HDR artwork because the old handler lived in
   js/scripts.js, which only the homepage loaded — the case study logos never
   tilted and should not start now. */
.top-header-logo .box-logo:has(.logo-hdr) .logo {
	transition: transform var(--mo-fast) var(--mo-ease-spring);
}

.top-header-logo .box-logo:has(.logo-hdr):hover .logo {
	transform: translateX(-50%) translateY(-50%) rotate(5deg);
}

/* One-shot: only the first arrival gets the long luminance ramp. JS drops
   this class once the bloom is done, so hover stays snappy afterwards. */
html.tk-blooming .top-header-logo .box-logo .logo-hdr {
	transition: opacity var(--mo-hdr-bloom) var(--mo-hdr-ease)
	            var(--mo-hdr-bloom-delay);
}

/* tk-settled lands once the loader is fully done (or immediately, on a page
   that never ran one), so the artwork never appears mid-flight. */
html.tk-settled .top-header-logo .box-logo .logo-hdr {
	opacity: 1;
}

html.tk-settled .top-header-logo .box-logo:hover .logo-hdr {
	opacity: 0;
}

/* The tile keeps its cyan outline — the artwork no longer carries a border of
   its own, so there is nothing to collide with. */


/* --------------------------------------------------------------------------
   3. REVEAL
   Scroll-driven entrances. Targets are registered by selector in
   js/motion.js — pages do not hand-tag elements.
   -------------------------------------------------------------------------- */

[data-reveal] {
	opacity: 0;
	transform: translateY(var(--mo-travel-lg));
	transition:
		opacity var(--mo-slow) var(--mo-ease-out),
		transform var(--mo-slow) var(--mo-ease-out);
	transition-delay: calc(var(--mo-i, 0) * var(--mo-stagger));
	will-change: opacity, transform;
}

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

/* Once an element has arrived it must stop advertising itself to the
   compositor, or long pages accumulate dozens of promoted layers. */
[data-reveal].is-settled {
	will-change: auto;
	transition-delay: 0ms;
}

/* Shorter travel for smaller things — a thumbnail sliding 24px reads as
   sloppy next to a section that travels the same distance. */
[data-reveal='item'] {
	transform: translateY(var(--mo-travel-md));
	transition-duration: var(--mo-base);
}

[data-reveal='micro'] {
	transform: translateY(var(--mo-travel-sm));
	transition-duration: var(--mo-base);
}


/* --------------------------------------------------------------------------
   4. INTERACTION
   Hover and focus feedback lives under --mo-fast so it always feels like a
   direct response to the cursor.
   -------------------------------------------------------------------------- */

.item-group .item .item-thumb {
	transition: outline-color var(--mo-fast) var(--mo-ease-out),
	            transform var(--mo-fast) var(--mo-ease-out);
}

.item-group .item:hover .item-thumb {
	outline-color: rgba(191, 234, 252, 0.9);
	transform: translateY(calc(var(--mo-travel-sm) * -0.5));
}

.item-group .item .item-thumb .thumb-1 {
	transition: opacity var(--mo-base) var(--mo-ease-in-out);
}

.item-group .item .item-title {
	transition: color var(--mo-fast) var(--mo-ease-out);
}

/* Keyboard parity — anything that responds to hover responds to focus. */
.item-group a.item:focus-visible .item-thumb,
.item-group a.item:focus-visible .item-thumb {
	outline-color: #E9DAA1;
	transform: translateY(calc(var(--mo-travel-sm) * -0.5));
}

a:focus-visible,
button:focus-visible {
	outline: 2px solid #E9DAA1;
	outline-offset: 3px;
	border-radius: 2px;
}


/* --------------------------------------------------------------------------
   5. REDUCED MOTION
   Honor the OS setting everywhere. Nothing is removed — everything simply
   resolves through opacity, instantly. The loader still appears and still
   dismisses, so the load sequence stays coherent.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
	:root {
		--mo-instant: 1ms;
		--mo-fast:    1ms;
		--mo-base:    1ms;
		--mo-slow:    1ms;
		--mo-stately: 1ms;
		--mo-stagger: 0ms;
		--mo-travel-sm: 0px;
		--mo-travel-md: 0px;
		--mo-travel-lg: 0px;
		--mo-loader-draw: 1ms;
		--mo-loader-min: 200ms;
		/* No entry scale — the mark simply sits at its resting size. */
		--mo-loader-scale-in: var(--mo-loader-scale);
		--mo-hdr-bloom: 1ms;
		--mo-hdr-bloom-delay: 0ms;
	}

	.tk-loader.is-drawing .tk-loader__svg path {
		animation: none;
		stroke-dashoffset: 0;
	}

	.tk-loader__mark,
	.tk-loader.is-drawing .tk-loader__mark {
		transform: scale(var(--mo-loader-scale));
		transition: none;
	}

	.tk-loader__svg path {
		fill-opacity: 1;
		stroke-opacity: 0;
	}

	[data-reveal] {
		transform: none;
	}

	.marquee,
	.v-marquee,
	.sleeper-hero {
		animation: none !important;
	}

	html {
		scroll-behavior: auto;
	}
}
