/* ==========================================================================
 * QynTro AI - Hero Atmosphere
 *
 * Replaces the hero's static backdrop with a generative flow field: particles
 * streaming along a vector field, curling around the cursor, leaving trails.
 *
 * Mounts inside `.qp-has-bg-video` (the hero background container - the class
 * name is misleading, there is no video in it) and hides `.qp-bg-poster`, the
 * Wistia-hosted still it was showing. `.qp-bg-scrim` is deliberately KEPT: it
 * is what holds the headline legible, and the field is brightened to sit under
 * it rather than the scrim being removed.
 *
 * Loaded from the admin header-code box alongside the model bands.
 *
 * ADDITIVE. Everything is .qfx-prefixed. The poster is hidden only via
 * `.qfx-on`, a class the JS adds after the canvas has actually mounted - if
 * anything fails, the original backdrop is still there.
 *
 * DEPLOY ORDER MATTERS: upload FIRST, then bump ?v=. 7-day cache.
 * ========================================================================== */

/* !important on position because this layer is injected into markup that is not
   ours. A single `.qp-has-bg-video > *` rule in the theme - same specificity,
   declared later - would flatten it to static, collapse it to zero height and
   render nothing, with no error anywhere. Learned the hard way in preview. */
.qfx {
	position: absolute !important;
	inset: 0;
	overflow: hidden;
	pointer-events: none;
	z-index: 0;
}

/* Only once the canvas is really up.
 *
 * BOTH layers, and this is the bit that was missed first time round:
 * qyntro-premium.js sets QP_BG_VIDEO (a Wistia .bin) and builds `.qp-bg-poster`
 * AND `.qp-bg-video`. Hiding only the poster left the video playing underneath,
 * which is exactly what "video still there" meant.
 *
 * The video also MOUNTS LATE - it is not in the DOM when this script first
 * runs - so the JS keeps watching for it and neutralises it as well; this rule
 * is only the visual half. */
.qfx-on .qp-bg-poster,
.qfx-on .qp-bg-video { display: none !important; }

/* Aurora underlay. Carries the colour ground now that the photo is gone, so
   these are brighter than they would be as a mere accent. */
.qfx-blob {
	position: absolute;
	border-radius: 50%;
	filter: blur(100px);
	will-change: transform;
}

.qfx-b1 {
	width: 46vw; height: 46vw; left: -10vw; top: -14vw;
	background: rgba(203,255,60,.20);
	animation: qfx-d1 52s ease-in-out infinite;
}
.qfx-b2 {
	width: 54vw; height: 54vw; right: -14vw; top: 0;
	background: rgba(79,209,255,.17);
	animation: qfx-d2 67s ease-in-out infinite;
}
.qfx-b3 {
	width: 40vw; height: 40vw; left: 28vw; bottom: -18vw;
	background: rgba(139,124,255,.15);
	animation: qfx-d3 45s ease-in-out infinite;
}

/* Three different cycle lengths, none a multiple of another, so the
   arrangement never visibly repeats. */
@keyframes qfx-d1 {
	0%,100% { transform: translate3d(0,0,0) scale(1); }
	33%     { transform: translate3d(14vw,7vw,0) scale(1.16); }
	66%     { transform: translate3d(5vw,-5vw,0) scale(.92); }
}
@keyframes qfx-d2 {
	0%,100% { transform: translate3d(0,0,0) scale(1); }
	40%     { transform: translate3d(-16vw,9vw,0) scale(.88); }
	75%     { transform: translate3d(-6vw,-6vw,0) scale(1.18); }
}
@keyframes qfx-d3 {
	0%,100% { transform: translate3d(0,0,0) scale(1); }
	50%     { transform: translate3d(-11vw,-10vw,0) scale(1.22); }
}

.qfx-canvas {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	display: block;
}

