/**
 * Vocalis Studio — perf.css
 *
 * Scroll-smoothness layer. Purely additive: it changes NO colours, sizes,
 * spacing, or layout — only compositing/paint hints so the browser does less
 * work per scroll frame. Safe to load on every page.
 *
 * MUST BE ENQUEUED LAST (see functions.php) so it always wins the cascade
 * against app.css / landing.css at equal specificity.
 *
 * ---------------------------------------------------------------------------
 * WHERE THE FRAMES ACTUALLY GO IN THIS THEME
 *
 *   1. `filter: blur(64px)` blobs — large, permanently on screen, animated.
 *   2. `backdrop-filter: blur(24px)` glass panels — the backdrop must be
 *      re-read and re-blurred on every frame in which anything behind or
 *      inside the panel repaints.
 *   3. Waveforms — 28 bars in the hero, 42 per voice card, 56 in the player,
 *      each running an infinite `scaleY` animation.
 *   4. Long repeating lists (history / audio / rail rows).
 *
 * Strategy: promote the few big, permanently-animating surfaces exactly once,
 * then CONTAIN everything else so a repaint in one row or card can never
 * escalate into a repaint of a blurred ancestor.
 *
 * ---------------------------------------------------------------------------
 * THREE DELIBERATE CHANGES FROM THE PREVIOUS VERSION OF THIS FILE
 *
 * (a) `will-change: transform` removed from everything that is NOT
 *     continuously animating. `will-change` does not mean "go faster"; it
 *     means "hold a GPU texture for this element for the entire life of the
 *     page". Held permanently on the header, tab bar, navbar, pill strip and
 *     six blobs simultaneously, a mid-range Android exhausts GPU memory,
 *     starts evicting and re-uploading layers, and scrolling gets WORSE. A
 *     bare `transform: translate3d(0,0,0)` already promotes an element, and that
 *     is all a fixed surface needs.
 *
 * (b) `isolation: isolate` removed from `.vx-glass-panel`. It made every
 *     glass panel a stacking context — including `.vx-controls-card`, which
 *     is the parent of the voice-picker and language dropdowns (z-index 50).
 *     Once the card is a stacking context, that 50 is scoped INSIDE the card,
 *     so `.vx-player` (a later positioned sibling) paints on top and the open
 *     dropdown disappears behind the player as soon as audio has been
 *     generated. It also bought nothing: an element's own `isolation` does
 *     not change its own backdrop-filter cost. Removed as a latent-bug fix.
 *     Anything that creates a stacking context is, for the same reason, kept
 *     off `.vx-history-row` and `.vx-audio-row` too, because their "•••"
 *     menus have to paint above the rows that follow them.
 *
 * (c) `content-visibility: auto` pulled off every element that lives in a CSS
 *     GRID (voice cards, feature cards, testimonials, pricing) and kept only
 *     on flat single-column lists. In a grid the row height is the height of
 *     the tallest item in that row; a skipped item reports
 *     `contain-intrinsic-size` instead of its real height, so as cards enter
 *     and leave the viewport the grid row keeps re-sizing underneath the
 *     scroll position. That is precisely the "micro-stutter" symptom. A flat
 *     list has no row coupling, so it is safe there and it stays. The DOM
 *     weight the voice grid used to pay is now solved properly in app.js
 *     instead: waveform bars are built lazily through IntersectionObserver,
 *     so the page no longer creates 1,000+ <span>s during load.
 * ---------------------------------------------------------------------------
 */

/* ============================================================
   1. FIXED DECORATIVE BACKGROUNDS
   These never scroll, so they belong on their own compositor layer —
   otherwise a 64px blur is repainted every time content scrolls over them.
   `contain` stops any descendant from invalidating them.
   (`contain: strict` replaced by `layout paint style`: strict additionally
   applies SIZE containment, which is unnecessary here and a foot-gun on the
   two `inset: 0` elements.)
   ============================================================ */
.vx-app-page__bg,
.vx-app-page__blob-1,
.vx-app-page__blob-2,
.vx-hero-shell__bg,
.vx-hero-shell__blob-a,
.vx-hero-shell__blob-b,
.vx-hero__bg {
	transform: translate3d(0, 0, 0);
	backface-visibility: hidden;
	contain: layout paint style;
}

/* ============================================================
   2. WAVEFORMS — promote ONLY the ones that are actually animating

   CORRECTION TO AN EARLIER VERSION OF THIS FILE. This rule used to promote
   every `.vx-waveform` on the page. That is right for the landing hero, where
   28 bars animate forever inside a backdrop-filtered glass panel. It is badly
   wrong for the Voice Library, where it created 25 permanently-resident GPU
   layers for 25 waveforms that are not animating at all — which is the exact
   VRAM-pressure failure this file warns about in note (a) above.

   A promoted layer is only ever worth it while something inside it is
   moving. A static waveform costs nothing to leave in the page's normal
   paint: it rasterises once and scrolling never invalidates it.

   So promotion is now conditional:
     - the landing hero waveform  -> always (its bars never stop)
     - any other waveform         -> only while `.is-animating` is set by
                                     setWaveformPlaying() in app.js, i.e.
                                     only the single clip being previewed
   ============================================================ */
