fix: resolve navigation overlap and smooth animations

This commit is contained in:
adminroot committed 2026-09-08 00:40:24 +00:00
1 parent 731f57da78
commit 41a529e50e
6 files changed
+230 -85

No files matched your search

+1
View File
@@ -8,6 +8,7 @@
"build": "tsc -b && vite build",
"typecheck": "tsc -b --pretty false",
"visual-check": "node scripts/visual-check.mjs",
"navigation-motion-check": "node scripts/navigation-motion-check.mjs",
"sidebar-resize-check": "node scripts/sidebar-resize-check.mjs",
"viewer-resize-check": "node scripts/viewer-resize-check.mjs",
"viewer-similar-check": "node scripts/viewer-similar-check.mjs"
@@ -0,0 +1,147 @@
import assert from "node:assert/strict";
import { chromium } from "playwright-core";
const browser = await chromium.launch({
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE
?? "/root/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome",
headless: true,
args: ["--no-sandbox", "--disable-dev-shm-usage"],
});
async function traceNavigation(page, toggles) {
return page.evaluate(async (times) => {
const menu = document.querySelector(".gallery-sidebar-toggle");
const toolbar = document.querySelector(".topbar");
const sidebar = () => document.querySelector(innerWidth <= 720 ? ".mobile-sidebar" : ".desktop-sidebar");
const right = () => sidebar()?.getBoundingClientRect().right ?? 0;
const controls = [...toolbar.querySelectorAll("button")].filter(button => !button.closest("[inert]"));
const menuRect = menu.getBoundingClientRect();
const result = { frames: 0, reversals: [], leaks: [], coveredSidebar: 0, coveredControls: 0, menuDrift: 0, partialFrames: 0 };
const started = performance.now();
let index = 0;
while (performance.now() - started < times.at(-1) + 440) {
await new Promise(requestAnimationFrame);
if (index < times.length && performance.now() - started >= times[index]) {
const before = right();
menu.click();
// Flush the component update without advancing the animation timeline.
await Promise.resolve();
await Promise.resolve();
result.reversals.push(Math.abs(right() - before));
index++;
}
result.frames++;
const panel = sidebar();
const panelRect = panel?.getBoundingClientRect();
const topbarRect = toolbar.getBoundingClientRect();
const rect = menu.getBoundingClientRect();
result.menuDrift = Math.max(result.menuDrift, Math.hypot(rect.x - menuRect.x, rect.y - menuRect.y));
if (panelRect && panelRect.right > 1 && panelRect.right < panelRect.width - 1) result.partialFrames++;
for (const button of controls) {
const box = button.getBoundingClientRect();
if (!button.contains(document.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2))) result.coveredControls++;
}
for (const y of [topbarRect.top + 1, topbarRect.top + topbarRect.height / 2, topbarRect.bottom - 1]) {
for (let x = 2; x < innerWidth; x += 16) {
const hit = document.elementFromPoint(x, y);
if (!hit || hit.closest(".content")) result.leaks.push({ x, y });
if (
panelRect && x < panelRect.right - 1 && !panel.closest("[inert]")
&& !panel.contains(hit) && !toolbar.contains(hit)
) result.coveredSidebar++;
}
}
}
result.open = menu.getAttribute("aria-expanded");
result.drawerMounted = Boolean(document.querySelector(".mobile-nav-layer"));
result.bodyLocked = document.body.style.overflow === "hidden";
result.overflow = document.documentElement.scrollWidth > innerWidth;
return result;
}, toggles);
}
function checkTrace(trace, reduced = false) {
assert.deepEqual(trace.leaks, [], "the header lets clicks reach gallery cards during a transition");
assert.equal(trace.coveredSidebar, 0, "the header surface covers the visible sidebar");
assert.equal(trace.coveredControls, 0, "navigation covers a toolbar control");
assert.ok(trace.menuDrift < 1, `the menu button moved during navigation: ${trace.menuDrift}px`);
assert.equal(trace.overflow, false);
if (!reduced) {
assert.ok(trace.partialFrames > 1, "the sidebar did not animate");
assert.ok(Math.max(...trace.reversals) < 2, `navigation jumped when reversed: ${trace.reversals}`);
}
}
try {
for (const width of [1440, 1000, 721, 720, 390, 320]) {
const mobile = width <= 720;
const page = await browser.newPage({ viewport: { width, height: 844 }, hasTouch: mobile });
const errors = [];
page.on("pageerror", error => errors.push(error.message));
await page.addInitScript(() => localStorage.setItem("pixhelf.sidebar-collapsed", "true"));
await page.goto(process.env.PIXHELF_URL ?? "http://127.0.0.1:3002");
await page.locator(".image-card").nth(24).waitFor();
// Put real, clickable cards behind the header, including its left edge.
await page.evaluate(() => window.scrollTo(0, 320));
await page.waitForTimeout(250);
const opened = await traceNavigation(page, [0]);
checkTrace(opened);
assert.equal(opened.open, "true");
const panel = mobile ? ".mobile-sidebar" : ".desktop-sidebar";
const brandClickable = await page.locator(`${panel} .brand-lockup`).evaluate(brand =>
[...brand.querySelectorAll(".brand-mark, .brand-name, .brand-version")].every(element => {
const rect = element.getBoundingClientRect();
return element.closest("a").contains(document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2));
}),
);
assert.equal(brandClickable, true, "the sidebar brand is obscured by the header");
await page.screenshot({ path: `/tmp/pixhelf-navigation-${width}.png` });
const closed = await traceNavigation(page, [0]);
checkTrace(closed);
assert.equal(closed.open, "false");
assert.equal(closed.drawerMounted || closed.bodyLocked, false);
const rapid = await traceNavigation(page, [0, 70, 140, 210, 280, 350]);
checkTrace(rapid);
assert.equal(rapid.drawerMounted || rapid.bodyLocked, false);
// A genuine pointer click in the blank header must not open the card below it.
await page.mouse.click(Math.min(100, width / 2), 25);
assert.equal(await page.locator(".image-viewer").count(), 0);
await page.locator(".gallery-sidebar-toggle").click();
await page.locator(`${panel} .brand-name`).click();
await page.waitForFunction(() => document.querySelector(".content").getAttribute("aria-busy") === "false");
await page.locator(".search-toggle").click();
await page.locator(".search-popover-panel input").fill("navigation-check");
assert.equal(await page.locator(".search-popover-panel input").inputValue(), "navigation-check");
await page.locator(".clear-search").click();
await page.locator(".search-toggle").click();
if (!mobile) await page.locator(".gallery-sidebar-toggle").click();
await page.waitForTimeout(360);
await page.emulateMedia({ reducedMotion: "reduce" });
const reduced = await traceNavigation(page, [0, 90]);
checkTrace(reduced, true);
assert.equal(reduced.drawerMounted || reduced.bodyLocked, false);
assert.equal(reduced.partialFrames, 0, "reduced motion still animates the sidebar");
if (width === 390) {
await page.evaluate(() => window.scrollTo(0, 320));
await page.waitForTimeout(200);
await page.evaluate(() => document.documentElement.style.setProperty("--visual-viewport-top", "24px"));
const offset = await traceNavigation(page, [0, 90]);
checkTrace(offset, true);
await page.evaluate(() => document.documentElement.style.removeProperty("--visual-viewport-top"));
await page.emulateMedia({ reducedMotion: "no-preference" });
await page.locator(".gallery-sidebar-toggle").click();
await page.locator(".mobile-sidebar .brand-name").waitFor();
await page.setViewportSize({ width: 1000, height: 844 });
await page.waitForFunction(() => !document.querySelector(".mobile-nav-layer") && document.body.style.overflow !== "hidden");
assert.equal(await page.locator(".gallery-sidebar-toggle").getAttribute("aria-controls"), "desktop-album-navigation");
await page.setViewportSize({ width, height: 844 });
await page.waitForFunction(() => document.querySelector(".gallery-sidebar-toggle").getAttribute("aria-controls") === "mobile-album-navigation");
assert.equal(await page.locator(".gallery-sidebar-toggle").getAttribute("aria-expanded"), "false");
}
assert.deepEqual(errors, []);
console.log(JSON.stringify({ width, frames: opened.frames + closed.frames + rapid.frames, maxReversalJump: Math.max(...rapid.reversals), brandClickable, reducedMotion: true }));
await page.close();
}
} finally {
await browser.close();
}
+8 -8
View File
@@ -172,7 +172,7 @@ try {
topbarPosition: topbar ? getComputedStyle(topbar).position : "",
topbarTop: topbarRect?.top ?? -1,
topbarHeight: topbarRect?.height ?? -1,
topbarSurfaceLeft: topbar ? Number.parseFloat(getComputedStyle(topbar, "::before").left) : -1,
topbarSurfaceLeft: document.querySelector(".topbar-surface")?.getBoundingClientRect().left ?? -1,
sidebarTop: document.querySelector(".desktop-sidebar")?.getBoundingClientRect().top ?? -1,
topbarSearches: document.querySelectorAll(
".topbar-actions > .topbar-search",
@@ -1561,7 +1561,7 @@ try {
progressPanels: document.querySelectorAll(".sidebar-progress").length,
inlineToggles: document.querySelectorAll(".desktop-sidebar-inline-toggle").length,
galleryToggles: document.querySelectorAll(".gallery-sidebar-toggle").length,
topbarSurfaceLeft: Number.parseFloat(getComputedStyle(document.querySelector(".topbar"), "::before").left),
topbarSurfaceLeft: document.querySelector(".topbar-surface")?.getBoundingClientRect().left ?? -1,
brandVisibility: getComputedStyle(document.querySelector(".desktop-sidebar .brand-lockup")).visibility,
brandInert: Boolean(document.querySelector(".desktop-sidebar .brand")?.closest("[inert]")),
}));
@@ -1655,8 +1655,8 @@ try {
layerTopGap: navigationLayer && topbar
? navigationLayer.y - topbar.y
: -1,
topbarSurfaceLeft: await page.locator(".topbar").evaluate((header) =>
Number.parseFloat(getComputedStyle(header, "::before").left)),
topbarSurfaceLeft: await page.locator(".topbar-surface").evaluate((surface) =>
surface.getBoundingClientRect().left),
toggleWidth: navigationToggleBefore?.width ?? -1,
toggleHeight: navigationToggleBefore?.height ?? -1,
expandedToggleWidth: navigationToggleOpen?.width ?? -1,
@@ -1696,9 +1696,9 @@ try {
for (const width of [320, 360, 375, target.width]) {
await page.setViewportSize({ width, height: target.height });
await page.waitForFunction(() => {
const header = document.querySelector(".topbar");
const drawer = document.querySelector(".mobile-sidebar").getBoundingClientRect();
return Math.abs(Number.parseFloat(getComputedStyle(header, "::before").left) - drawer.right) < 1;
// The gallery's ResizeObserver commits on the next animation frame.
return Math.abs(drawer.left) < 1 && document.documentElement.scrollWidth <= innerWidth;
});
const dimensions = await page.evaluate(() => {
const drawer = document.querySelector(".mobile-sidebar").getBoundingClientRect();
@@ -2006,7 +2006,7 @@ try {
desktopSidebar.toggleHeaderInsetY < 5 ||
desktopSidebar.toggleHeaderInsetY > 9 ||
Math.abs(layout.sidebarTop) > 1 ||
Math.abs(layout.topbarSurfaceLeft - desktopSidebar.contentBefore) > 1 ||
Math.abs(layout.topbarSurfaceLeft) > 1 ||
Math.abs(desktopSidebar.topbarSurfaceLeft) > 1 ||
desktopSidebar.stationaryToggleDelta < 0 ||
desktopSidebar.stationaryToggleDelta > 1 ||
@@ -2035,7 +2035,7 @@ try {
mobileNavigation.metaRows !== 0 ||
Math.abs(mobileNavigation.layerTopGap) > 1 ||
Math.abs(layout.topbarSurfaceLeft) > 1 ||
Math.abs(mobileNavigation.topbarSurfaceLeft - mobileNavigation.drawerWidth) > 1 ||
Math.abs(mobileNavigation.topbarSurfaceLeft) > 1 ||
Math.abs(mobileNavigation.toggleWidth - 42) > 1 ||
Math.abs(mobileNavigation.toggleHeight - 42) > 1 ||
Math.abs(mobileNavigation.expandedToggleWidth - 42) > 1 ||
+15 -5
View File
@@ -285,7 +285,7 @@ function useDebounced<T>(value: T, delay: number): T {
return debounced;
}
function useDelayedUnmount(visible: boolean, delay: number): boolean {
function useTransitionPresence(visible: boolean, duration: number) {
const [mounted, setMounted] = useState(visible);
useLayoutEffect(() => {
@@ -295,11 +295,16 @@ function useDelayedUnmount(visible: boolean, delay: number): boolean {
}
if (!mounted) return;
const timer = window.setTimeout(() => setMounted(false), delay);
// The transition event normally removes the drawer. Cover hidden tabs and breakpoint changes.
const timer = window.setTimeout(() => setMounted(false), duration + 100);
return () => window.clearTimeout(timer);
}, [delay, mounted, visible]);
}, [duration, mounted, visible]);
return mounted;
const finishExit = useCallback(() => {
if (!visible) setMounted(false);
}, [visible]);
return { mounted: visible || mounted, finishExit };
}
function errorMessage(reason: unknown, fallback: string): string {
@@ -363,7 +368,10 @@ function App() {
const [viewerReturnRequest, setViewerReturnRequest] = useState<ViewerReturnRequest | null>(null);
const [reloadToken, setReloadToken] = useState(0);
const compactLayout = useMediaQuery("(max-width: 720px)");
const mobileNavMounted = useDelayedUnmount(mobileNavOpen, MOBILE_NAV_EXIT_MS);
const { mounted: mobileNavMounted, finishExit: finishMobileNavExit } = useTransitionPresence(
mobileNavOpen,
MOBILE_NAV_EXIT_MS,
);
const textSearchReady = Boolean(
status?.backgroundComplete
&& status.textSearch.enabled
@@ -1164,6 +1172,7 @@ function App() {
data-viewer-returning={Boolean(viewerReturnRequest)}
data-viewer-return-image-id={viewerReturnRequest?.imageId}
>
<div className="topbar-surface" aria-hidden="true" />
<Header
search={search}
searchOpen={searchOpen}
@@ -1203,6 +1212,7 @@ function App() {
onHome={goHome}
mobileOpen={mobileNavOpen}
mobileMounted={mobileNavMounted}
onMobileExited={finishMobileNavExit}
onClose={() => setMobileNavOpen(false)}
desktopCollapsed={sidebarCollapsed}
/>
+9
View File
@@ -301,6 +301,7 @@ export function Sidebar({
onHome,
mobileOpen,
mobileMounted,
onMobileExited,
onClose,
desktopCollapsed,
}: {
@@ -311,6 +312,7 @@ export function Sidebar({
onHome: () => void;
mobileOpen: boolean;
mobileMounted: boolean;
onMobileExited: () => void;
onClose: () => void;
desktopCollapsed: boolean;
}) {
@@ -370,6 +372,13 @@ export function Sidebar({
className="sidebar mobile-sidebar"
role="dialog"
aria-label="相册导航"
onTransitionEnd={(event) => {
if (
event.target === event.currentTarget
&& event.propertyName === "transform"
&& !mobileOpen
) onMobileExited();
}}
>
<SidebarBrand onHome={onHome} />
{navigation}
+50 -72
View File
@@ -87,22 +87,21 @@ select:focus-visible,
align-items: center;
gap: 12px;
padding: 0 18px;
/* Keep the sidebar brand beneath the transparent header area clickable. */
pointer-events: none;
}
.topbar::before {
position: absolute;
z-index: -1;
inset: 0 0 0 var(--sidebar-layout-width);
content: "";
.topbar-surface {
/* Block gallery clicks below both sidebars; toolbar controls stay above them. */
position: fixed;
z-index: 10;
inset: 0 0 auto;
height: var(--topbar-height);
pointer-events: auto;
background: rgba(252, 253, 252, 0.94);
border-bottom: 1px solid var(--border);
box-shadow: 0 4px 18px rgba(25, 35, 31, 0.035);
backdrop-filter: blur(16px) saturate(1.08);
-webkit-backdrop-filter: blur(16px) saturate(1.08);
transition: left var(--motion-medium) var(--ease-standard);
}
.topbar-leading > *,
@@ -229,17 +228,15 @@ select:focus-visible,
.request-progress {
position: fixed;
z-index: 31;
z-index: 11;
top: calc(var(--visual-viewport-top) + var(--topbar-height) - 2px);
right: 0;
left: var(--sidebar-layout-width);
left: 0;
height: 2px;
overflow: hidden;
opacity: 0;
pointer-events: none;
transition:
opacity 180ms ease,
left var(--motion-medium) var(--ease-standard);
transition: opacity 180ms ease;
}
.request-progress[data-visible="true"] { opacity: 1; }
@@ -250,6 +247,9 @@ select:focus-visible,
display: block;
background: linear-gradient(90deg, transparent 0%, #2f8d82 28%, #78bab2 58%, transparent 100%);
transform-origin: left;
}
.request-progress[data-visible="true"] span {
animation: request-progress 1.1s var(--ease-standard) infinite;
}
@@ -810,7 +810,7 @@ select:focus-visible,
overflow: hidden;
margin: 0;
padding: 0;
background: #e5e8e6;
background: #e3e7e5;
border: 1px solid rgba(26, 35, 31, 0.08);
border-radius: 5px;
box-shadow: 0 1px 1px rgba(21, 31, 27, 0.05);
@@ -823,16 +823,16 @@ select:focus-visible,
}
.image-card::before,
.viewer-similar-card::before {
.viewer-similar-card::before,
.skeleton-grid span::before,
.viewer-similar-skeleton span::before {
position: absolute;
z-index: 0;
inset: 0;
content: "";
background:
linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, .48) 47%, transparent 64%),
#e3e7e5;
background-size: 220% 100%, 100% 100%;
background: linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, .48) 47%, transparent 64%);
opacity: 1;
transform: translate3d(-100%, 0, 0);
transition: opacity var(--motion-medium) ease;
animation: none;
}
@@ -844,7 +844,9 @@ select:focus-visible,
}
.image-card[data-loading="true"]::before,
.viewer-similar-card[data-loading="true"]::before {
.viewer-similar-card[data-loading="true"]::before,
.skeleton-grid span::before,
.viewer-similar-skeleton span::before {
animation: card-shimmer 1.35s ease-in-out infinite;
}
@@ -924,8 +926,8 @@ select:focus-visible,
}
@keyframes card-shimmer {
from { background-position: 130% 0, 0 0; }
to { background-position: -120% 0, 0 0; }
from { transform: translate3d(-100%, 0, 0); }
to { transform: translate3d(100%, 0, 0); }
}
.skeleton-grid span,
@@ -933,19 +935,11 @@ select:focus-visible,
position: absolute;
display: block;
min-height: 64px;
background:
linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, .48) 47%, transparent 64%),
#e3e7e5;
background-size: 220% 100%, 100% 100%;
overflow: hidden;
background: #e3e7e5;
border: 1px solid rgba(26, 35, 31, .08);
border-radius: 5px;
box-shadow: 0 1px 1px rgba(21, 31, 27, .05);
animation: card-shimmer 1.35s ease-in-out infinite;
}
@keyframes skeleton-shimmer {
from { background-position: 130% 0; }
to { background-position: -120% 0; }
}
.empty-state {
@@ -1451,11 +1445,8 @@ select:focus-visible,
}
.viewer-similar-card::before,
.viewer-similar-skeleton span {
background:
linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, .07) 47%, transparent 64%),
#1b211e;
background-size: 220% 100%, 100% 100%;
.viewer-similar-skeleton span::before {
background: linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, .07) 47%, transparent 64%);
}
.viewer-similar-card:focus-visible {
@@ -1683,9 +1674,6 @@ select:focus-visible,
/* Reserve three 42px tools, their gaps and the drawer clearance. */
--mobile-drawer-width: min(286px, calc(100vw - 144px - max(12px, env(safe-area-inset-right))));
}
.app-shell[data-mobile-navigation-open="true"] {
--sidebar-layout-width: var(--mobile-drawer-width);
}
.topbar {
position: fixed;
top: var(--visual-viewport-top);
@@ -1697,20 +1685,11 @@ select:focus-visible,
max(12px, env(safe-area-inset-left));
gap: 8px;
}
.topbar::before {
.topbar-surface {
top: var(--visual-viewport-top);
background: #fcfdfc;
backdrop-filter: none;
-webkit-backdrop-filter: none;
transition: left 240ms cubic-bezier(.55, .05, .8, .45);
}
.request-progress {
transition:
opacity 180ms ease,
left 240ms cubic-bezier(.55, .05, .8, .45);
}
.app-shell[data-mobile-navigation-open="true"] .topbar::before,
.app-shell[data-mobile-navigation-open="true"] .request-progress {
transition-timing-function: var(--ease-standard);
}
.gallery-sidebar-toggle.sidebar-toggle-button {
width: 42px;
@@ -1812,11 +1791,13 @@ select:focus-visible,
.mobile-nav-layer[data-state="closing"] { pointer-events: none; }
.mobile-nav-scrim {
position: absolute;
inset: 0;
inset: var(--topbar-height) 0 0;
width: 100%;
padding: 0;
background: rgba(18, 25, 22, .32);
border: 0;
opacity: 0;
transition: opacity 180ms ease;
}
.mobile-sidebar {
position: absolute;
@@ -1824,36 +1805,31 @@ select:focus-visible,
width: var(--mobile-drawer-width);
overflow: hidden;
box-shadow: 10px 0 24px rgba(13, 21, 17, .13);
will-change: transform;
transform: translate3d(-100%, 0, 0);
transition: transform 240ms var(--ease-standard);
}
.mobile-sidebar .sidebar-brand {
transform: translateX(var(--mobile-drawer-width));
transition: transform 240ms var(--ease-standard);
}
.mobile-nav-layer[data-state="open"] .mobile-nav-scrim {
animation: mobile-scrim-in 180ms ease-out both;
opacity: 1;
}
.mobile-nav-layer[data-state="open"] .mobile-sidebar {
animation: mobile-drawer-in 240ms var(--ease-standard) both;
transform: translate3d(0, 0, 0);
}
.mobile-nav-layer[data-state="open"] .sidebar-brand {
animation: mobile-brand-in 240ms var(--ease-standard) both;
transform: translateX(0);
}
.mobile-nav-layer[data-state="closing"] .mobile-nav-scrim {
animation: mobile-scrim-out 180ms ease-in both;
}
.mobile-nav-layer[data-state="closing"] .mobile-sidebar {
animation: mobile-drawer-out 240ms cubic-bezier(.55, .05, .8, .45) both;
}
.mobile-nav-layer[data-state="closing"] .sidebar-brand {
animation: mobile-brand-out 240ms cubic-bezier(.55, .05, .8, .45) both;
@starting-style {
.mobile-nav-layer[data-state="open"] .mobile-nav-scrim { opacity: 0; }
.mobile-nav-layer[data-state="open"] .mobile-sidebar { transform: translate3d(-100%, 0, 0); }
.mobile-nav-layer[data-state="open"] .sidebar-brand { transform: translateX(var(--mobile-drawer-width)); }
}
.mobile-sidebar .sidebar-status {
min-height: calc(50px + env(safe-area-inset-bottom));
padding-bottom: calc(11px + env(safe-area-inset-bottom));
}
@keyframes mobile-scrim-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes mobile-scrim-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes mobile-drawer-in { from { transform: translateX(-100%); } to { transform: translateX(0); } }
@keyframes mobile-drawer-out { from { transform: translateX(0); } to { transform: translateX(-100%); } }
@keyframes mobile-brand-in { from { transform: translateX(var(--mobile-drawer-width)); } to { transform: translateX(0); } }
@keyframes mobile-brand-out { from { transform: translateX(0); } to { transform: translateX(var(--mobile-drawer-width)); } }
}
@media (max-width: 340px) {
@@ -1928,8 +1904,10 @@ select:focus-visible,
}
.image-card::before,
.skeleton-grid span,
.viewer-similar-skeleton span {
background-position: 0 0;
.viewer-similar-card::before,
.skeleton-grid span::before,
.viewer-similar-skeleton span::before {
animation: none !important;
transform: none;
}
}