diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx
index 7834ef9..c19285c 100644
--- a/app/(auth)/layout.tsx
+++ b/app/(auth)/layout.tsx
@@ -1,8 +1,7 @@
import type { Metadata } from "next";
-import { AuthProvider } from "@/components/console/AuthContext";
export const metadata: Metadata = {
- title: "Sign in — Livepeer Early Access",
+ title: "Livepeer Early Access",
description: "Sign in or create an account to access Livepeer Early Access.",
};
@@ -12,10 +11,8 @@ export default function ConsoleAuthLayout({
children: React.ReactNode;
}) {
return (
-
-
- {children}
-
-
+
-
-
-
-
-
-
-
-
- {mode === "signin"
- ? "Log in to Livepeer Early Access"
- : "Get started with Livepeer"}
-
-
-
-
-
-
- {mode === "signin" ? (
- <>
- Don't have an account?{" "}
-
- Sign up
-
- >
- ) : (
- <>
- Already have an account?{" "}
-
- Log in
-
- >
- )}
-
-
-
- {mode === "signup" && (
-
- By creating an account, you agree to our{" "}
-
- Terms
- {" "}
- and{" "}
-
- Privacy Policy
-
- .
-
- )}
-
-
+
+
+
-
-
-
+
);
}
diff --git a/components/console/auth/AuthMediaRing.tsx b/components/console/auth/AuthMediaRing.tsx
new file mode 100644
index 0000000..fbf22ac
--- /dev/null
+++ b/components/console/auth/AuthMediaRing.tsx
@@ -0,0 +1,210 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { motion } from "framer-motion";
+
+type MediaRingItem = {
+ src: string;
+ alt: string;
+};
+
+const RING = {
+ cardWidth: 286,
+ cardAspectRatio: 4 / 3,
+ radiusX: 620,
+ radiusY: 360,
+ spinSpeed: 2.5,
+ cardTiltDegrees: 5,
+ pathTiltDegrees: -18,
+ introDurationMs: 1100,
+} as const;
+
+export const DEFAULT_MEDIA_RING_ITEMS: MediaRingItem[] = [
+ {
+ src: "/images/console/explore/stable-video-diffusion.webp",
+ alt: "Stable Video Diffusion preview",
+ },
+ {
+ src: "/images/console/explore/img2img-sdxl.webp",
+ alt: "Image editing preview",
+ },
+ {
+ src: "/images/console/explore/live-video-to-video.webp",
+ alt: "Live video preview",
+ },
+ {
+ src: "/images/console/explore/flux-schnell.webp",
+ alt: "Flux Schnell preview",
+ },
+ { src: "/images/console/daydream.png", alt: "Daydream preview" },
+ { src: "/images/console/explore/sdxl-turbo.webp", alt: "SDXL Turbo preview" },
+ {
+ src: "/images/console/explore/real-esrgan-4x.webp",
+ alt: "Image upscale preview",
+ },
+];
+
+interface AuthMediaRingProps {
+ items?: MediaRingItem[];
+}
+
+export function AuthMediaRing({
+ items = DEFAULT_MEDIA_RING_ITEMS,
+}: AuthMediaRingProps) {
+ const rootRef = useRef
(null);
+ const cardRefs = useRef>([]);
+ const loadedImagesRef = useRef(new Set());
+ const rotationRef = useRef(-16);
+ const [isReady, setIsReady] = useState(false);
+
+ function markImageReady(index: number) {
+ loadedImagesRef.current.add(index);
+ if (loadedImagesRef.current.size >= items.length) setIsReady(true);
+ }
+
+ useEffect(() => {
+ const fallback = window.setTimeout(() => setIsReady(true), 900);
+ return () => window.clearTimeout(fallback);
+ }, []);
+
+ useEffect(() => {
+ if (!isReady) return;
+
+ const prefersReducedMotion = window.matchMedia(
+ "(prefers-reduced-motion: reduce)"
+ ).matches;
+ const cardHeight = RING.cardWidth * RING.cardAspectRatio;
+ const pathTilt = (RING.pathTiltDegrees * Math.PI) / 180;
+ let animationFrame = 0;
+ let introStartTime = 0;
+ let lastTime = 0;
+
+ const render = (rotation: number, introProgress: number) => {
+ const root = rootRef.current;
+ if (!root || items.length === 0) return;
+
+ const viewportScale = Math.min(
+ 1.16,
+ Math.max(0.7, root.clientWidth / 1120)
+ );
+ const width = RING.cardWidth * viewportScale;
+ const height = cardHeight * viewportScale;
+
+ cardRefs.current.forEach((card, index) => {
+ if (!card) return;
+ const radians =
+ (index / items.length) * Math.PI * 2 + (rotation * Math.PI) / 180;
+ const depth = (Math.sin(radians) + 1) / 2;
+ const rawX =
+ Math.cos(radians) *
+ RING.radiusX *
+ viewportScale *
+ (0.74 + depth * 0.26);
+ const rawY =
+ Math.sin(radians) *
+ RING.radiusY *
+ viewportScale *
+ (0.88 + depth * 0.12);
+ const targetX = rawX * Math.cos(pathTilt) - rawY * Math.sin(pathTilt);
+ const targetY = rawX * Math.sin(pathTilt) + rawY * Math.cos(pathTilt);
+ const targetScale = 0.7 + depth * 0.32;
+ const x = targetX * introProgress;
+ const y = targetY * introProgress;
+ const scale = 0.42 + (targetScale - 0.42) * introProgress;
+ const blur = (1 - depth) * 8 + (1 - introProgress) * 4;
+ const image = card.firstElementChild as HTMLElement | null;
+
+ card.style.width = `${width}px`;
+ card.style.height = `${height}px`;
+ card.style.marginLeft = `${-width / 2}px`;
+ card.style.marginTop = `${-height / 2}px`;
+ card.style.opacity = `${Math.min(1, introProgress * 1.2)}`;
+ card.style.zIndex = `${Math.round(depth * 1000)}`;
+ card.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale}) rotate(${RING.cardTiltDegrees}deg)`;
+ card.style.boxShadow = `0 ${8 + depth * 14}px ${22 + depth * 28}px rgba(0,0,0,${0.04 + depth * 0.08})`;
+
+ if (image) {
+ image.style.filter = `blur(${blur}px)`;
+ image.style.transform = `scale(${1 + blur * 0.012})`;
+ }
+ });
+ };
+
+ const renderFinalFrame = () => render(rotationRef.current, 1);
+
+ if (prefersReducedMotion) {
+ renderFinalFrame();
+ window.addEventListener("resize", renderFinalFrame);
+ return () => window.removeEventListener("resize", renderFinalFrame);
+ }
+
+ const tick = (now: number) => {
+ if (!introStartTime) introStartTime = now;
+ if (!lastTime) lastTime = now;
+ const deltaSeconds = (now - lastTime) / 1000;
+ const linearProgress = Math.min(
+ 1,
+ (now - introStartTime) / RING.introDurationMs
+ );
+ const introProgress = 1 - Math.pow(1 - linearProgress, 3);
+ lastTime = now;
+ rotationRef.current += RING.spinSpeed * deltaSeconds;
+ render(rotationRef.current, introProgress);
+ animationFrame = window.requestAnimationFrame(tick);
+ };
+
+ window.addEventListener("resize", renderFinalFrame);
+ animationFrame = window.requestAnimationFrame(tick);
+ return () => {
+ window.cancelAnimationFrame(animationFrame);
+ window.removeEventListener("resize", renderFinalFrame);
+ };
+ }, [isReady, items]);
+
+ return (
+
+
+
+
+ {items.map((item, index) => (
+
{
+ cardRefs.current[index] = node;
+ }}
+ className="absolute left-1/2 top-1/2 aspect-[3/4] overflow-hidden rounded-[14px] bg-muted opacity-0 ring-1 ring-border/40 will-change-transform sm:rounded-[18px] md:rounded-[22px]"
+ style={{
+ width: RING.cardWidth,
+ height: RING.cardWidth * RING.cardAspectRatio,
+ marginLeft: -RING.cardWidth / 2,
+ marginTop: -(RING.cardWidth * RING.cardAspectRatio) / 2,
+ transformStyle: "preserve-3d",
+ backfaceVisibility: "hidden",
+ }}
+ >
+

markImageReady(index)}
+ onError={() => markImageReady(index)}
+ className="block h-full w-full object-cover will-change-transform"
+ />
+
+ ))}
+
+
+
+ );
+}
diff --git a/components/console/auth/AuthPanel.tsx b/components/console/auth/AuthPanel.tsx
new file mode 100644
index 0000000..f8aba9b
--- /dev/null
+++ b/components/console/auth/AuthPanel.tsx
@@ -0,0 +1,137 @@
+"use client";
+
+import Link from "next/link";
+import type { FormEvent } from "react";
+import { motion } from "framer-motion";
+import { LivepeerLockup } from "@/components/design-system/LivepeerLogo";
+
+export type AuthMode = "signin" | "signup";
+
+interface AuthPanelProps {
+ mode: AuthMode;
+ onContinue?: () => void;
+}
+
+const COPY = {
+ signin: {
+ submit: "Sign in",
+ google: "Sign in with Google",
+ footer: "Don't have an account?",
+ footerAction: "Sign up",
+ footerHref: "/signup",
+ },
+ signup: {
+ submit: "Sign up",
+ google: "Sign up with Google",
+ footer: "Already have an account?",
+ footerAction: "Log in",
+ footerHref: "/login",
+ },
+} as const;
+
+function GoogleIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
+
+export function AuthPanel({ mode, onContinue }: AuthPanelProps) {
+ const copy = COPY[mode];
+ const providerButtonClass =
+ "inline-flex h-11 w-full items-center justify-center gap-2.5 rounded-sm border border-border bg-background px-4 text-sm font-medium text-foreground transition-colors hover:bg-foreground/3 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/35";
+ const primaryButtonClass =
+ "inline-flex h-11 w-full items-center justify-center rounded-sm bg-foreground px-4 text-sm font-medium text-background transition-colors hover:bg-foreground/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/35";
+ const inputClass =
+ "h-11 w-full rounded-sm border border-border bg-background px-3 text-sm text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-muted-foreground";
+
+ function continuePreview(event: FormEvent) {
+ event.preventDefault();
+ onContinue?.();
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {copy.footer}{" "}
+
+ {copy.footerAction}
+
+
+
+
+ );
+}