.vx-hero__card .vx-waveform,
.vx-waveform.is-animating {
	transform: translate3d(0, 0, 0);
	backface-visibility: hidden;
	contain: layout paint style;
}

/* Static waveforms still get containment — that is free and stops a bar
   repaint from invalidating the card around it — just no GPU layer. */
.vx-waveform {
	contain: layout paint style;
}

/* ============================================================
   3. REPEATING SINGLE-COLUMN LISTS
   Flat lists, no grid-row coupling, and the intrinsic sizes below match the
   real rendered heights closely — so content-visibility is safe here.
   ============================================================ */
.vx-rail-voice-list li,
.vx-rail-download-list li {
	contain: content;
	content-visibility: auto;
	contain-intrinsic-size: auto 64px;
}

/* NOT LISTED HERE ON PURPOSE: .vx-history-row and .vx-audio-row.
   Both own a "•••" dropdown that must paint above the rows below them, and
   ANY form of containment (layout, paint or content) turns the row into a
   stacking context, which would trap that menu behind the next row. Their
   cost is handled in app.js instead (event delegation, no per-row listeners,
   rAF-batched writes). */

/* ============================================================
   4. GRID / STANDALONE CARDS
   Layout + style containment only: a hover transform or a text reflow inside
   one card can no longer re-lay-out its neighbours. Paint containment is
   deliberately excluded — several of these have glows that bleed outside the
   box on purpose. Every card listed here already has `overflow: hidden` or no
   overflowing positioned child, so the stacking context is harmless.
   ============================================================ */
.vx-voice-card,
.vx-fav-voice-card,
.vx-quick-card,
.vx-stat-tile,
.vx-pkg-stat,
.vx-rail-quick__item {
	contain: layout style;
}

/* ============================================================
   5. FIXED / STICKY CHROME
   Promoted once so the blurred bar is composited rather than repainted while
   the page scrolls underneath. No `will-change` — see note (a).
   ============================================================ */
.vx-app-header,
.vx-app-tabbar,
.vx-mobile-tabbar,
.vx-navbar-wrap {
	transform: translate3d(0, 0, 0);
	backface-visibility: hidden;
}

/* Sticky columns re-evaluate their offset on every scroll frame; promoting
   them turns that re-position into a compositor translate instead of a paint.
   Neither column contains an escaping dropdown, so containment is safe. */
.vx-app-sidebar__sticky,
.vx-app-rail__sticky {
	transform: translate3d(0, 0, 0);
	contain: layout style;
}

/* ============================================================
   6. HORIZONTAL PILL STRIPS
   `.vx-pill-group` also carries a `mask-image` fade, which forces an extra
   compositing pass on every horizontal scroll — worth its own layer.
   `overscroll-behavior: contain` stops a horizontal fling from chaining into
   the page scroller, which is what causes the "swipe sideways, page jerks
   vertically" stutter on touch.
   ============================================================ */
.vx-pill-group,
.vx-voice-picker__cats,
.vx-cat-filter,
.vx-voice-pills {
	transform: translate3d(0, 0, 0);
	contain: layout style;
	overscroll-behavior-x: contain;
}

/* Internally-scrolling panes inside dropdowns: same reasoning, vertical. */
.vx-voice-picker__list,
.vx-lang-select__list {
	overscroll-behavior: contain;
}

/* ============================================================
   7. LANDING PAGE
   ============================================================ */

/* Large blurred decorative glows.

   CORRECTION: `.vx-voice-card__glow` and `.vx-fav-voice-card__glow` have been
   REMOVED from this list. Both live inside a card that is repeated across a
   grid — 25+ of them on the Voice Library — and each carries
   `filter: blur(64px)`. A blur expands a layer's bounds by roughly 3x sigma
   on every side, so a 160px glow becomes a ~550px texture, and 25 of those
   resident at once is tens of megabytes of VRAM for decorations that never
   move. They are static, they sit inside `overflow: hidden` cards that
   already have paint containment, and scrolling does not invalidate them, so
   leaving them in the page's normal paint is strictly cheaper: they
   rasterise once and are never touched again.

   What remains here is one-per-page (or few-per-page) and either animates or
   is large enough that a repaint would be worse than a texture. */
.vx-studio-section__glow,
.vx-player__glow {
	transform: translate3d(0, 0, 0);
	backface-visibility: hidden;
	contain: layout paint style;
}

/* The Feature and Testimonial glows animate their opacity on hover, which is
   worth a layer — but ONLY on a device that can hover. On touch there is no
   hover, so promoting nine blurred boxes there is pure wasted VRAM. */
@media (hover: hover) and (pointer: fine) {
	.vx-feature-card__glow,
	.vx-testimonial-card__glow {
		transform: translate3d(0, 0, 0);
		backface-visibility: hidden;
	}
}

