/* =============================================================================
   KineticHub Scroll to Top - KineticPowers

   A KineticPower is decorative choreography that follows the activation
   lifecycle. It never scrolls anything: something sets a phase attribute on the
   host element and these rules do the rest.

   This file is loaded on the front end only when a power is actually selected,
   and on the settings screen so the live preview can play the real sequence
   rather than an imitation of it. That is why every selector keys off a
   namespaced attribute instead of a context-specific class: the host is #khstt
   on the front end and the preview control in the admin, and neither knows
   about the other.

     data-khstt-power         which power is selected
     data-khstt-power-i       amplitude: subtle, balanced (default) or strong
     data-khstt-power-state   the running phase, written while a sequence plays

   The host must also carry --khstt-size and --khstt-icon, which everything
   here scales from, so a 44px control and an 80px control get the same
   choreography at their own scale.

   Phases, and the durations the script schedules against:
     launch   ignition and recoil          140ms
     travel   the rise, then a held state  420ms
     arrival  the spark, then reset        300ms

   Every animation runs exactly once. Nothing here loops, so the control never
   carries continuous motion (WCAG 2.2.2).

   Everything is drawn over the control's own background, and the flight is
   sized to finish inside it. That is a contrast decision as much as a
   compositional one: these layers paint in currentColor, which is chosen to
   read against the button, and nothing whatsoever guarantees it reads against
   what the page happens to put behind the button. An earlier draft let the
   rocket fly clear of the control, and a white rocket leaving a dark button
   onto a white page simply disappeared.

   Amplitude, set per intensity. Each is a fraction of the button size unless
   it says otherwise, so 44px and 80px controls get the same choreography at
   their own scale:

     --khstt-p-recoil  dip before the rise, in pixels
     --khstt-p-rise    how far the rocket climbs
     --khstt-p-tl      trail length
     --khstt-p-trail   trail and spark strength, as an opacity
     --khstt-p-glow    glow radius
     --khstt-p-tw      trail width
   ============================================================================= */

[data-khstt-power] {
	--khstt-p-recoil: 2.5px;
	--khstt-p-rise: .4;
	--khstt-p-tl: .58;
	--khstt-p-trail: .55;
	--khstt-p-glow: .07;
	--khstt-p-tw: max(2px, calc(var(--khstt-size) * .05));
}

[data-khstt-power][data-khstt-power-i="subtle"] {
	--khstt-p-recoil: 1.5px;
	--khstt-p-rise: .3;
	--khstt-p-tl: .46;
	--khstt-p-trail: .38;
	--khstt-p-glow: .04;
}

[data-khstt-power][data-khstt-power-i="strong"] {
	--khstt-p-recoil: 3.5px;
	--khstt-p-rise: .52;
	--khstt-p-tl: .7;
	--khstt-p-trail: .72;
	--khstt-p-glow: .1;
}

/* -----------------------------------------------------------------------------
   The stage

   Absolutely positioned and inert: it takes part in no layout and no hit
   testing, so it cannot be measured as part of the progress ring's box and
   cannot become a target. It paints only while a phase is running, so an idle
   control carries no extra compositing work at all.
   -------------------------------------------------------------------------- */

.khstt__power {
	position: absolute;
	inset: 0;
	display: none;
	pointer-events: none;
}

[data-khstt-power-state] .khstt__power {
	display: block;
}

/* The resting layer - whatever the control shows when nothing is happening -
   steps aside for the sequence and fades back on arrival. Both the front-end
   control and the preview mark theirs with this class, so the handoff is
   written once. */
.khstt-p-rest {
	transition: opacity .14s ease;
}

[data-khstt-power-state="launch"] .khstt-p-rest,
[data-khstt-power-state="travel"] .khstt-p-rest {
	opacity: 0;
}

/* -----------------------------------------------------------------------------
   Rocket Boost
   -------------------------------------------------------------------------- */

.khstt__rocket {
	position: absolute;
	inset: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	opacity: 0;
}

.khstt__rocket-glyph {
	display: block;
	width: var(--khstt-icon);
	height: var(--khstt-icon);
}

/* The trail hangs from the centre of the control and stretches as the rocket
   climbs. currentColor means it takes the icon colour, including the hover
   colour, with no setting of its own. */
.khstt__trail {
	position: absolute;
	top: 50%;
	left: calc(50% - var(--khstt-p-tw) / 2);
	width: var(--khstt-p-tw);
	height: calc(var(--khstt-size) * var(--khstt-p-tl));
	background: linear-gradient(to top, transparent, currentColor);
	border-radius: 999px;
	opacity: 0;
	transform-origin: 50% 0;
}

.khstt__spark {
	position: absolute;
	top: 50%;
	left: 50%;
	width: calc(var(--khstt-size) * .36);
	height: calc(var(--khstt-size) * .36);
	margin: calc(var(--khstt-size) * -.18) 0 0 calc(var(--khstt-size) * -.18);
	border: 2px solid currentColor;
	border-radius: 50%;
	opacity: 0;
}

/* Two fixed marks rather than a particle system: the whole arrival accent is
   three painted shapes. They fly outward past the ring, because inside it they
   read as part of the glyph coming back rather than as sparks leaving. */
.khstt__spark::before,
.khstt__spark::after {
	content: "";
	position: absolute;
	bottom: 50%;
	left: 50%;
	width: 2px;
	height: 22%;
	margin-left: -1px;
	background: currentColor;
	border-radius: 999px;
	transform-origin: 50% 100%;
	opacity: 0;
}

.khstt__spark::before {
	--khstt-p-tick: -48deg;
}

.khstt__spark::after {
	--khstt-p-tick: 48deg;
}

