diff --git a/public/avalon/css-banner.mp4 b/public/avalon/css-banner.mp4
new file mode 100644
index 0000000..a1dd559
Binary files /dev/null and b/public/avalon/css-banner.mp4 differ
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 447e6d6..94f5040 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -32,6 +32,8 @@ import {
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { DiscordBannerPermanent } from "@/components/ui/discord-banner-permanent";
+import { CssEditorBanner } from "@/components/ui/css-editor-banner";
+import { BannerCarousel } from "@/components/ui/banner-carousel";
import Script from "next/script";
interface CardDataV2 {
@@ -447,12 +449,35 @@ export default function Home() {
{/* Discord Banner - Disabled if NEXT_PUBLIC_DISABLE_DISCORD_BANNER is set */}
{process.env.NEXT_PUBLIC_DISABLE_DISCORD_BANNER !== "true" && (
-
+ <>
+ {/* Desktop: banners side by side */}
+
diff --git a/src/components/ui/banner-carousel.tsx b/src/components/ui/banner-carousel.tsx
new file mode 100644
index 0000000..37a0a03
--- /dev/null
+++ b/src/components/ui/banner-carousel.tsx
@@ -0,0 +1,85 @@
+"use client";
+
+import { useState, useEffect, ReactNode } from "react";
+import { ChevronLeft, ChevronRight } from "lucide-react";
+
+interface BannerCarouselProps {
+ slides: ReactNode[];
+ intervalMs?: number;
+ className?: string;
+}
+
+export function BannerCarousel({
+ slides,
+ intervalMs = 8000,
+ className = "",
+}: BannerCarouselProps) {
+ const [index, setIndex] = useState(0);
+
+ // Pick a random starting slide on the client to avoid hydration mismatch
+ useEffect(() => {
+ setIndex(Math.floor(Math.random() * slides.length));
+ }, [slides.length]);
+
+ // Re-keyed on index so a manual change restarts the countdown
+ useEffect(() => {
+ const id = setTimeout(
+ () => setIndex((i) => (i + 1) % slides.length),
+ intervalMs
+ );
+ return () => clearTimeout(id);
+ }, [index, intervalMs, slides.length]);
+
+ const go = (delta: number) =>
+ setIndex((i) => (i + delta + slides.length) % slides.length);
+
+ return (
+
+ {/* Slides stacked in one grid cell so the container fits the tallest */}
+
+ {slides.map((slide, i) => (
+
+ {slide}
+
+ ))}
+
+
+
+
+
+
+ {slides.map((_, i) => (
+
+
+ );
+}
diff --git a/src/components/ui/css-editor-banner.tsx b/src/components/ui/css-editor-banner.tsx
new file mode 100644
index 0000000..cfa5c3f
--- /dev/null
+++ b/src/components/ui/css-editor-banner.tsx
@@ -0,0 +1,100 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+
+interface CssEditorBannerProps {
+ href?: string;
+ className?: string;
+ /** "contain" letterboxes the video so its caption is never cropped */
+ fit?: "cover" | "contain";
+}
+
+export function CssEditorBanner({
+ href = "https://tools.from-avalon.com/janitor-profile-maker/",
+ className = "",
+ fit = "cover",
+}: CssEditorBannerProps) {
+ const videoRef = useRef
(null);
+ const canvasRef = useRef(null);
+
+ // Browsers like Opera GX overlay pop-out controls on large