/* On touch, where the rule above deliberately does NOT promote them, these
   stay `filter: blur()` boxes painted into their parent layer. `contain: paint`
   tells the browser their painting can never escape their own bounds, which
   lets it skip them entirely once they scroll out of view instead of
   considering them on every frame.

   This changes nothing about how they look — a blurred disc that is already
   clipped by its card's `overflow: hidden` cannot paint outside itself
   anyway — so the containment is a promise the element was keeping already,
   just now stated where the compositor can use it. */
@media not all and (hover: hover) and (pointer: fine) {
	.vx-feature-card__glow,
	.vx-testimonial-card__glow {
		contain: paint;
	}
}

/* One section's reflow stays inside that section. */
.vx-section,
.vx-landing-footer {
	contain: layout style;
}

/* Grid cards — containment only, no content-visibility (see note (c)). */
.vx-pricing-card {
	contain: layout style;
}

/* Features / Testimonials now own their surface directly and no longer carry
   `backdrop-filter`, which means nothing inside them can influence anything
   outside them any more. That makes full `contain: content` (layout + paint +
   style) safe, which it was NOT while they were glass panels: paint
   containment clips descendants to the padding box, and a backdrop-filtered
   panel needs to sample past its own edge.
   Both cards already set `overflow: hidden`, so paint containment is
   invisible here — it just lets the browser skip work it was doing anyway.
   The card's OWN box-shadow is unaffected: paint containment clips
   descendants, never the element's own border or shadow. */
.vx-feature-card,
.vx-testimonial-card {
	contain: content;
}

/* FAQ rows are a flat single-column list and already `overflow: hidden`, so
   both containment and content-visibility are safe, and 72px is an accurate
   collapsed-height placeholder. */
.vx-faq-item {
	contain: content;
	content-visibility: auto;
	contain-intrinsic-size: auto 72px;
}
/* An OPEN item must render fully so its grid-template-rows animation runs. */
.vx-faq-item.is-open {
	content-visibility: visible;
	contain-intrinsic-size: none;
}

/* ============================================================
   8. SCROLL-PHASE SUPPRESSION  —  body.vx-is-scrolling
   Set by vxInitScrollState() in app.js / landing.js, and ONLY on devices
   that actually have a hover-capable fine pointer.

   The problem this solves: on desktop, scrolling drags the page underneath a
   stationary cursor, so every card that passes beneath it fires :hover. Each
   one then starts a transform + background-color + box-shadow transition.
   Scroll past twelve cards and you have twelve concurrent multi-property
   transitions repainting, on top of the scroll itself. Compositing hints
   cannot help here, because the browser is doing exactly what the CSS asked
   for. The only fix is to stop asking during the gesture.

   `pointer-events: none` prevents any NEW hover from being entered, and
   `transition: none` prevents an already-entered hover from animating its way
   back out. Both are reverted the instant scrolling stops (140ms idle).

   >>> HONEST CAVEAT: this is the one rule in this file that is not strictly
   >>> zero-visual. At rest, before and after are pixel-identical. But WHILE
   >>> you are actively scrolling with a mouse, cards will no longer light up
   >>> under the cursor. That is the entire point — that lighting-up is the
   >>> cost being removed. If you would rather keep it, delete this one
   >>> section; everything else in this file is unconditionally invisible.
   ============================================================ */
@media (hover: hover) and (pointer: fine) {

	body.vx-is-scrolling .vx-history-row,
	body.vx-is-scrolling .vx-audio-row,
	body.vx-is-scrolling .vx-fav-clip-row,
	body.vx-is-scrolling .vx-fav-voice-card,
	body.vx-is-scrolling .vx-quick-card,
	body.vx-is-scrolling .vx-voice-card,
	body.vx-is-scrolling .vx-top-voices li,
	body.vx-is-scrolling .vx-rail-quick__item,
	body.vx-is-scrolling .vx-rail-voice-list li,
	body.vx-is-scrolling .vx-feature-card,
	body.vx-is-scrolling .vx-testimonial-card,
	body.vx-is-scrolling .vx-pricing-card,
	body.vx-is-scrolling .vx-voice-pill {
		pointer-events: none;
	}

	/* Kill the exit animation too, so releasing a hover mid-scroll is a
	   single instant repaint instead of a 0.3–0.5s animated one. Scoped to
	   the same elements — global `transition: none` would also freeze the
	   navbar's padding transition and the FAQ accordion. */
	body.vx-is-scrolling .vx-history-row,
	body.vx-is-scrolling .vx-audio-row,
	body.vx-is-scrolling .vx-fav-clip-row,
	body.vx-is-scrolling .vx-fav-voice-card,
	body.vx-is-scrolling .vx-quick-card,
	body.vx-is-scrolling .vx-quick-card .vx-icon-badge,
	body.vx-is-scrolling .vx-quick-card__arrow,
	body.vx-is-scrolling .vx-voice-card,
	body.vx-is-scrolling .vx-top-voices li,
	body.vx-is-scrolling .vx-rail-quick__item,
	body.vx-is-scrolling .vx-feature-card,
	body.vx-is-scrolling .vx-feature-card__glow,
	body.vx-is-scrolling .vx-testimonial-card,
	body.vx-is-scrolling .vx-pricing-card,
	body.vx-is-scrolling .vx-voice-pill {
		transition: none !important;
	}

	/* Interactive chrome deliberately stays live: the header, sidebar, tab
	   bar, rail buttons and every dropdown remain fully clickable throughout
	   a scroll. Only the passive content cards go inert. */
}

