diff --git a/src/website/src/app/(programs)/africa-clean-air-forum-2026/selfies/page.tsx b/src/website/src/app/(programs)/africa-clean-air-forum-2026/selfies/page.tsx
new file mode 100644
index 0000000000..21dd65188b
--- /dev/null
+++ b/src/website/src/app/(programs)/africa-clean-air-forum-2026/selfies/page.tsx
@@ -0,0 +1,15 @@
+import SelfiesScreen from '@/features/clean-air-forum-2026/screens/SelfiesScreen';
+import { generateMetadata } from '@/lib/metadata';
+
+export const metadata = generateMetadata({
+ title: 'Selfies | Faces of Clean Air',
+ description:
+ 'View selfies shared by attendees of the Africa Clean Air Forum showcasing air quality readings from their locations.',
+ keywords:
+ 'Faces of Clean Air selfies, Africa Clean Air Forum photos, air quality advocates, PM2.5 readings Africa, clean air community',
+ url: '/africa-clean-air-forum-2026/selfies',
+});
+
+export default function SelfiesRoute() {
+ return ;
+}
diff --git a/src/website/src/components/clean-air-forum-2026/QrCodeButton.tsx b/src/website/src/components/clean-air-forum-2026/QrCodeButton.tsx
index fcabb70268..dc7cb35c73 100644
--- a/src/website/src/components/clean-air-forum-2026/QrCodeButton.tsx
+++ b/src/website/src/components/clean-air-forum-2026/QrCodeButton.tsx
@@ -19,16 +19,16 @@ export default function QrCodeButton({ src = QR_IMAGE_SRC }: { src?: string }) {
aria-label="Open QR code in full view"
>
-
+
-
+
Scan to participate
diff --git a/src/website/src/components/clean-air-forum-2026/selfie-components.tsx b/src/website/src/components/clean-air-forum-2026/selfie-components.tsx
new file mode 100644
index 0000000000..98eea43a14
--- /dev/null
+++ b/src/website/src/components/clean-air-forum-2026/selfie-components.tsx
@@ -0,0 +1,462 @@
+'use client';
+
+import {
+ motion,
+ type PanInfo,
+ useMotionValue,
+ useSpring,
+ useTransform,
+ type Variants,
+} from 'framer-motion';
+import Image from 'next/image';
+import {
+ type KeyboardEvent,
+ type PointerEvent as ReactPointerEvent,
+ useCallback,
+ useEffect,
+ useState,
+} from 'react';
+import { FiCamera } from 'react-icons/fi';
+
+import type { CleanAirSubmissionWithId } from '@/services/external/faces-of-clean-air.service';
+
+export const CAROUSEL_INTERVAL_MS = 7600;
+export const SWIPE_DISTANCE_THRESHOLD = 70;
+export const SWIPE_VELOCITY_THRESHOLD = 500;
+export const MOBILE_MEDIA_QUERY = '(max-width: 639px)';
+export const DESKTOP_CARDS_PER_PAGE = 8;
+
+const SMOOTH_SPRING = { stiffness: 150, damping: 25, mass: 0.9 };
+
+export const headerContainerVariants: Variants = {
+ hidden: {},
+ visible: {
+ transition: { delayChildren: 0.1, staggerChildren: 0.16 },
+ },
+};
+
+export const headerItemVariants: Variants = {
+ hidden: { opacity: 0, y: -20 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.78, ease: [0.22, 1, 0.36, 1] },
+ },
+};
+
+const cardVariants: Variants = {
+ enter: { opacity: 0, y: 28, scale: 0.94 },
+ center: { opacity: 1, y: 0, scale: 1 },
+ exit: { opacity: 0, y: -16, scale: 0.96 },
+};
+
+export function useMediaQuery(query: string): boolean {
+ const [matches, setMatches] = useState(false);
+
+ useEffect(() => {
+ const mediaQuery = window.matchMedia(query);
+ const updateMatch = () => setMatches(mediaQuery.matches);
+ updateMatch();
+ mediaQuery.addEventListener('change', updateMatch);
+ return () => mediaQuery.removeEventListener('change', updateMatch);
+ }, [query]);
+
+ return matches;
+}
+
+export function FaceCard({
+ submission,
+ priority,
+ reduceMotion,
+}: {
+ submission: CleanAirSubmissionWithId;
+ priority: boolean;
+ reduceMotion: boolean | null;
+}) {
+ const pointerX = useMotionValue(0.5);
+ const pointerY = useMotionValue(0.5);
+
+ const transformedRotateX = useTransform(pointerY, [0, 1], [5, -5]);
+ const transformedRotateY = useTransform(pointerX, [0, 1], [-5, 5]);
+
+ const rotateX = useSpring(transformedRotateX, SMOOTH_SPRING);
+ const rotateY = useSpring(transformedRotateY, SMOOTH_SPRING);
+
+ const displayName = submission.displayName?.trim() || 'Clean Air Champion';
+
+ const location =
+ submission.locationName?.trim() || 'the Africa Clean Air Forum';
+
+ const handlePointerMove = useCallback(
+ (event: ReactPointerEvent
) => {
+ if (reduceMotion || event.pointerType !== 'mouse') return;
+
+ const cardBounds = event.currentTarget.getBoundingClientRect();
+ const relativeX = (event.clientX - cardBounds.left) / cardBounds.width;
+ const relativeY = (event.clientY - cardBounds.top) / cardBounds.height;
+
+ pointerX.set(Math.min(Math.max(relativeX, 0), 1));
+ pointerY.set(Math.min(Math.max(relativeY, 0), 1));
+ },
+ [pointerX, pointerY, reduceMotion],
+ );
+
+ const resetPointerPosition = useCallback(() => {
+ pointerX.set(0.5);
+ pointerY.set(0.5);
+ }, [pointerX, pointerY]);
+
+ return (
+
+
+
+ );
+}
+
+export function SkeletonCard({
+ index,
+ reduceMotion,
+}: {
+ index: number;
+ reduceMotion: boolean | null;
+}) {
+ return (
+
+
+
+ {!reduceMotion && (
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+export function SelfieEmptyState({
+ reduceMotion,
+}: {
+ reduceMotion: boolean | null;
+}) {
+ return (
+
+
+ {!reduceMotion && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+
+ No faces yet
+
+
+ Be the first to share an air quality selfie from the Africa Clean Air
+ Forum.
+
+
+ );
+}
+
+export function SelfieErrorState({
+ onRetry,
+ reduceMotion,
+}: {
+ onRetry: () => void;
+ reduceMotion: boolean | null;
+}) {
+ return (
+
+
+ Could not load selfies
+
+
+
+ Please check the connection and try again.
+
+
+
+ Try again
+
+
+ );
+}
+
+export function SelfieDesktopPagination({
+ page,
+ totalPages,
+ isPaused,
+ reduceMotion,
+ onPageChange,
+ layoutId = 'active-carousel-indicator',
+}: {
+ page: number;
+ totalPages: number;
+ isPaused: boolean;
+ reduceMotion: boolean | null;
+ onPageChange: (page: number) => void;
+ layoutId?: string;
+}) {
+ return (
+
+ {Array.from({ length: totalPages }).map((_, index) => {
+ const isActive = index === page;
+
+ return (
+ onPageChange(index)}
+ aria-label={`Show selfie page ${index + 1} of ${totalPages}`}
+ aria-current={isActive ? 'page' : undefined}
+ whileHover={reduceMotion ? undefined : { scale: 1.1, y: -1 }}
+ whileTap={reduceMotion ? undefined : { scale: 0.92 }}
+ transition={{ type: 'spring', stiffness: 320, damping: 30 }}
+ className={`relative h-2.5 overflow-hidden rounded-full border border-white/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-transparent ${
+ isActive
+ ? 'w-11 bg-white/30'
+ : 'w-2.5 bg-white/30 hover:bg-white/50'
+ }`}
+ >
+ {isActive && (
+ <>
+
+
+ {!reduceMotion && !isPaused && (
+
+ )}
+
+ {(reduceMotion || isPaused) && (
+
+ )}
+ >
+ )}
+
+ );
+ })}
+
+ );
+}
+
+export function SelfieMobilePagination({
+ page,
+ totalPages,
+ reduceMotion,
+}: {
+ page: number;
+ totalPages: number;
+ reduceMotion: boolean | null;
+}) {
+ return (
+
+
+
+
+
+
+ {page + 1}/{totalPages}
+
+
+ );
+}
+
+export function useSelfieCarouselKeyboard(
+ totalSlides: number,
+ goToRelativePage: (offset: number) => void,
+) {
+ return useCallback(
+ (event: KeyboardEvent) => {
+ if (totalSlides <= 1) return;
+ if (event.key === 'ArrowRight') {
+ event.preventDefault();
+ goToRelativePage(1);
+ }
+ if (event.key === 'ArrowLeft') {
+ event.preventDefault();
+ goToRelativePage(-1);
+ }
+ },
+ [goToRelativePage, totalSlides],
+ );
+}
+
+export function useSelfieDragEnd(
+ totalSlides: number,
+ goToRelativePage: (offset: number) => void,
+) {
+ return useCallback(
+ (
+ _event: MouseEvent | TouchEvent | globalThis.PointerEvent,
+ info: PanInfo,
+ ) => {
+ if (totalSlides <= 1) return;
+
+ const movedLeft =
+ info.offset.x < -SWIPE_DISTANCE_THRESHOLD ||
+ info.velocity.x < -SWIPE_VELOCITY_THRESHOLD;
+ const movedRight =
+ info.offset.x > SWIPE_DISTANCE_THRESHOLD ||
+ info.velocity.x > SWIPE_VELOCITY_THRESHOLD;
+
+ if (movedLeft) goToRelativePage(1);
+ else if (movedRight) goToRelativePage(-1);
+ },
+ [goToRelativePage, totalSlides],
+ );
+}
diff --git a/src/website/src/components/layout/Footer.tsx b/src/website/src/components/layout/Footer.tsx
index 14841fcd19..f89b01a375 100644
--- a/src/website/src/components/layout/Footer.tsx
+++ b/src/website/src/components/layout/Footer.tsx
@@ -43,6 +43,7 @@ const footerLinks = {
label: 'Faces of Clean Air',
href: '/africa-clean-air-forum-2026/faces-of-clean-air',
},
+ { label: 'Clean Air Faces', href: '/africa-clean-air-forum-2026/selfies' },
{ label: 'Air Quality Quiz', href: '/africa-clean-air-forum-2026/quiz' },
{
label: 'Quiz Leaderboard',
diff --git a/src/website/src/features/clean-air-forum-2026/screens/LandingScreen.tsx b/src/website/src/features/clean-air-forum-2026/screens/LandingScreen.tsx
index 1ad1c39297..d428062f07 100644
--- a/src/website/src/features/clean-air-forum-2026/screens/LandingScreen.tsx
+++ b/src/website/src/features/clean-air-forum-2026/screens/LandingScreen.tsx
@@ -7,7 +7,6 @@ import { useRouter } from 'next/navigation';
import { FormEvent, useState } from 'react';
import AmbientBackground from '@/components/clean-air-forum-2026/AmbientBackground';
-import QrCodeButton from '@/components/clean-air-forum-2026/QrCodeButton';
import Screen from '@/components/clean-air-forum-2026/Screen';
import { Button } from '@/components/ui/button';
import {
@@ -220,8 +219,6 @@ export default function LandingScreen() {
-
-