/* ---- Phase 1: launch ---- */

[data-khstt-power-state="launch"] .khstt__power {
	animation: khstt-power-recoil 140ms ease-out 1 both;
}

[data-khstt-power-state="launch"] .khstt__rocket {
	animation: khstt-rocket-launch 140ms cubic-bezier(.2, .8, .3, 1) 1 both;
}

[data-khstt-power-state="launch"] .khstt__trail {
	animation: khstt-trail-ignite 140ms ease-out 1 both;
}

/* ---- Phase 2: travel ---- */

[data-khstt-power-state="travel"] .khstt__rocket {
	animation: khstt-rocket-travel 420ms cubic-bezier(.35, 0, .2, 1) 1 both;
}

[data-khstt-power-state="travel"] .khstt__trail {
	animation: khstt-trail-travel 420ms cubic-bezier(.35, 0, .2, 1) 1 both;
}

/* The glow is the one filter in the sequence and it exists only while the
   rocket is on screen. */
[data-khstt-power-state="launch"] .khstt__rocket,
[data-khstt-power-state="travel"] .khstt__rocket {
	filter: drop-shadow(0 0 calc(var(--khstt-size) * var(--khstt-p-glow)) currentColor);
}

/* Scoped to the phases that actually move, so nothing is promoted at rest. */
[data-khstt-power-state="launch"] .khstt__rocket,
[data-khstt-power-state="launch"] .khstt__trail,
[data-khstt-power-state="travel"] .khstt__rocket,
[data-khstt-power-state="travel"] .khstt__trail {
	will-change: transform, opacity;
}

/* ---- Phase 3: arrival ---- */

[data-khstt-power-state="arrival"] .khstt__spark {
	animation: khstt-spark 300ms ease-out 1 both;
}

/* Delayed a frame or two so the ring leads and the marks follow it out. */
[data-khstt-power-state="arrival"] .khstt__spark::before,
[data-khstt-power-state="arrival"] .khstt__spark::after {
	animation: khstt-spark-tick 300ms ease-out 40ms 1 both;
}

/* -----------------------------------------------------------------------------
   Keyframes
   -------------------------------------------------------------------------- */

@keyframes khstt-power-recoil {
	0% {
		transform: translateY(0);
	}

	45% {
		transform: translateY(var(--khstt-p-recoil));
	}

	100% {
		transform: translateY(0);
	}
}

@keyframes khstt-rocket-launch {
	0% {
		opacity: 0;
		transform: translateY(var(--khstt-p-recoil)) scale(.7);
	}

	55% {
		opacity: 1;
		transform: translateY(calc(var(--khstt-p-recoil) * .5)) scale(1.08);
	}

	100% {
		opacity: 1;
		transform: translateY(0) scale(1);
	}
}

/* The rocket is gone by 80%, and the last fifth of the phase is the held state
   the script sits in until the scroll reports its arrival. */
@keyframes khstt-rocket-travel {
	0% {
		opacity: 1;
		transform: translateY(0) scale(1);
	}

	45% {
		opacity: .92;
	}

	80% {
		opacity: 0;
		transform: translateY(calc(var(--khstt-size) * var(--khstt-p-rise) * -.86)) scale(.8);
	}

	100% {
		opacity: 0;
		transform: translateY(calc(var(--khstt-size) * var(--khstt-p-rise) * -1)) scale(.76);
	}
}

@keyframes khstt-trail-ignite {
	0% {
		opacity: 0;
		transform: translateY(0) scaleY(.1);
	}

	100% {
		opacity: var(--khstt-p-trail);
		transform: translateY(0) scaleY(.32);
	}
}

@keyframes khstt-trail-travel {
	0% {
		opacity: var(--khstt-p-trail);
		transform: translateY(0) scaleY(.32);
	}

	40% {
		opacity: var(--khstt-p-trail);
		transform: translateY(calc(var(--khstt-size) * var(--khstt-p-rise) * -.5)) scaleY(1);
	}

	80% {
		opacity: 0;
		transform: translateY(calc(var(--khstt-size) * var(--khstt-p-rise) * -1)) scaleY(.35);
	}

	100% {
		opacity: 0;
		transform: translateY(calc(var(--khstt-size) * var(--khstt-p-rise) * -1)) scaleY(.2);
	}
}

@keyframes khstt-spark {
	0% {
		opacity: 0;
		transform: scale(.15);
	}

	28% {
		opacity: var(--khstt-p-trail);
		transform: scale(.7);
	}

	100% {
		opacity: 0;
		transform: scale(1.3);
	}
}

@keyframes khstt-spark-tick {
	0% {
		opacity: 0;
		transform: rotate(var(--khstt-p-tick)) scaleY(.4) translateY(calc(var(--khstt-size) * -.1));
	}

	35% {
		opacity: var(--khstt-p-trail);
		transform: rotate(var(--khstt-p-tick)) scaleY(1) translateY(calc(var(--khstt-size) * -.2));
	}

	100% {
		opacity: 0;
		transform: rotate(var(--khstt-p-tick)) scaleY(.5) translateY(calc(var(--khstt-size) * -.32));
	}
}

/* -----------------------------------------------------------------------------
   Reduced motion

   A KineticPower does not run at all. The script already declines to start a
   sequence, and this makes the stage unpaintable even if a phase attribute
   were somehow set.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
	[data-khstt-power-state] .khstt__power {
		display: none;
	}

	.khstt__power,
	.khstt__rocket,
	.khstt__trail,
	.khstt__spark,
	.khstt__spark::before,
	.khstt__spark::after {
		animation: none;
	}

	.khstt-p-rest {
		opacity: 1;
		transition: none;
	}
}