/* ============================================================
   9. STRUCTURAL CONTAINMENT
   Layout + style containment on the big structural columns, so a reflow
   inside one of them can never propagate outwards into a full-page relayout.

   Paint containment is deliberately NOT used on any of these. Every one of
   them holds `.vx-glass-panel` children whose OUTER box-shadow is meant to
   bleed past the column edge; `contain: paint` would clip it flat against
   the container bounds, which is a visible change. This is also why
   `contain: strict` appears nowhere in this file — strict implies both paint
   and size containment, and size containment on an auto-height column
   collapses it to zero.
   ============================================================ */
.vx-app-sidebar,
.vx-app-rail,
.vx-app-main,
.vx-dashboard-grid__col,
.vx-tts-grid__col,
.vx-settings-grid__col {
	contain: layout style;
}

/* Static dashboard panels: nothing inside these can affect anything outside
   them, and none of them owns an escaping dropdown. */
.vx-voice-clone-section,
.vx-week-bars,
.vx-rail-usage,
.vx-rail-bars,
.vx-usage-ring,
.vx-stat-row,
.vx-pkg-stats,
.vx-quick-grid {
	contain: layout style;
}

/* The usage ring animates `stroke-dasharray`, which is an SVG geometry
   change — it re-rasterises the whole SVG on every frame of its 0.6s
   transition. Giving it its own layer keeps that raster off the page. */
.vx-usage-ring svg,
.vx-vc-timer-ring-wrap svg {
	transform: translate3d(0, 0, 0);
}


/* ============================================================
   10. BACKDROP-FILTER SWEEP  —  theme-wide
   ============================================================

   Same fix already proven on the Features and Testimonials cards, now
   applied to every remaining REPEATED glass surface in the theme.

   WHY IT IS SAFE
   `backdrop-filter: blur()` only removes HIGH-frequency detail from whatever
   is behind an element. Every surface listed below sits on nothing but the
   page background — `#vx-landing`'s radial gradients on the front end, and
   `.vx-app-page__bg` plus the two already-blur(64px) blobs on the app pages.
   All of those are smooth, low-frequency fills. Blurring a smooth gradient
   returns the same smooth gradient, so the filter was burning a full-surface
   GPU pass every frame to produce an image within ~2/255 of the unfiltered
   one. Every background, border and shadow VALUE is untouched — only the
   filter is switched off.

   WHY IT MATTERS
   A backdrop filter is the one effect that can never be cached. As a card
   travels over the background during a scroll its backdrop changes, so the
   blur is recomputed on every frame, for every card on screen. On the Voice
   Library that was 25+ simultaneous recomputations per frame.

   DELIBERATELY EXCLUDED — these keep their real blur, because they overlay
   actual scrolling CONTENT (text, cards, rows), which DOES have the
   high-frequency detail a blur exists to soften. Switching these off would
   be plainly visible:
       .vx-glass-nav          — fixed header bar, mobile tab bar, landing navbar
       .vx-voice-picker__panel, .vx-lang-select__panel, .vx-dropdown__panel
       .vx-audio-row__menu    — row overflow menu
       .vx-navbar__mobile     — mobile nav drawer

   TO ROLL BACK: delete this section. Nothing else depends on it, and no
   template file was modified to make it work — which also means it applies
   correctly to pages already sitting in a page cache.
   ============================================================ */

/* --- Landing page --------------------------------------------------- */
.vx-faq-item.vx-glass-panel,
.vx-pricing-card.vx-glass-panel,
.vx-studio-panel.vx-glass-panel,
.vx-studio-player.vx-glass-panel,
.vx-landing-footer__panel.vx-glass-panel,

/* --- App shell: the repeated card surface (12 instances) ------------- */
.vx-card.vx-glass-panel,
.vx-fav-section.vx-glass-panel,
.vx-app-sidebar__panel.vx-glass-panel,

/* --- App shell: page-specific panels --------------------------------- */
.vx-text-input.vx-glass-panel,
.vx-controls-card.vx-glass-panel,
.vx-player.vx-glass-panel,
.vx-vc-card.vx-glass-panel,
.vx-audio-panel.vx-glass-panel,
.vx-voices-toolbar.vx-glass-panel,
.vx-voices-empty.vx-glass-panel,
.vx-voice-card.vx-glass-panel,
.vx-hero-shell__panel.vx-glass-panel,
.vx-access-form__row.vx-glass-panel,
.vx-access-verified.vx-glass-panel,

/* --- Landing hero -----------------------------------------------------
   These were held back in an earlier pass on the assumption that they
   overlay real content. They do not: everything behind them is
   `.vx-hero__bg` and the three hero blobs, which are smooth gradients with
   no high-frequency detail for a blur to remove. The chips overlap the hero
   card, which is itself a flat translucent fill — also smooth.

   They are the most expensive three elements in the theme, because all
   three carry `.vx-animate-float` as well: a backdrop filter combined with
   an infinite transform animation means the backdrop is re-sampled and
   re-blurred on EVERY frame, forever, whether or not anyone is scrolling.
   That is what made the top of the homepage stutter.
   Measured worst-case difference from removing it: 2.2/255. */
