diff --git a/resources/css/app.css b/resources/css/app.css
index e97d485..9d5b04f 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -660,6 +660,31 @@ html.guest-theme.dark {
50% { background-position: 100% 50%; }
}
+.guest-aurora {
+ background-size: 300% 300%;
+ animation: aurora 18s ease infinite;
+}
+
+.guest-aurora-soft {
+ background-size: 260% 260%;
+ animation: aurora 26s ease-in-out infinite;
+}
+
+@keyframes guest-cta-pulse {
+ 0%, 100% {
+ box-shadow: 0 12px 28px var(--cta-glow, rgba(0, 0, 0, 0.25)), 0 0 0 0 var(--cta-ring, rgba(0, 0, 0, 0));
+ transform: translateY(0);
+ }
+ 50% {
+ box-shadow: 0 18px 45px var(--cta-glow, rgba(0, 0, 0, 0.35)), 0 0 0 12px var(--cta-ring, rgba(0, 0, 0, 0.12));
+ transform: translateY(-1px);
+ }
+}
+
+.guest-cta-pulse {
+ animation: guest-cta-pulse 3.2s ease-in-out infinite;
+}
+
.bg-aurora-enhanced {
background: radial-gradient(circle at 20% 80%, #a8edea 0%, #fed6e3 50%, #d299c2 100%), linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%, 400% 400%;
@@ -672,6 +697,14 @@ html.guest-theme.dark {
animation: aurora 20s ease infinite;
}
+@media (prefers-reduced-motion: reduce) {
+ .guest-aurora,
+ .guest-aurora-soft,
+ .guest-cta-pulse {
+ animation: none;
+ }
+}
+
.guest-immersive .guest-header {
display: none !important;
}
diff --git a/resources/js/guest/components/BottomNav.tsx b/resources/js/guest/components/BottomNav.tsx
index 63769a5..ca81168 100644
--- a/resources/js/guest/components/BottomNav.tsx
+++ b/resources/js/guest/components/BottomNav.tsx
@@ -86,11 +86,12 @@ export default function BottomNav() {
return (
+
handleEmotionSelect(emotion)}
- className="group flex flex-col gap-2 rounded-2xl border border-muted/40 bg-white/80 px-4 py-3 text-left shadow-sm transition hover:-translate-y-0.5 hover:border-foreground/10 hover:shadow-md dark:border-gray-700/60 dark:bg-gray-900/70"
+ className="group relative flex flex-col gap-2 rounded-2xl p-[1px] text-left shadow-sm transition hover:-translate-y-0.5 hover:shadow-lg active:scale-[0.98]"
+ style={{
+ backgroundImage: 'linear-gradient(135deg, color-mix(in oklch, var(--guest-primary) 45%, white), color-mix(in oklch, var(--guest-secondary) 40%, white))',
+ }}
>
-
-
- {emotion.emoji}
-
-
-
{localizedName}
- {localizedDescription && (
-
{localizedDescription}
- )}
+
+
+
+ {emotion.emoji}
+
+
+
{localizedName}
+ {localizedDescription && (
+
{localizedDescription}
+ )}
+
+
-
);
diff --git a/resources/js/guest/components/GalleryPreview.tsx b/resources/js/guest/components/GalleryPreview.tsx
index 4e6ed74..76404d1 100644
--- a/resources/js/guest/components/GalleryPreview.tsx
+++ b/resources/js/guest/components/GalleryPreview.tsx
@@ -8,6 +8,7 @@ import { Heart } from 'lucide-react';
import { useTranslation } from '../i18n/useTranslation';
import { useEventBranding } from '../context/EventBrandingContext';
import { cn } from '@/lib/utils';
+import { motion } from 'framer-motion';
type Props = { token: string };
@@ -97,14 +98,16 @@ export default function GalleryPreview({ token }: Props) {
style={{ borderRadius: radius, fontFamily: bodyFont }}
>
-
+
-
Live-Galerie
+
+ Live-Galerie
+
Alle Uploads auf einen Blick
Alle ansehen →
@@ -112,28 +115,31 @@ export default function GalleryPreview({ token }: Props) {
-
- {filters.map((filter, index) => {
+
+ {filters.map((filter) => {
const isActive = mode === filter.value;
return (
-
-
- {index < filters.length - 1 && (
-
+
+ >
+ {isActive && (
+
+ )}
+
{filter.label}
+
);
})}
diff --git a/resources/js/guest/components/Header.tsx b/resources/js/guest/components/Header.tsx
index c780d6d..a279fcd 100644
--- a/resources/js/guest/components/Header.tsx
+++ b/resources/js/guest/components/Header.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { createPortal } from 'react-dom';
-import { Link, useLocation } from 'react-router-dom';
+import { Link } from 'react-router-dom';
import AppearanceToggleDropdown from '@/components/appearance-dropdown';
import {
User,
@@ -22,13 +22,12 @@ import {
} from 'lucide-react';
import { useEventData } from '../hooks/useEventData';
import { useOptionalEventStats } from '../context/EventStatsContext';
-import { useOptionalGuestIdentity } from '../context/GuestIdentityContext';
import { SettingsSheet } from './settings-sheet';
import { useTranslation, type TranslateFn } from '../i18n/useTranslation';
import { DEFAULT_EVENT_BRANDING, useOptionalEventBranding } from '../context/EventBrandingContext';
import { useOptionalNotificationCenter, type NotificationCenterValue } from '../context/NotificationCenterContext';
import { usePushSubscription } from '../hooks/usePushSubscription';
-import { getContrastingTextColor, relativeLuminance } from '../lib/color';
+import { getContrastingTextColor, relativeLuminance, hexToRgb } from '../lib/color';
import { isTaskModeEnabled } from '../lib/engagement';
const EVENT_ICON_COMPONENTS: Record
> = {
@@ -81,6 +80,14 @@ function getInitials(name: string): string {
return name.substring(0, 2).toUpperCase();
}
+function toRgba(value: string, alpha: number): string {
+ const rgb = hexToRgb(value);
+ if (!rgb) {
+ return `rgba(255, 255, 255, ${alpha})`;
+ }
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
+}
+
function EventAvatar({
name,
icon,
@@ -167,9 +174,7 @@ function EventAvatar({
}
export default function Header({ eventToken, title = '' }: { eventToken?: string; title?: string }) {
- const location = useLocation();
const statsContext = useOptionalEventStats();
- const identity = useOptionalGuestIdentity();
const { t } = useTranslation();
const brandingContext = useOptionalEventBranding();
const branding = brandingContext?.branding ?? DEFAULT_EVENT_BRANDING;
@@ -206,23 +211,6 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
return () => document.removeEventListener('mousedown', handler);
}, [notificationsOpen]);
- if (!eventToken) {
- return (
-
- );
- }
-
const headerFont = branding.typography?.heading ?? branding.fontFamily ?? undefined;
const bodyFont = branding.typography?.body ?? branding.fontFamily ?? undefined;
const logoPosition = branding.logo?.position ?? 'left';
@@ -231,16 +219,48 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
color: headerTextColor,
fontFamily: headerFont,
};
+ const headerGlowPrimary = toRgba(branding.primaryColor, 0.35);
+ const headerGlowSecondary = toRgba(branding.secondaryColor, 0.35);
+ const headerShimmer = `linear-gradient(120deg, ${toRgba(branding.primaryColor, 0.28)}, transparent 45%, ${toRgba(branding.secondaryColor, 0.32)})`;
+ const headerHairline = `linear-gradient(90deg, transparent, ${toRgba(headerTextColor, 0.4)}, transparent)`;
+
+ if (!eventToken) {
+ return (
+
+ );
+ }
const accentColor = branding.secondaryColor;
if (status === 'loading') {
return (
-
-
{t('header.loading')}
-
-
-
+
+
+
+
+
+
+
{t('header.loading')}
+
);
@@ -254,16 +274,20 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
statsContext && statsContext.eventKey === eventToken ? statsContext : undefined;
return (
+
+
+
+
- {event.name}
+ {event.name}
{stats && tasksEnabled && (
<>
@@ -295,7 +319,7 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
-
+
{notificationCenter && eventToken && (
();
const { name, hydrated } = useGuestIdentity();
@@ -56,6 +65,14 @@ export default function HomePage() {
}
}, [heroStorageKey]);
+ React.useEffect(() => {
+ if (typeof document === 'undefined') {
+ return;
+ }
+ document.body.classList.remove('guest-immersive');
+ document.body.classList.remove('guest-nav-visible');
+ }, []);
+
const dismissHero = React.useCallback(() => {
setHeroVisible(false);
if (typeof window === 'undefined') {
@@ -73,6 +90,13 @@ export default function HomePage() {
const eventNameDisplay = event?.name ?? t('home.hero.defaultEventName');
const accentColor = branding.primaryColor;
const secondaryAccent = branding.secondaryColor;
+ const glowPrimary = toRgba(accentColor, 0.35);
+ const glowSecondary = toRgba(secondaryAccent, 0.32);
+ const shimmerGradient = `linear-gradient(120deg, ${toRgba(accentColor, 0.22)}, transparent 45%, ${toRgba(secondaryAccent, 0.32)})`;
+ const welcomePanelStyle = React.useMemo(() => ({
+ background: `linear-gradient(135deg, color-mix(in oklch, ${accentColor} 18%, var(--background)), color-mix(in oklch, ${secondaryAccent} 16%, var(--background)))`,
+ borderColor: toRgba(accentColor, 0.25),
+ }), [accentColor, secondaryAccent]);
const uploadsRequireApproval =
(event?.guest_upload_visibility as 'immediate' | 'review' | undefined) !== 'immediate';
const tasksEnabled = isTaskModeEnabled(event);
@@ -80,6 +104,31 @@ export default function HomePage() {
const containerMotion = getMotionContainerProps(motionEnabled, STAGGER_FAST);
const fadeUpMotion = getMotionItemProps(motionEnabled, FADE_UP);
const fadeScaleMotion = getMotionItemProps(motionEnabled, FADE_SCALE);
+ const backdropStyle = React.useMemo(() => ({
+ '--guest-glow-primary': glowPrimary,
+ '--guest-glow-secondary': glowSecondary,
+ }) as React.CSSProperties & Record, [glowPrimary, glowSecondary]);
+
+ const renderWithBackdrop = (content: React.ReactNode) => (
+
+ );
const [missionDeck, setMissionDeck] = React.useState([]);
const [missionPool, setMissionPool] = React.useState([]);
@@ -293,17 +342,24 @@ export default function HomePage() {
const introMessage = introMessageRef.current;
if (!tasksEnabled) {
- return (
-
+ return renderWithBackdrop(
+
-
- {t('home.welcomeLine').replace('{name}', displayName)}
-
- {introMessage && {introMessage}
}
+
+
+
+ {t('home.welcomeLine').replace('{name}', displayName)}
+
+ {introMessage &&
{introMessage}
}
+
+
@@ -328,21 +384,28 @@ export default function HomePage() {
);
}
- return (
-
+ return renderWithBackdrop(
+
-
- {t('home.welcomeLine').replace('{name}', displayName)}
-
- {introMessage && (
-
- {introMessage}
-
- )}
+
+
+
+ {t('home.welcomeLine').replace('{name}', displayName)}
+
+ {introMessage && (
+
+ {introMessage}
+
+ )}
+
+
{heroVisible && (
@@ -507,6 +570,7 @@ export function MissionActionCard({
const bodyFont = branding.typography?.body ?? branding.fontFamily ?? undefined;
const cards = mission ? [mission, ...stack] : stack;
const shellRadius = `${radius + 10}px`;
+ const motionEnabled = !prefersReducedMotion();
const normalizeText = (value: string | undefined | null) =>
(value ?? '').trim().toLowerCase().replace(/\s+/g, ' ');
const [expandedTaskId, setExpandedTaskId] = React.useState(null);
@@ -516,8 +580,31 @@ export function MissionActionCard({
const [showSwipeHint, setShowSwipeHint] = React.useState(false);
const hintTimeoutRef = React.useRef(null);
const hintStorageKey = token ? `guestMissionSwipeHintSeen_${token}` : 'guestMissionSwipeHintSeen';
- const motionEnabled = !prefersReducedMotion();
const shouldShowHint = motionEnabled && cards.length > 1 && Boolean(swipeHintLabel);
+ const ctaPulseKey = token ? `guestMissionCtaPulse_${token}` : 'guestMissionCtaPulse';
+ const [ctaPulse, setCtaPulse] = React.useState(false);
+
+ React.useEffect(() => {
+ if (!motionEnabled || typeof window === 'undefined') {
+ return;
+ }
+ try {
+ if (window.sessionStorage.getItem(ctaPulseKey)) {
+ return;
+ }
+ } catch {
+ // ignore storage exceptions
+ }
+
+ setCtaPulse(true);
+ const timeout = window.setTimeout(() => setCtaPulse(false), 4800);
+ try {
+ window.sessionStorage.setItem(ctaPulseKey, '1');
+ } catch {
+ // ignore storage exceptions
+ }
+ return () => window.clearTimeout(timeout);
+ }, [ctaPulseKey, motionEnabled]);
const dismissSwipeHint = React.useCallback((persist = true) => {
if (!showSwipeHint) {
@@ -622,6 +709,13 @@ export function MissionActionCard({
const titleClamp = isExpanded ? '' : 'line-clamp-2 sm:line-clamp-3';
const titleClasses = `text-xl font-semibold leading-snug text-slate-900 dark:text-white sm:text-2xl break-words py-1 min-h-[3.75rem] sm:min-h-[4.5rem] ${titleClamp}`;
const titleId = card ? `task-title-${card.id}` : undefined;
+ const ctaStyles = {
+ borderRadius: `${radius}px`,
+ background: `linear-gradient(120deg, ${primary}, ${secondary})`,
+ boxShadow: `0 12px 28px ${primary}25`,
+ '--cta-glow': toRgba(primary, 0.35),
+ '--cta-ring': toRgba(primary, 0.16),
+ } as React.CSSProperties & Record;
const toggleExpanded = () => {
if (!card) return;
setExpandedTaskId((prev) => (prev === card.id ? null : card.id));
@@ -748,12 +842,8 @@ export function MissionActionCard({