diff --git a/services/frontend/src/app/globals.css b/services/frontend/src/app/globals.css
index 86aab0e..e325adf 100644
--- a/services/frontend/src/app/globals.css
+++ b/services/frontend/src/app/globals.css
@@ -280,6 +280,25 @@
.animate-fade-up {
animation: fade-up 0.4s cubic-bezier(0.22, 1, 0.36, 1) both;
}
+/* List stagger: each child rises in sequence. Pair with inline
+ style={{ animationDelay }} set via staggerDelay(). */
+.animate-stagger {
+ animation: fade-up 0.42s cubic-bezier(0.22, 1, 0.36, 1) both;
+}
+/* Toast: slide up + settle from the dock edge. */
+.animate-toast-in {
+ animation: toast-in 0.22s cubic-bezier(0.22, 1, 0.36, 1) both;
+}
+@keyframes toast-in {
+ from {
+ opacity: 0;
+ transform: translateY(14px) scale(0.98);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
@media (prefers-reduced-motion: reduce) {
.scan-line::after,
@@ -289,7 +308,9 @@
.animate-pulse-ring,
.animate-shimmer,
.animate-breathe,
- .animate-fade-up {
+ .animate-fade-up,
+ .animate-stagger,
+ .animate-toast-in {
animation: none !important;
}
.scan-line {
diff --git a/services/frontend/src/components/primitives/button.tsx b/services/frontend/src/components/primitives/button.tsx
index 3d34fca..7b6cdea 100644
--- a/services/frontend/src/components/primitives/button.tsx
+++ b/services/frontend/src/components/primitives/button.tsx
@@ -40,7 +40,7 @@ export const Button = ({
return (
diff --git a/services/frontend/src/components/shared/section.tsx b/services/frontend/src/components/shared/section.tsx
index f5e7e74..80331f9 100644
--- a/services/frontend/src/components/shared/section.tsx
+++ b/services/frontend/src/components/shared/section.tsx
@@ -37,6 +37,7 @@ export function MetricTile({
spark,
icon,
className,
+ style,
}: {
label: string;
value: React.ReactNode;
@@ -45,6 +46,7 @@ export function MetricTile({
spark?: number[];
icon?: React.ReactNode;
className?: string;
+ style?: React.CSSProperties;
}) {
const toneColor =
tone === "vermilion"
@@ -60,6 +62,7 @@ export function MetricTile({
"glass p-4 ring-focus transition-transform duration-200 hover:-translate-y-0.5",
className,
)}
+ style={style}
>
{label}
diff --git a/services/frontend/src/lib/utils.ts b/services/frontend/src/lib/utils.ts
index 365058c..183cf42 100644
--- a/services/frontend/src/lib/utils.ts
+++ b/services/frontend/src/lib/utils.ts
@@ -4,3 +4,16 @@ import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+
+/**
+ * Returns an inline style that staggers a list item's entrance animation.
+ * Pair with the `animate-stagger` class. Caps the delay so long lists still
+ * appear promptly.
+ */
+export function staggerDelay(
+ index: number,
+ step = 45,
+ max = 600,
+): React.CSSProperties {
+ return { animationDelay: `${Math.min(index * step, max)}ms` };
+}