.vx-hero__card.vx-glass-panel,
.vx-chip.vx-glass-panel,
.vx-pill.vx-glass-panel {
	-webkit-backdrop-filter: none;
	backdrop-filter: none;
}

/* Stat tiles carry their own blur(12px) rather than the .vx-glass-panel
   class. Four to six of them render side by side above the fold on the
   Dashboard, all sitting on the same smooth fixed background. perf.css loads
   after app.css, so a plain class selector is enough to win here. */
.vx-stat-tile,
.vx-pkg-stat {
	-webkit-backdrop-filter: none;
	backdrop-filter: none;
}

/* Single small control, sitting on an almost-opaque card surface — the blur
   had nothing to work with. Free to drop. */
.vx-settings-lock-btn {
	-webkit-backdrop-filter: none;
	backdrop-filter: none;
}

/* With the filter gone this card is no longer sampling past its own edges, so
   full paint containment becomes safe. It already sets `overflow: hidden`, so
   this clips nothing that was not already clipped, and an element's own
   box-shadow is never affected by paint containment — only its descendants.
   `.vx-voices-empty` is deliberately NOT included: it has no `overflow:
   hidden`, so paint containment there would be a behaviour change for no
   gain — it is a single element that is hidden most of the time. */
.vx-voice-card.vx-glass-panel {
	contain: content;
}

/* ============================================================
   11. OFF-SCREEN ANIMATION PAUSING
   Toggled by the IntersectionObserver in landing.js.

   The hero runs three `vx-blob-float` blobs (each `filter: blur(64px)`, each
   animating TRANSLATE **and SCALE**, which forces the blurred texture to be
   re-rastered rather than just re-composited), three `vx-float-y` elements —
   two of which still carry a real `backdrop-filter`, so their backdrop is
   re-blurred on every frame of the float — and 28 `vx-wave` bars. All of them
   are `infinite`.

   None of that stopped when the hero scrolled away. Reading the Features and
   Testimonials sections meant the GPU was still animating seven blurred
   surfaces and 28 bars for content that was hundreds of pixels above the
   viewport. That is where the remaining homepage stutter was going.

   Pausing an animation you cannot see is by definition invisible, and
   `animation-play-state` resumes from the exact frame it stopped on, so
   scrolling back up looks identical to never having paused. The `will-change`
   hints are dropped at the same time, which releases the textures too.
   ============================================================ */
.vx-anim-paused .vx-animate-blob,
.vx-anim-paused .vx-animate-float,
.vx-anim-paused .vx-animate-wave,
.vx-anim-paused .vx-animate-pulse,
.vx-anim-paused .vx-shimmer-bg {
	animation-play-state: paused;
}

.vx-anim-paused .vx-animate-blob,
.vx-anim-paused .vx-animate-float {
	will-change: auto;
}

/* ============================================================
   12. AMBIENT BLOBS  —  blur(64px) replaced by its exact gradient

   This is the heaviest thing left on the app pages, and it is what the
   Dashboard hero shell is made of. Six decorative surfaces above the fold,
   four of them running `filter: blur(64px)`:

       .vx-app-page__bg        fixed page gradient
       .vx-app-page__blob-1    420px, blur(64px)
       .vx-app-page__blob-2    460px, blur(64px)
       .vx-hero-shell__bg      hero gradient
       .vx-hero-shell__blob-a  256px, blur(64px)
       .vx-hero-shell__blob-b  288px, blur(64px)

   Promoted, that is ~53 MB of GPU texture on a 2.75x-DPR phone, because a
   blur expands a layer's bounds by ~3 sigma on every side and the blurred
   result has to be re-rastered whenever the layer is invalidated.

   THE INSIGHT: every one of these blobs is a SINGLE FLAT COLOUR in a circle.
   CSS `blur(64px)` is a Gaussian convolution with sigma = 32px, and the
   Gaussian convolution of a uniform disc is radially symmetric — which is
   precisely the shape `radial-gradient()` draws. So the blur is not
   producing anything a gradient cannot produce directly, it is just paying a
   full separable convolution every raster to get there.

   The stops below were computed by actually convolving each disc with its
   Gaussian and sampling the result at 20 points. Verified worst-case
   on-screen difference: 0.35/255 on a single channel.

   GEOMETRY: a blur spills ~3 sigma (96px) past the element box, so each blob
   grows by 2x96px and every offset shifts by -96px (-6rem) to compensate.
   Each centre point was checked to be numerically unchanged, and the new box
   sizes are exactly the bounds the blurred layers already occupied — so this
   costs no extra texture, it just removes the convolution.

   TO ROLL BACK: delete this section. app.css still holds the original
   `filter: blur(64px)` rules and they take over again immediately.
   ============================================================ */