/* Legibility mask. The field is at its busiest exactly where the headline sits,
   and without this the type reads grey instead of white - the background wins a
   fight it should not be in. Darkest in the middle, clear at the edges, so the
   flow still shows around the type rather than being uniformly dimmed. */
.qfx-focus {
	position: absolute;
	inset: 0;
	pointer-events: none;
	background:
		radial-gradient(58% 46% at 50% 46%, rgba(15,17,19,.78) 0%, rgba(15,17,19,.55) 45%, rgba(15,17,19,0) 78%),
		linear-gradient(180deg, rgba(15,17,19,.35) 0%, rgba(15,17,19,0) 22%, rgba(15,17,19,0) 72%, rgba(15,17,19,.45) 100%);
}

/* Large soft gradients band badly on dark screens; the grain is what stops the
   whole thing reading as flat CSS. */
.qfx-grain {
	position: absolute;
	inset: 0;
	opacity: .03;
	mix-blend-mode: overlay;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
	background-size: 140px 140px;
}

@media (prefers-reduced-motion: reduce) {
	/* The aurora alone still reads as a designed background, so the poster stays
	   hidden - only the moving parts stop. */
	.qfx-blob { animation: none; }
	.qfx-canvas { display: none; }
}

/* ==========================================================================
 * SCROLL-FLICKER FIX (added 2026-08-19)
 *
 * Symptom: the whole hero occasionally painted solid black for a frame or two
 * WHILE SCROLLING - text and all. Cause: the hero background is a live,
 * continuously-repainting <canvas> plus several decorative orb/blob layers,
 * and those layers carried a PERMANENT `will-change: transform`. Permanent
 * will-change pins many elements onto their own GPU layers indefinitely
 * (a documented anti-pattern), and on scroll Chrome sometimes fails to
 * repaint the big promoted hero layer in step with the page, exposing its
 * black backing over the composited text.
 *
 * Fix, CSS-only and reversible: (1) drop the permanent promotion on the
 * decorative layers - their CSS animations still run fine without it; (2) pin
 * a single, stable composited layer on the hero itself with a solid painted
 * backing so a mid-scroll repaint can never expose black; (3) backface-hidden,
 * the classic Chrome flicker cure, on the animated background layers.
 * ========================================================================== */

/* 1 - stop permanently promoting the many decorative layers */
.frontend-body .qp-orb,
.frontend-body .qfx-blob {
	will-change: auto !important;
}

/* 2 - one stable, painted hero layer that cannot flash its backing */
.frontend-body #main #main-background {
	isolation: isolate;                 /* self-contained stacking context */
	-webkit-backface-visibility: hidden;
	backface-visibility: hidden;
	background-color: var(--qp-bg, #0f1113); /* guaranteed solid paint under everything */
}

/* 3 - keep the animated background layers stable during scroll repaints */
.frontend-body #main-background .qfx,
.frontend-body #main-background .qfx-canvas,
.frontend-body #main-background .qfx-blob,
.frontend-body #main-background .qp-orb {
	-webkit-backface-visibility: hidden;
	backface-visibility: hidden;
}

/* ==========================================================================
 * BLUE-FLASH-ON-FAST-SCROLL FIX (added 2026-08-19)
 *
 * Symptom (Chrome, fast scroll only): a blue band flashed across the page.
 * Cause: the page's own content wrapper, `.side-app.frontend-background`,
 * carries Bootstrap's default primary blue (#007bff) as its background and is
 * collapsed to width:0, so it is normally invisible - but that layer is the
 * FULL page height (~28,000px), and on a fast-scroll repaint Chrome briefly
 * paints its blue backing across the viewport before clipping it back to zero
 * width. The wrapper's background is meant to be the dark theme, not blue.
 *
 * Fix: pin the wrapper's background to the dark theme colour, so even if the
 * compositor flashes that layer it is indistinguishable from the page.
 * ========================================================================== */
.frontend-body .side-app,
.frontend-body .side-app.frontend-background {
	background-color: var(--qp-bg, #0f1113) !important;
}
