diff --git a/src/components/admin/CommandPalette.tsx b/src/components/admin/CommandPalette.tsx
index 2a33a51..9fcbce1 100644
--- a/src/components/admin/CommandPalette.tsx
+++ b/src/components/admin/CommandPalette.tsx
@@ -278,15 +278,21 @@ export default function CommandPalette() {
}}
>
{/* Local keyframes — scoped via the dialog's runtime so they don't
- leak into the global stylesheet. */}
+ leak into the global stylesheet. The panel now fades in without
+ the scale(0.98) start (removed — the slight scale combined with
+ the backdrop fade read as a "pop" when the palette opened). */}
diff --git a/src/components/admin/TabSwitcher.tsx b/src/components/admin/TabSwitcher.tsx
index 3615429..88d6aa0 100644
--- a/src/components/admin/TabSwitcher.tsx
+++ b/src/components/admin/TabSwitcher.tsx
@@ -10,18 +10,26 @@ type Props = {
/**
* Fades the content of a tabbed panel when the active tab changes.
* The tab key drives the animation, so switching tabs triggers a quick
- * fade-in of the new content. No motion when the tab key is stable
- * (i.e. on first render of a given tab).
+ * opacity-only fade-in of the new content.
+ *
+ * Motion note: the previous version used `y: 4` / `y: -4` on enter/exit
+ * for a small slide. That 4px slide, multiplied across every tab change
+ * in the admin, was a steady source of positional movement. Now it's
+ * pure opacity, 120ms, ease-out. `reducedMotion="user"` on the global
+ * will further strip this to instant if the user has
+ * prefers-reduced-motion enabled.
+ *
+ * No motion when the tab key is stable (i.e. on first render of a given tab).
*/
export function TabSwitcher({ tabKey, children }: Props) {
return (
{children}
diff --git a/src/styles/admin-design-system.css b/src/styles/admin-design-system.css
index 98d021a..4d9beb3 100644
--- a/src/styles/admin-design-system.css
+++ b/src/styles/admin-design-system.css
@@ -358,9 +358,11 @@
}
/* === Toast Animations === */
+/* Motion-reduced: was 100% translateX with 0.3s — too far across the screen
+ * for a small notification. Now 24px slide, 180ms. */
@keyframes slide-in-from-right {
from {
- transform: translateX(100%);
+ transform: translateX(24px);
opacity: 0;
}
to {
@@ -379,7 +381,7 @@
}
.animate-in {
- animation: slide-in-from-right 0.3s ease-out, fade-in 0.3s ease-out;
+ animation: slide-in-from-right 0.18s ease-out, fade-in 0.18s ease-out;
}
/* ============================================================ */
@@ -1003,7 +1005,7 @@
align-items: center;
justify-content: center;
z-index: 2;
- animation: ha-check-pop 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
+ animation: ha-check-pop 0.16s ease-out;
}
.ha-product-card-plus {
position: absolute;
@@ -1056,9 +1058,12 @@
opacity: 1;
}
+/* Motion-reduced: was scale 0 → 1.15 → 1 with the bouncy
+ * cubic-bezier(0.34, 1.56, 0.64, 1) — that overshoots 15% past the
+ * target then settles, which reads as twitchy. Now scale 0.92 → 1
+ * (small "appear" with no overshoot) in 160ms with a plain ease-out. */
@keyframes ha-check-pop {
- 0% { transform: scale(0); opacity: 0; }
- 60% { transform: scale(1.15); }
+ 0% { transform: scale(0.92); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
@@ -1498,11 +1503,14 @@
0 20px 40px -8px rgba(60, 56, 37, 0.18),
0 4px 8px -2px rgba(60, 56, 37, 0.06);
padding: 0.875rem 1rem 0.75rem 1rem;
- animation: ha-popover-in 0.16s cubic-bezier(0.4, 0, 0.2, 1);
+ animation: ha-popover-in 0.12s ease-out;
}
+/* Motion-reduced: removed translateY(-4px) and scale(0.98) — popovers now
+ * just fade in. The 4px slide + slight scale compounded across the day
+ * when hovering many calendar events. */
@keyframes ha-popover-in {
- from { opacity: 0; transform: translateY(-4px) scale(0.98); }
- to { opacity: 1; transform: translateY(0) scale(1); }
+ from { opacity: 0; }
+ to { opacity: 1; }
}
.ha-event-popover-arrow {
position: absolute;
@@ -1616,7 +1624,7 @@
background: rgba(60, 56, 37, 0.3);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
- animation: ha-fade-in 0.18s ease-out;
+ animation: ha-fade-in 0.14s ease-out;
}
@keyframes ha-fade-in {
from { opacity: 0; }
@@ -1637,10 +1645,13 @@
-4px 0 8px -2px rgba(60, 56, 37, 0.04);
display: flex;
flex-direction: column;
- animation: ha-drawer-in 0.22s cubic-bezier(0.4, 0, 0.2, 1);
+ /* Motion-reduced: was 100% translateX (drawer traveled the full width of
+ * the screen). Now 24px — enough to feel like the drawer is sliding in
+ * from the right, but doesn't make the eye track across the whole screen. */
+ animation: ha-drawer-in 0.18s ease-out;
}
@keyframes ha-drawer-in {
- from { transform: translateX(100%); }
+ from { transform: translateX(24px); }
to { transform: translateX(0); }
}
.ha-drawer-header {
@@ -1954,11 +1965,12 @@
/* === Admin Dashboard — Compact Card System ================== */
/* ============================================================ */
-/* Entrance animation — staggered fade-up */
+/* Entrance animation — staggered fade-up.
+ * Motion-reduced: was 10px Y slide, 0.35s. Now 4px Y, 0.2s. */
@keyframes dashboard-fade-up {
from {
opacity: 0;
- transform: translateY(10px);
+ transform: translateY(4px);
}
to {
opacity: 1;
@@ -1966,7 +1978,7 @@
}
}
.animate-fade-up {
- animation: dashboard-fade-up 0.35s cubic-bezier(0.4, 0, 0.2, 1) both;
+ animation: dashboard-fade-up 0.2s ease-out both;
}
/* ── Stat Card ─────────────────────────────────────────────── */
@@ -2338,3 +2350,61 @@
font-size: 0.6875rem;
}
}
+
+/* ============================================================ */
+/* === Reduced motion — admin scope =========================== */
+/* ============================================================ */
+/* The globals.css media block already wipes animation/transition durations
+ * for the public site. This block does the same for the admin design system:
+ * - kills all admin-specific keyframes (popovers, drawers, toasts, fade-up)
+ * - locks every transition to ~instant
+ * - drops the last positional nudges (arrow translate, sidebar pulse)
+ * The skeleton/shimmer loading animations are kept because they're
+ * load-state indicators, not interaction feedback.
+ */
+@media (prefers-reduced-motion: reduce) {
+ .animate-fade-up,
+ .animate-in,
+ .ha-drawer,
+ .ha-drawer-backdrop,
+ .ha-event-popover {
+ animation: none !important;
+ }
+ .animate-fade-up,
+ .ha-drawer {
+ transform: none !important;
+ opacity: 1 !important;
+ }
+ .admin-section-card-arrow {
+ transition: none !important;
+ transform: none !important;
+ }
+ /* Tab switcher relies on framer-motion AnimatePresence which is governed
+ * by the global in Providers.tsx. */
+}
+
+/* ============================================================ */
+/* === Tailwind animate-in overrides ========================= */
+/* ============================================================ */
+/* These target the `tailwindcss-animate` plugin's utilities (`animate-in`
+ * paired with `slide-in-from-*`, `zoom-in-*`, `fade-in`). Used in
+ * ToastContainer and LotDetailPanel.
+ *
+ * The defaults in that plugin define keyframes with:
+ * - `slide-in-from-right-5` → translateX(1.25rem) (20px — a lot for a toast)
+ * - `zoom-in-95` → scale 0.95 → 1 (5% scale on modal open)
+ *
+ * Both compounds quickly: every toast + every modal pop animates a positional
+ * or scale change. The overrides below use CSS specificity to swap the
+ * keyframes for opacity-only fades. */
+@keyframes slide-in-from-right-5 { from { opacity: 0; } to { opacity: 1; } }
+@keyframes slide-in-from-right-4 { from { opacity: 0; } to { opacity: 1; } }
+@keyframes slide-in-from-right-3 { from { opacity: 0; } to { opacity: 1; } }
+@keyframes slide-in-from-right-2 { from { opacity: 0; } to { opacity: 1; } }
+@keyframes slide-in-from-right { from { opacity: 0; } to { opacity: 1; } }
+@keyframes slide-in-from-bottom { from { opacity: 0; } to { opacity: 1; } }
+@keyframes slide-in-from-top { from { opacity: 0; } to { opacity: 1; } }
+@keyframes slide-in-from-left { from { opacity: 0; } to { opacity: 1; } }
+@keyframes zoom-in-95 { from { opacity: 0; } to { opacity: 1; } }
+@keyframes zoom-in-90 { from { opacity: 0; } to { opacity: 1; } }
+@keyframes zoom-in { from { opacity: 0; } to { opacity: 1; } }