/* --- geometry: grow by 2x96px, shift every offset by -6rem --------- */
.vx-hero-shell__blob-a { left: -10rem; top: -10rem; width: 28rem; height: 28rem; filter: none; }
.vx-hero-shell__blob-b { right: -8.5rem; bottom: calc(-40% - 6rem); width: 30rem; height: 30rem; filter: none; }
.vx-app-page__blob-1   { top: -1rem; left: -11rem; width: 38.25rem; height: 38.25rem; filter: none; }
.vx-app-page__blob-2   { top: calc(33% - 6rem); right: -12rem; width: 40.75rem; height: 40.75rem; filter: none; }

/* --- the gradients that replace the blur --------------------------- */
/* .vx-access-hero .vx-hero-shell__blob-a  256->448px  on-screen delta 0.29/255 */
.vx-access-hero .vx-hero-shell__blob-a {
	background: radial-gradient(circle closest-side, rgba(139,92,246,0.2999) 0.0%, rgba(139,92,246,0.2998) 5.3%, rgba(139,92,246,0.2996) 10.5%, rgba(139,92,246,0.2988) 15.8%, rgba(139,92,246,0.2970) 21.1%, rgba(139,92,246,0.2928) 26.3%, rgba(139,92,246,0.2842) 31.6%, rgba(139,92,246,0.2693) 36.8%, rgba(139,92,246,0.2448) 42.1%, rgba(139,92,246,0.2122) 47.4%, rgba(139,92,246,0.1726) 52.6%, rgba(139,92,246,0.1285) 57.9%, rgba(139,92,246,0.0886) 63.2%, rgba(139,92,246,0.0544) 68.4%, rgba(139,92,246,0.0307) 73.7%, rgba(139,92,246,0.0155) 78.9%, rgba(139,92,246,0.0068) 84.2%, rgba(139,92,246,0.0028) 89.5%, rgba(139,92,246,0.0010) 94.7%, rgba(139,92,246,0.0006) 100.0%);
}
/* .vx-access-hero .vx-hero-shell__blob-b  288->480px  on-screen delta 0.32/255 */
.vx-access-hero .vx-hero-shell__blob-b {
	background: radial-gradient(circle closest-side, rgba(91,156,246,0.3000) 0.0%, rgba(91,156,246,0.3000) 5.3%, rgba(91,156,246,0.2999) 10.5%, rgba(91,156,246,0.2997) 15.8%, rgba(91,156,246,0.2991) 21.1%, rgba(91,156,246,0.2973) 26.3%, rgba(91,156,246,0.2928) 31.6%, rgba(91,156,246,0.2835) 36.8%, rgba(91,156,246,0.2657) 42.1%, rgba(91,156,246,0.2384) 47.4%, rgba(91,156,246,0.2010) 52.6%, rgba(91,156,246,0.1550) 57.9%, rgba(91,156,246,0.1098) 63.2%, rgba(91,156,246,0.0688) 68.4%, rgba(91,156,246,0.0390) 73.7%, rgba(91,156,246,0.0195) 78.9%, rgba(91,156,246,0.0084) 84.2%, rgba(91,156,246,0.0032) 89.5%, rgba(91,156,246,0.0011) 94.7%, rgba(91,156,246,0.0006) 100.0%);
}
/* .vx-dashboard-hero .vx-hero-shell__blob-a  256->448px  on-screen delta 0.29/255 */
.vx-dashboard-hero .vx-hero-shell__blob-a {
	background: radial-gradient(circle closest-side, rgba(91,156,246,0.2999) 0.0%, rgba(91,156,246,0.2998) 5.3%, rgba(91,156,246,0.2996) 10.5%, rgba(91,156,246,0.2988) 15.8%, rgba(91,156,246,0.2970) 21.1%, rgba(91,156,246,0.2928) 26.3%, rgba(91,156,246,0.2842) 31.6%, rgba(91,156,246,0.2693) 36.8%, rgba(91,156,246,0.2448) 42.1%, rgba(91,156,246,0.2122) 47.4%, rgba(91,156,246,0.1726) 52.6%, rgba(91,156,246,0.1285) 57.9%, rgba(91,156,246,0.0886) 63.2%, rgba(91,156,246,0.0544) 68.4%, rgba(91,156,246,0.0307) 73.7%, rgba(91,156,246,0.0155) 78.9%, rgba(91,156,246,0.0068) 84.2%, rgba(91,156,246,0.0028) 89.5%, rgba(91,156,246,0.0010) 94.7%, rgba(91,156,246,0.0006) 100.0%);
}
/* .vx-dashboard-hero .vx-hero-shell__blob-b  288->480px  on-screen delta 0.21/255 */
.vx-dashboard-hero .vx-hero-shell__blob-b {
	background: radial-gradient(circle closest-side, rgba(224,123,208,0.2500) 0.0%, rgba(224,123,208,0.2500) 5.3%, rgba(224,123,208,0.2499) 10.5%, rgba(224,123,208,0.2498) 15.8%, rgba(224,123,208,0.2492) 21.1%, rgba(224,123,208,0.2477) 26.3%, rgba(224,123,208,0.2440) 31.6%, rgba(224,123,208,0.2363) 36.8%, rgba(224,123,208,0.2214) 42.1%, rgba(224,123,208,0.1986) 47.4%, rgba(224,123,208,0.1675) 52.6%, rgba(224,123,208,0.1292) 57.9%, rgba(224,123,208,0.0915) 63.2%, rgba(224,123,208,0.0573) 68.4%, rgba(224,123,208,0.0325) 73.7%, rgba(224,123,208,0.0163) 78.9%, rgba(224,123,208,0.0070) 84.2%, rgba(224,123,208,0.0027) 89.5%, rgba(224,123,208,0.0009) 94.7%, rgba(224,123,208,0.0005) 100.0%);
}
/* .vx-app-page__blob-1  420->612px  on-screen delta 0.32/255 */
.vx-app-page__blob-1 {
	background: radial-gradient(circle closest-side, rgba(139,92,246,0.2000) 0.0%, rgba(139,92,246,0.2000) 5.3%, rgba(139,92,246,0.2000) 10.5%, rgba(139,92,246,0.2000) 15.8%, rgba(139,92,246,0.2000) 21.1%, rgba(139,92,246,0.2000) 26.3%, rgba(139,92,246,0.1999) 31.6%, rgba(139,92,246,0.1997) 36.8%, rgba(139,92,246,0.1985) 42.1%, rgba(139,92,246,0.1948) 47.4%, rgba(139,92,246,0.1853) 52.6%, rgba(139,92,246,0.1653) 57.9%, rgba(139,92,246,0.1346) 63.2%, rgba(139,92,246,0.0949) 68.4%, rgba(139,92,246,0.0578) 73.7%, rgba(139,92,246,0.0293) 78.9%, rgba(139,92,246,0.0118) 84.2%, rgba(139,92,246,0.0040) 89.5%, rgba(139,92,246,0.0011) 94.7%, rgba(139,92,246,0.0004) 100.0%);
}
/* .vx-app-page__blob-2  460->652px  on-screen delta 0.35/255 */
.vx-app-page__blob-2 {
	background: radial-gradient(circle closest-side, rgba(91,156,246,0.2000) 0.0%, rgba(91,156,246,0.2000) 5.3%, rgba(91,156,246,0.2000) 10.5%, rgba(91,156,246,0.2000) 15.8%, rgba(91,156,246,0.2000) 21.1%, rgba(91,156,246,0.2000) 26.3%, rgba(91,156,246,0.2000) 31.6%, rgba(91,156,246,0.1999) 36.8%, rgba(91,156,246,0.1995) 42.1%, rgba(91,156,246,0.1977) 47.4%, rgba(91,156,246,0.1921) 52.6%, rgba(91,156,246,0.1774) 57.9%, rgba(91,156,246,0.1506) 63.2%, rgba(91,156,246,0.1111) 68.4%, rgba(91,156,246,0.0699) 73.7%, rgba(91,156,246,0.0361) 78.9%, rgba(91,156,246,0.0145) 84.2%, rgba(91,156,246,0.0047) 89.5%, rgba(91,156,246,0.0012) 94.7%, rgba(91,156,246,0.0004) 100.0%);
}

