diff --git a/src/App.jsx b/src/App.jsx
index ff7a350..f4c3b84 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,7 +1,7 @@
import React, { Suspense, useEffect, useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { version as appVersion } from "../package.json";
-import TopBar from "./components/Layout/TopBar";
+import TopBar from "./components/Layout/TopBarNew";
import PostList from "./components/Posts/PostList";
import ContentFilters from "./components/Filters/ContentFilters";
import IslandUniverse from "./components/Island/IslandUniverse";
diff --git a/src/components/Layout/TopBarNew.jsx b/src/components/Layout/TopBarNew.jsx
new file mode 100644
index 0000000..383a471
--- /dev/null
+++ b/src/components/Layout/TopBarNew.jsx
@@ -0,0 +1,865 @@
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import { motion, AnimatePresence } from "framer-motion";
+import { useTranslation } from "react-i18next";
+import { useAuth } from "../../auth/AuthContext";
+import { registerUser, updateProfileUsername, fetchFriendRequests, toSmallImageUrl } from "../../api/client";
+import AuthToast from "../Auth/AuthToast";
+import { promptInstall, isRunningAsPWA, canInstall } from '../../pwaInstall';
+import { trackEvent } from "../../utils/analytics";
+import FriendsList from "../Friends/FriendsList";
+
+const SUPPORTED_LANGS = ['en', 'fr', 'es'];
+const THEMES = ['dark', 'blue', 'light'];
+const LAST_SIGNUP_KEY = "sociowire:lastSignup";
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Utilities
+// ─────────────────────────────────────────────────────────────────────────────
+
+function initialLetter(name = "") {
+ const s = (name || "").trim();
+ return s ? s[0].toUpperCase() : "G";
+}
+
+function truthyParam(v) {
+ if (!v) return false;
+ const x = String(v).toLowerCase().trim();
+ return x === "1" || x === "true" || x === "yes" || x === "ok";
+}
+
+function readLastSignup() {
+ try {
+ const raw = localStorage.getItem(LAST_SIGNUP_KEY);
+ if (!raw) return null;
+ const j = JSON.parse(raw);
+ if (!j || typeof j !== "object") return null;
+ return { username: (j.username || "").toString().trim(), email: (j.email || "").toString().trim() };
+ } catch { return null; }
+}
+
+function parseMergedParams() {
+ const search = window.location.search || "";
+ const hash = window.location.hash || "";
+ const qs = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
+ let hashQs = "";
+ if (hash.includes("?")) hashQs = hash.slice(hash.indexOf("?") + 1);
+ const hs = new URLSearchParams(hashQs);
+ const get = (k) => {
+ const a = qs.get(k);
+ if (a != null && a !== "") return a;
+ const b = hs.get(k);
+ return b != null ? b : null;
+ };
+ return { qs, hs, get, hash };
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Animation Variants
+// ─────────────────────────────────────────────────────────────────────────────
+
+const dropdownVariants = {
+ hidden: { opacity: 0, y: -8, scale: 0.95 },
+ visible: { opacity: 1, y: 0, scale: 1, transition: { duration: 0.15, ease: "easeOut" } },
+ exit: { opacity: 0, y: -8, scale: 0.95, transition: { duration: 0.1 } },
+};
+
+const buttonVariants = {
+ hover: { scale: 1.05, y: -2 },
+ tap: { scale: 0.95 },
+};
+
+const badgeVariants = {
+ initial: { scale: 0 },
+ animate: { scale: 1, transition: { type: "spring", stiffness: 500, damping: 25 } },
+};
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Sub-components
+// ─────────────────────────────────────────────────────────────────────────────
+
+function ActionButton({ icon, label, active, color = "default", badge, onClick, title, disabled }) {
+ const colorClasses = {
+ default: "from-surface-800/80 to-surface-700/80 border-surface-600/30 text-surface-100 hover:border-accent/50 hover:shadow-glow-sm",
+ green: "from-emerald-600 to-emerald-700 border-emerald-500/50 text-white hover:from-emerald-500 hover:to-emerald-600",
+ purple: "from-violet-600 to-purple-700 border-violet-500/50 text-white hover:from-violet-500 hover:to-purple-600",
+ amber: "from-amber-500 to-orange-600 border-amber-400/50 text-white hover:from-amber-400 hover:to-orange-500",
+ cyan: "from-cyan-500/20 to-cyan-600/20 border-cyan-400/40 text-cyan-100 hover:border-cyan-300/60",
+ };
+
+ return (
+
+
+ {label && {label}}
+
+ {badge > 0 && (
+
+ {badge}
+
+ )}
+
+ );
+}
+
+function DropdownMenu({ show, position, children, onClose }) {
+ const menuRef = useRef(null);
+
+ useEffect(() => {
+ if (!show) return;
+ const handleClick = (e) => {
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
+ onClose?.();
+ }
+ };
+ document.addEventListener("mousedown", handleClick);
+ return () => document.removeEventListener("mousedown", handleClick);
+ }, [show, onClose]);
+
+ return (
+
+ {show && (
+
+ {children}
+
+ )}
+
+ );
+}
+
+function DropdownOption({ active, onClick, children }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function UserChip({ authenticated, loading, username, avatarUrl, onClick }) {
+ const { t } = useTranslation();
+
+ const displayUsername = useMemo(() => {
+ const raw = (username || "user").toString();
+ if (raw.includes("@")) return "user";
+ if (raw.length <= 9) return raw;
+ return raw.slice(0, 9) + "..";
+ }, [username]);
+
+ return (
+
+ {/* Avatar */}
+
+ {avatarUrl ? (
+
})
+ ) : (
+
+ {authenticated ? initialLetter(username) : }
+
+ )}
+
+
+ {/* Text - hidden on mobile */}
+
+
+ {loading ? t('common.loading') : authenticated ? t('common.online') : t('common.guest')}
+
+
+ @{authenticated ? displayUsername : t('common.anon')}
+
+
+
+ );
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Main Component
+// ─────────────────────────────────────────────────────────────────────────────
+
+export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClick, onUserIsland }) {
+ const { t, i18n } = useTranslation();
+ const {
+ loading, authenticated, username, login, logout, restore, loginWithProvider, ssoEnabled,
+ avatarUrl, profileLoaded, needsUsername, needsEmailVerification, lastError, lastInfo, clearError, clearInfo,
+ } = useAuth();
+
+ // Dropdown states
+ const [showLangMenu, setShowLangMenu] = useState(false);
+ const [langMenuPos, setLangMenuPos] = useState({ top: 0, right: 0 });
+ const langBtnRef = useRef(null);
+
+ const [showThemeMenu, setShowThemeMenu] = useState(false);
+ const [themeMenuPos, setThemeMenuPos] = useState({ top: 0, right: 0 });
+ const themeBtnRef = useRef(null);
+
+ const [showAuth, setShowAuth] = useState(false);
+ const [mode, setMode] = useState("login");
+ const [showUsernameModal, setShowUsernameModal] = useState(false);
+ const [showFriends, setShowFriends] = useState(false);
+ const [pendingRequestsCount, setPendingRequestsCount] = useState(0);
+ const [showInstallBtn, setShowInstallBtn] = useState(false);
+
+ // Form states
+ const [loginUser, setLoginUser] = useState("");
+ const [loginPass, setLoginPass] = useState("");
+ const [firstName, setFirstName] = useState("");
+ const [lastName, setLastName] = useState("");
+ const [regUser, setRegUser] = useState("");
+ const [regEmail, setRegEmail] = useState("");
+ const [regPass, setRegPass] = useState("");
+ const [signupError, setSignupError] = useState("");
+ const [signupInfo, setSignupInfo] = useState("");
+ const [signupLoading, setSignupLoading] = useState(false);
+ const [authNotice, setAuthNotice] = useState("");
+ const [desiredUsername, setDesiredUsername] = useState("");
+ const [usernameError, setUsernameError] = useState("");
+ const [usernameInfo, setUsernameInfo] = useState("");
+ const [usernameSaving, setUsernameSaving] = useState(false);
+
+ const usernamePromptTimer = useRef(null);
+
+ // Toggle functions
+ const toggleLangMenu = () => {
+ if (!showLangMenu && langBtnRef.current) {
+ const rect = langBtnRef.current.getBoundingClientRect();
+ setLangMenuPos({ top: rect.bottom + 8, right: window.innerWidth - rect.right });
+ }
+ setShowLangMenu(!showLangMenu);
+ setShowThemeMenu(false);
+ };
+
+ const toggleThemeMenu = () => {
+ if (!showThemeMenu && themeBtnRef.current) {
+ const rect = themeBtnRef.current.getBoundingClientRect();
+ setThemeMenuPos({ top: rect.bottom + 8, right: window.innerWidth - rect.right });
+ }
+ setShowThemeMenu(!showThemeMenu);
+ setShowLangMenu(false);
+ };
+
+ const openModal = (nextMode) => {
+ setMode(nextMode);
+ setShowAuth(true);
+ setSignupError("");
+ trackEvent("auth_modal_open", { mode: nextMode || "login" });
+ };
+
+ const closeModal = () => {
+ setShowAuth(false);
+ setLoginPass("");
+ setSignupError("");
+ setAuthNotice("");
+ };
+
+ // Reset password URL
+ const resetPasswordUrl = useMemo(() => {
+ const base = (import.meta?.env?.VITE_KC_URL || "https://auth.sociowire.com").replace(/\/+$/, "");
+ const realm = import.meta?.env?.VITE_KC_REALM || "sociowire";
+ const clientId = import.meta?.env?.VITE_KC_CLIENT_ID || "sociowire-frontend";
+ const redirectUri = window.location.origin + window.location.pathname;
+ return `${base}/realms/${realm}/login-actions/reset-credentials?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}`;
+ }, []);
+
+ // Effects
+ useEffect(() => {
+ if (authenticated) setShowAuth(false);
+ }, [authenticated]);
+
+ useEffect(() => {
+ if (!authenticated) { setPendingRequestsCount(0); return; }
+ const loadRequests = async () => {
+ try {
+ const res = await fetchFriendRequests();
+ const received = (res?.requests || []).filter(r => r?.direction === 'received');
+ setPendingRequestsCount(received.length);
+ } catch { setPendingRequestsCount(0); }
+ };
+ loadRequests();
+ const interval = setInterval(loadRequests, 30000);
+ return () => clearInterval(interval);
+ }, [authenticated]);
+
+ useEffect(() => {
+ const checkInstall = () => {
+ if (isRunningAsPWA()) { setShowInstallBtn(false); return; }
+ setShowInstallBtn(canInstall());
+ };
+ checkInstall();
+ const handlePrompt = () => setTimeout(checkInstall, 100);
+ window.addEventListener('beforeinstallprompt', handlePrompt);
+ window.addEventListener('appinstalled', () => setShowInstallBtn(false));
+ return () => {
+ window.removeEventListener('beforeinstallprompt', handlePrompt);
+ window.removeEventListener('appinstalled', () => setShowInstallBtn(false));
+ };
+ }, []);
+
+ useEffect(() => {
+ const handler = (e) => {
+ const detail = e?.detail || {};
+ const msg = (detail.message || "").toString().trim() || t('auth.pleaseSignIn');
+ setAuthNotice(msg);
+ setMode(detail.mode === "signup" ? "signup" : "login");
+ setShowAuth(true);
+ };
+ window.addEventListener("sociowire:auth-required", handler);
+ return () => window.removeEventListener("sociowire:auth-required", handler);
+ }, [t]);
+
+ // Username prompt
+ useEffect(() => {
+ const requiresUsername = (name) => {
+ if (!name) return false;
+ if (name.includes("@")) return true;
+ return !/^[A-Za-z0-9_.]{3,24}$/.test(name);
+ };
+ if (usernamePromptTimer.current) clearTimeout(usernamePromptTimer.current);
+ if (loading) return;
+ if (!authenticated) { setShowUsernameModal(false); return; }
+ const shouldPrompt = !!needsUsername || requiresUsername(username);
+ if (shouldPrompt && !showUsernameModal) {
+ const base = (username || "").split("@")[0] || "";
+ const cleaned = base.replace(/[^A-Za-z0-9_.]/g, "").slice(0, 24);
+ setDesiredUsername(cleaned);
+ usernamePromptTimer.current = setTimeout(() => setShowUsernameModal(true), 600);
+ } else if (!shouldPrompt && showUsernameModal) {
+ setShowUsernameModal(false);
+ }
+ }, [authenticated, username, showUsernameModal, loading, needsUsername]);
+
+ // Handlers
+ const handleLoginSubmit = async (e) => {
+ e.preventDefault();
+ trackEvent("login_submit");
+ const res = await login(loginUser, loginPass);
+ if (res?.ok) trackEvent("login_success");
+ };
+
+ const handleSignupSubmit = async (e) => {
+ e.preventDefault();
+ setSignupError("");
+ setSignupInfo("");
+ if (!firstName.trim() || !lastName.trim() || !regUser.trim() || !regEmail.trim() || !regPass) {
+ setSignupError(t('auth.fieldsRequired'));
+ return;
+ }
+ try {
+ setSignupLoading(true);
+ trackEvent("signup_submit");
+ const u = regUser.trim();
+ const em = regEmail.trim();
+ await registerUser({ username: u, email: em, password: regPass, firstName: firstName.trim(), lastName: lastName.trim() });
+ try { localStorage.setItem(LAST_SIGNUP_KEY, JSON.stringify({ username: u, email: em })); } catch {}
+ setSignupInfo("✅ " + t('auth.accountCreatedMsg') + " (" + em + ")");
+ trackEvent("signup_success");
+ setMode("login");
+ setLoginUser(u || em);
+ setLoginPass("");
+ } catch (err) {
+ setSignupError(err.message || t('auth.registrationError'));
+ } finally {
+ setSignupLoading(false);
+ }
+ };
+
+ const handleUsernameSave = async (e) => {
+ e.preventDefault();
+ const next = (desiredUsername || "").trim();
+ if (!/^[A-Za-z0-9_.]{3,24}$/.test(next) || next.includes("@")) {
+ setUsernameError(t('auth.usernameHint'));
+ return;
+ }
+ setUsernameSaving(true);
+ setUsernameError("");
+ const res = await updateProfileUsername(next);
+ setUsernameSaving(false);
+ if (!res.ok) {
+ setUsernameError(res.status === 409 ? t('auth.usernameTaken') : (res.text || t('auth.usernameUpdateError')));
+ return;
+ }
+ setUsernameInfo(t('auth.usernameSaved'));
+ setShowUsernameModal(false);
+ await restore();
+ };
+
+ // Toast
+ const toast = useMemo(() => {
+ if (lastError) {
+ const toastType = needsEmailVerification ? "warn" : "error";
+ const title = needsEmailVerification ? t('auth.emailVerificationRequired') : t('auth.loginFailed');
+ return { type: toastType, title, message: lastError };
+ }
+ if (lastInfo) return { type: "success", title: "", message: lastInfo };
+ return null;
+ }, [lastError, lastInfo, needsEmailVerification, t]);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Render
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ return (
+ <>
+ {/* Toast */}
+ {toast?.message && (
+
+
{
+ if (lastError) clearError?.();
+ if (lastInfo) clearInfo?.();
+ }}
+ />
+
+ )}
+
+ {/* Header */}
+
+ {/* Left: Logo & Brand */}
+
+
+
+
+
+
+
+ SOCIOWIRE
+
+
+ {t('topbar.wiredToLife')}
+
+
+
+ {/* Theme dots - desktop only */}
+
+ {THEMES.map((themeKey) => (
+ onChangeTheme?.(themeKey)}
+ whileHover={{ scale: 1.15 }}
+ whileTap={{ scale: 0.9 }}
+ >
+ {t(`theme.${themeKey}`)[0]}
+
+ ))}
+
+
+
+ {/* Right: Actions */}
+
+
+ {/* User chip */}
+
authenticated && onUserIsland?.(username)}
+ />
+
+ {/* PWA Install */}
+ {showInstallBtn && (
+ promptInstall()}
+ title={t('topbar.installApp')}
+ />
+ )}
+
+ {/* Theme - mobile */}
+
+
+ {/* Language */}
+
+
+ {/* Friends */}
+ {authenticated && (
+ setShowFriends(!showFriends)}
+ title={t('topbar.friends')}
+ />
+ )}
+
+ {/* Auth */}
+ {
+ if (authenticated) { trackEvent("logout_click"); logout(); }
+ else openModal("login");
+ }}
+ disabled={loading}
+ title={authenticated ? t('topbar.logout') : t('topbar.login')}
+ />
+
+
+
+ {/* Dropdowns */}
+ setShowLangMenu(false)}>
+ {SUPPORTED_LANGS.map((lang) => (
+ { i18n.changeLanguage(lang); setShowLangMenu(false); }}
+ >
+ {t(`language.${lang}`)}
+
+ ))}
+
+
+ setShowThemeMenu(false)}>
+ {THEMES.map((themeKey) => (
+ { onChangeTheme?.(themeKey); setShowThemeMenu(false); }}
+ >
+ {t(`theme.${themeKey}`)}
+
+ ))}
+
+
+ {/* Friends Panel */}
+
+ {showFriends && (
+
+ { setShowFriends(false); onUserIsland?.(uname); }}
+ onClose={() => setShowFriends(false)}
+ />
+
+ )}
+
+
+ {/* Auth Modal */}
+
+ {showAuth && (
+
+ e.stopPropagation()}
+ >
+ {/* Header tabs */}
+
+
+
+
+
+
+ {/* Notices */}
+ {signupInfo && {signupInfo}
}
+ {authNotice && {authNotice}
}
+ {mode === "login" && lastError && (
+
+ {lastError}
+
+ )}
+
+ {/* Forms */}
+
+ {mode === "login" ? (
+
+ ) : (
+
+ )}
+
+
+
+ )}
+
+
+ {/* Username Modal */}
+
+ {showUsernameModal && (
+
+
+ {t('auth.chooseUsername')}
+ {t('auth.pickUsername')}
+ {usernameError && {usernameError}
}
+ {usernameInfo && {usernameInfo}
}
+
+
+
+ )}
+
+ >
+ );
+}
diff --git a/src/styles/mapMarkers.css b/src/styles/mapMarkers.css
index 8eb8496..0e96243 100644
--- a/src/styles/mapMarkers.css
+++ b/src/styles/mapMarkers.css
@@ -1460,3 +1460,304 @@ body[data-theme="light"] .sw-modal-x{
border-right: 6px solid transparent;
border-top: 8px solid #3b82f6;
}
+
+/* ═══════════════════════════════════════════════════════════════════════════
+ ENHANCED MARKER ANIMATIONS (v2 - Framer Motion style)
+ ═══════════════════════════════════════════════════════════════════════════ */
+
+/* Smooth entry animations */
+@keyframes markerBounceIn {
+ 0% {
+ opacity: 0;
+ transform: scale(0) translateY(20px);
+ }
+ 50% {
+ transform: scale(1.15) translateY(-5px);
+ }
+ 70% {
+ transform: scale(0.95) translateY(2px);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+@keyframes markerFadeSlide {
+ 0% {
+ opacity: 0;
+ transform: translateY(16px) scale(0.9);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+@keyframes markerPulseRing {
+ 0% {
+ transform: scale(0.8);
+ opacity: 0.8;
+ }
+ 50% {
+ transform: scale(1.4);
+ opacity: 0;
+ }
+ 100% {
+ transform: scale(0.8);
+ opacity: 0;
+ }
+}
+
+@keyframes glowPulse {
+ 0%, 100% {
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 rgba(96, 165, 250, 0);
+ }
+ 50% {
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4), 0 0 20px rgba(96, 165, 250, 0.4);
+ }
+}
+
+/* Simple marker enhancements */
+.sw-simple-marker {
+ transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1),
+ box-shadow 0.25s ease,
+ border-color 0.2s ease;
+ will-change: transform;
+}
+
+.post-pin--simple:hover .sw-simple-marker {
+ transform: scale(1.2) translateY(-2px);
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4), 0 0 15px rgba(96, 165, 250, 0.35);
+ border-color: rgba(96, 165, 250, 0.8);
+}
+
+/* Live marker pulsing glow */
+.sw-live-marker {
+ animation: glowPulse 2s ease-in-out infinite;
+}
+
+.sw-live-marker::before {
+ content: "";
+ position: absolute;
+ inset: -4px;
+ border-radius: 50%;
+ background: rgba(239, 68, 68, 0.3);
+ animation: markerPulseRing 2s ease-out infinite;
+ pointer-events: none;
+}
+
+/* Hover card smooth animation */
+.sw-simple-hover-card {
+ transform: translateX(-50%) translateY(-8px) scale(0.95);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1),
+ transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+.post-pin--simple:hover .sw-simple-hover-card {
+ opacity: 1;
+ transform: translateX(-50%) translateY(-12px) scale(1);
+ pointer-events: auto;
+}
+
+/* Entry animation for markers */
+.sw-appear {
+ opacity: 0;
+ animation: markerFadeSlide 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
+}
+
+.sw-appear-in {
+ opacity: 1;
+}
+
+/* Stagger effect for multiple markers */
+.sw-appear:nth-child(2) { animation-delay: 0.05s; }
+.sw-appear:nth-child(3) { animation-delay: 0.1s; }
+.sw-appear:nth-child(4) { animation-delay: 0.15s; }
+.sw-appear:nth-child(5) { animation-delay: 0.2s; }
+
+/* Cluster marker styles */
+.sw-cluster-marker {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+.sw-cluster-marker:hover {
+ transform: scale(1.1);
+}
+
+.sw-cluster-bubble {
+ min-width: 44px;
+ height: 44px;
+ padding: 0 12px;
+ border-radius: 22px;
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.95), rgba(139, 92, 246, 0.9));
+ border: 3px solid rgba(255, 255, 255, 0.95);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35), 0 0 20px rgba(96, 165, 250, 0.25);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ transition: all 0.25s ease;
+}
+
+.sw-cluster-marker:hover .sw-cluster-bubble {
+ background: linear-gradient(135deg, rgba(96, 165, 250, 0.98), rgba(167, 139, 250, 0.95));
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4), 0 0 30px rgba(96, 165, 250, 0.4);
+}
+
+.sw-cluster-count {
+ font-size: 16px;
+ font-weight: 800;
+ color: #fff;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+.sw-cluster-icon {
+ font-size: 14px;
+ color: rgba(255, 255, 255, 0.9);
+}
+
+.sw-cluster-pointer {
+ position: absolute;
+ bottom: -10px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 0;
+ height: 0;
+ border-left: 10px solid transparent;
+ border-right: 10px solid transparent;
+ border-top: 14px solid rgba(59, 130, 246, 0.95);
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
+}
+
+/* Category badges on markers */
+.sw-marker-badge {
+ position: absolute;
+ top: -6px;
+ right: -6px;
+ min-width: 18px;
+ height: 18px;
+ padding: 0 5px;
+ border-radius: 9px;
+ background: linear-gradient(135deg, #ef4444, #dc2626);
+ border: 2px solid #fff;
+ font-size: 10px;
+ font-weight: 700;
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
+}
+
+/* Modal overlay improvements */
+.sw-centered-modal {
+ animation: modalFadeIn 0.35s cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+@keyframes modalFadeIn {
+ 0% {
+ opacity: 0;
+ transform: scale(0.95) translateY(10px);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+/* Modal backdrop blur */
+.sw-modal-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(5, 8, 22, 0.8);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ animation: backdropFadeIn 0.3s ease;
+}
+
+@keyframes backdropFadeIn {
+ 0% { opacity: 0; }
+ 100% { opacity: 1; }
+}
+
+/* Improved hero section with gradient overlay */
+.sw-modal-hero {
+ position: relative;
+ overflow: hidden;
+ border-radius: 12px;
+}
+
+.sw-modal-hero::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(
+ to bottom,
+ transparent 0%,
+ transparent 50%,
+ rgba(15, 23, 42, 0.8) 100%
+ );
+ pointer-events: none;
+}
+
+.sw-modal-hero img {
+ transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.sw-modal-hero:hover img {
+ transform: scale(1.05);
+}
+
+/* Stats bar with micro-interactions */
+.sw-stat-pill {
+ transition: all 0.2s ease;
+ cursor: pointer;
+}
+
+.sw-stat-pill:hover {
+ transform: translateY(-2px);
+ background: rgba(96, 165, 250, 0.25) !important;
+ border-color: rgba(96, 165, 250, 0.5) !important;
+}
+
+.sw-stat-pill:active {
+ transform: scale(0.95);
+}
+
+/* Truth score rail animation */
+.sw-truth-rail-marker {
+ transition: top 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+/* CTA button hover effect */
+.sw-cta-primary {
+ position: relative;
+ overflow: hidden;
+ transition: all 0.25s ease;
+}
+
+.sw-cta-primary::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
+ transform: translateX(-100%);
+ transition: transform 0.5s ease;
+}
+
+.sw-cta-primary:hover::before {
+ transform: translateX(100%);
+}
+
+.sw-cta-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 16px rgba(96, 165, 250, 0.3);
+}
diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css
index 68c268b..41322ea 100644
--- a/src/styles/tailwind.css
+++ b/src/styles/tailwind.css
@@ -282,3 +282,305 @@ html {
-webkit-box-orient: vertical;
overflow: hidden;
}
+
+/* ─────────────────────────────────────────────────────────────────────────────
+ Enhanced Theme System - Dark/Blue/Light modes
+ ───────────────────────────────────────────────────────────────────────────── */
+
+/* Theme variables - Blue (default) */
+:root,
+body[data-theme="blue"] {
+ --theme-bg-primary: #050816;
+ --theme-bg-secondary: #0a1628;
+ --theme-bg-tertiary: #0f1d32;
+ --theme-bg-elevated: #142238;
+ --theme-text-primary: #f1f5f9;
+ --theme-text-secondary: #94a3b8;
+ --theme-text-muted: #64748b;
+ --theme-border: rgba(59, 130, 246, 0.2);
+ --theme-border-hover: rgba(96, 165, 250, 0.4);
+ --theme-accent: #60a5fa;
+ --theme-accent-soft: rgba(96, 165, 250, 0.15);
+ --theme-gradient-start: #0a1628;
+ --theme-gradient-end: #050816;
+ --theme-glow: rgba(96, 165, 250, 0.3);
+}
+
+/* Theme variables - Dark */
+body[data-theme="dark"] {
+ --theme-bg-primary: #09090b;
+ --theme-bg-secondary: #18181b;
+ --theme-bg-tertiary: #27272a;
+ --theme-bg-elevated: #3f3f46;
+ --theme-text-primary: #fafafa;
+ --theme-text-secondary: #a1a1aa;
+ --theme-text-muted: #71717a;
+ --theme-border: rgba(255, 255, 255, 0.1);
+ --theme-border-hover: rgba(255, 255, 255, 0.2);
+ --theme-accent: #a78bfa;
+ --theme-accent-soft: rgba(167, 139, 250, 0.15);
+ --theme-gradient-start: #18181b;
+ --theme-gradient-end: #09090b;
+ --theme-glow: rgba(167, 139, 250, 0.3);
+}
+
+/* Theme variables - Light */
+body[data-theme="light"] {
+ --theme-bg-primary: #ffffff;
+ --theme-bg-secondary: #f8fafc;
+ --theme-bg-tertiary: #f1f5f9;
+ --theme-bg-elevated: #ffffff;
+ --theme-text-primary: #0f172a;
+ --theme-text-secondary: #475569;
+ --theme-text-muted: #94a3b8;
+ --theme-border: rgba(15, 23, 42, 0.1);
+ --theme-border-hover: rgba(59, 130, 246, 0.3);
+ --theme-accent: #3b82f6;
+ --theme-accent-soft: rgba(59, 130, 246, 0.1);
+ --theme-gradient-start: #ffffff;
+ --theme-gradient-end: #f1f5f9;
+ --theme-glow: rgba(59, 130, 246, 0.2);
+}
+
+/* ─────────────────────────────────────────────────────────────────────────────
+ Theme-aware components
+ ───────────────────────────────────────────────────────────────────────────── */
+
+/* Themed container */
+.themed-container {
+ background: linear-gradient(180deg, var(--theme-gradient-start), var(--theme-gradient-end));
+ color: var(--theme-text-primary);
+}
+
+/* Themed card */
+.themed-card {
+ background: var(--theme-bg-elevated);
+ border: 1px solid var(--theme-border);
+ border-radius: 1rem;
+ transition: all 0.3s ease;
+}
+
+.themed-card:hover {
+ border-color: var(--theme-border-hover);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2), 0 0 16px var(--theme-glow);
+}
+
+/* Themed text */
+.text-themed-primary { color: var(--theme-text-primary); }
+.text-themed-secondary { color: var(--theme-text-secondary); }
+.text-themed-muted { color: var(--theme-text-muted); }
+.text-themed-accent { color: var(--theme-accent); }
+
+/* Themed backgrounds */
+.bg-themed-primary { background: var(--theme-bg-primary); }
+.bg-themed-secondary { background: var(--theme-bg-secondary); }
+.bg-themed-elevated { background: var(--theme-bg-elevated); }
+.bg-themed-accent { background: var(--theme-accent-soft); }
+
+/* Themed borders */
+.border-themed { border-color: var(--theme-border); }
+.border-themed-hover:hover { border-color: var(--theme-border-hover); }
+
+/* ─────────────────────────────────────────────────────────────────────────────
+ Improved Contrast Utilities
+ ───────────────────────────────────────────────────────────────────────────── */
+
+/* High contrast text for accessibility */
+.text-high-contrast {
+ color: #ffffff;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
+}
+
+body[data-theme="light"] .text-high-contrast {
+ color: #0f172a;
+ text-shadow: none;
+}
+
+/* Readable text on images */
+.text-on-image {
+ color: #ffffff;
+ text-shadow:
+ 0 1px 2px rgba(0, 0, 0, 0.8),
+ 0 2px 8px rgba(0, 0, 0, 0.4);
+}
+
+/* Focus ring for accessibility */
+.focus-ring:focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 3px var(--theme-accent-soft), 0 0 0 5px var(--theme-accent);
+}
+
+/* ─────────────────────────────────────────────────────────────────────────────
+ Interactive Elements
+ ───────────────────────────────────────────────────────────────────────────── */
+
+/* Hover lift effect */
+.hover-lift {
+ transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1),
+ box-shadow 0.25s ease;
+}
+
+.hover-lift:hover {
+ transform: translateY(-4px);
+}
+
+/* Press scale effect */
+.press-scale {
+ transition: transform 0.15s ease;
+}
+
+.press-scale:active {
+ transform: scale(0.97);
+}
+
+/* Glow on hover */
+.hover-glow {
+ transition: box-shadow 0.3s ease;
+}
+
+.hover-glow:hover {
+ box-shadow: 0 0 20px var(--theme-glow);
+}
+
+/* ─────────────────────────────────────────────────────────────────────────────
+ Status & State Indicators
+ ───────────────────────────────────────────────────────────────────────────── */
+
+/* Online indicator */
+.status-online {
+ position: relative;
+}
+
+.status-online::after {
+ content: "";
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: #22c55e;
+ border: 2px solid var(--theme-bg-primary);
+ box-shadow: 0 0 8px rgba(34, 197, 94, 0.5);
+}
+
+/* New content indicator */
+.has-new::before {
+ content: "";
+ position: absolute;
+ top: -2px;
+ right: -2px;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: #ef4444;
+ animation: livePulse 1.5s ease-in-out infinite;
+}
+
+/* Loading shimmer overlay */
+.shimmer-overlay {
+ position: relative;
+ overflow: hidden;
+}
+
+.shimmer-overlay::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(
+ 90deg,
+ transparent 0%,
+ rgba(255, 255, 255, 0.05) 50%,
+ transparent 100%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 2s linear infinite;
+}
+
+/* ─────────────────────────────────────────────────────────────────────────────
+ Spacing & Layout Helpers
+ ───────────────────────────────────────────────────────────────────────────── */
+
+/* Safe area padding for mobile */
+.safe-area-top { padding-top: env(safe-area-inset-top, 0); }
+.safe-area-bottom { padding-bottom: env(safe-area-inset-bottom, 0); }
+.safe-area-left { padding-left: env(safe-area-inset-left, 0); }
+.safe-area-right { padding-right: env(safe-area-inset-right, 0); }
+
+/* Content container with max-width */
+.content-container {
+ width: 100%;
+ max-width: 1400px;
+ margin-left: auto;
+ margin-right: auto;
+ padding-left: 1rem;
+ padding-right: 1rem;
+}
+
+@media (min-width: 640px) {
+ .content-container {
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+ }
+}
+
+/* ─────────────────────────────────────────────────────────────────────────────
+ Motion & Transitions
+ ───────────────────────────────────────────────────────────────────────────── */
+
+/* Reduced motion preference */
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* Smooth entrance animations */
+@keyframes enterFromBottom {
+ 0% {
+ opacity: 0;
+ transform: translateY(16px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes enterFromLeft {
+ 0% {
+ opacity: 0;
+ transform: translateX(-16px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+@keyframes enterFromRight {
+ 0% {
+ opacity: 0;
+ transform: translateX(16px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.animate-enter-bottom {
+ animation: enterFromBottom 0.4s cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+.animate-enter-left {
+ animation: enterFromLeft 0.4s cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+.animate-enter-right {
+ animation: enterFromRight 0.4s cubic-bezier(0.16, 1, 0.3, 1);
+}