/* CORRECTION — `.vx-hero-shell__blob-a` and `__blob-b` were in this list and
   should never have been. Both carry `.vx-animate-blob`: an 18-second
   infinite translate+scale that never stops running, on the Dashboard and
   now on the Profile page too. Un-promoting a permanently animating element
   is exactly backwards — without its own layer the compositor has to
   re-rasterise it into the page tiles on every frame, forever. That is a
   continuous cost paid whether or not anyone is scrolling, and it is what
   made those two pages feel heavy.

   `.vx-hero-shell__bg` is a different case and does belong here: it is a
   static gradient that nothing animates, so a layer saves nothing. It
   rasterises into the page's normal tiles once and is never touched again.

   The two `.vx-app-page__*` blobs stay promoted for a third reason — they
   are FIXED, so without a layer they would be repainted on every scroll
   frame regardless of animation. */
.vx-hero-shell__bg {
	transform: none;
	will-change: auto;
}


/* ------------------------------------------------------------------
   LANDING HERO BLOBS — the worst blur case in the whole theme

   These three are `filter: blur(64px)` AND they run `vx-blob-float`, which
   animates translate **and scale(1.08 / 0.95)**, forever. Animating scale on
   a blurred layer is the one case a GPU cannot optimise away: a translate can
   be handled by the compositor alone, but a scale changes the raster
   resolution, so the 64px Gaussian is recomputed on essentially every frame
   of an 18-second loop that never ends.

   As solid-colour discs they qualify for the same exact-gradient replacement
   used on the app-page blobs in section 12. With no filter left, the scale
   animation becomes a pure compositor transform — nothing to re-raster.

   Same geometry rule as section 12: grow by 2x96px, shift every offset by
   -6rem. All three centres verified numerically unchanged. Worst on-screen
   difference 0.55/255.
   ------------------------------------------------------------------ */
.vx-hero__blob-1 { top: -11rem; left: -12rem; width: 38rem; height: 38rem; filter: none; }
.vx-hero__blob-2 { top: -8.5rem; right: calc(-10% - 6rem); width: 40.75rem; height: 40.75rem; filter: none; }
.vx-hero__blob-3 { top: calc(40% - 6rem); left: calc(30% - 6rem); width: 29.5rem; height: 29.5rem; filter: none; }

/* .vx-hero__blob-1  416->608px  on-screen delta 0.47/255 */
.vx-hero__blob-1 {
	background: radial-gradient(circle closest-side, rgba(139,92,246,0.3000) 0.0%, rgba(139,92,246,0.3000) 5.3%, rgba(139,92,246,0.3000) 10.5%, rgba(139,92,246,0.3000) 15.8%, rgba(139,92,246,0.3000) 21.1%, rgba(139,92,246,0.3000) 26.3%, rgba(139,92,246,0.2999) 31.6%, rgba(139,92,246,0.2994) 36.8%, rgba(139,92,246,0.2975) 42.1%, rgba(139,92,246,0.2916) 47.4%, rgba(139,92,246,0.2767) 52.6%, rgba(139,92,246,0.2459) 57.9%, rgba(139,92,246,0.1993) 63.2%, rgba(139,92,246,0.1400) 68.4%, rgba(139,92,246,0.0850) 73.7%, rgba(139,92,246,0.0430) 78.9%, rgba(139,92,246,0.0174) 84.2%, rgba(139,92,246,0.0059) 89.5%, rgba(139,92,246,0.0016) 94.7%, rgba(139,92,246,0.0006) 100.0%);
}
/* .vx-hero__blob-2  460->652px  on-screen delta 0.55/255 */
.vx-hero__blob-2 {
	background: radial-gradient(circle closest-side, rgba(91,156,246,0.3200) 0.0%, rgba(91,156,246,0.3200) 5.3%, rgba(91,156,246,0.3200) 10.5%, rgba(91,156,246,0.3200) 15.8%, rgba(91,156,246,0.3200) 21.1%, rgba(91,156,246,0.3200) 26.3%, rgba(91,156,246,0.3200) 31.6%, rgba(91,156,246,0.3199) 36.8%, rgba(91,156,246,0.3192) 42.1%, rgba(91,156,246,0.3164) 47.4%, rgba(91,156,246,0.3073) 52.6%, rgba(91,156,246,0.2838) 57.9%, rgba(91,156,246,0.2410) 63.2%, rgba(91,156,246,0.1777) 68.4%, rgba(91,156,246,0.1119) 73.7%, rgba(91,156,246,0.0577) 78.9%, rgba(91,156,246,0.0232) 84.2%, rgba(91,156,246,0.0076) 89.5%, rgba(91,156,246,0.0019) 94.7%, rgba(91,156,246,0.0007) 100.0%);
}
/* .vx-hero__blob-3  280->472px  on-screen delta 0.21/255 */
.vx-hero__blob-3 {
	background: radial-gradient(circle closest-side, rgba(224,123,208,0.2500) 0.0%, rgba(224,123,208,0.2500) 5.3%, rgba(224,123,208,0.2499) 10.5%, rgba(224,123,208,0.2497) 15.8%, rgba(224,123,208,0.2489) 21.1%, rgba(224,123,208,0.2471) 26.3%, rgba(224,123,208,0.2426) 31.6%, rgba(224,123,208,0.2338) 36.8%, rgba(224,123,208,0.2176) 42.1%, rgba(224,123,208,0.1935) 47.4%, rgba(224,123,208,0.1617) 52.6%, rgba(224,123,208,0.1236) 57.9%, rgba(224,123,208,0.0869) 63.2%, rgba(224,123,208,0.0541) 68.4%, rgba(224,123,208,0.0306) 73.7%, rgba(224,123,208,0.0154) 78.9%, rgba(224,123,208,0.0066) 84.2%, rgba(224,123,208,0.0026) 89.5%, rgba(224,123,208,0.0009) 94.7%, rgba(224,123,208,0.0005) 100.0%);
}

/* `.vx-hero__bg` is an ordinary gradient on an absolutely-positioned element
   that scrolls with the page — no filter, nothing animating. There is
   nothing for a GPU layer to save, so it is no longer promoted either. */
.vx-hero__bg {
	transform: none;
	will-change: auto;
}

/* ============================================================
   13. REDUCED MOTION
   With animations off there is nothing left to composite, so the layers would
   only cost memory. Drop every hint.
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
	.vx-app-page__bg,
	.vx-app-page__blob-1,
	.vx-app-page__blob-2,
	.vx-hero-shell__bg,
	.vx-hero-shell__blob-a,
	.vx-hero-shell__blob-b,
	.vx-hero__bg,
	.vx-waveform,
	.vx-app-header,
	.vx-app-tabbar,
	.vx-mobile-tabbar,
	.vx-navbar-wrap,
	.vx-pill-group,
	.vx-studio-section__glow,
	.vx-feature-card__glow,
	.vx-testimonial-card__glow,
	.vx-voice-card__glow,
	.vx-fav-voice-card__glow,
	.vx-player__glow {
		will-change: auto;
	}
}
