diff --git a/backups/AuthContext.jsx.20251219-150345.bak b/backups/AuthContext.jsx.20251219-150345.bak
deleted file mode 100644
index 883d3a6..0000000
--- a/backups/AuthContext.jsx.20251219-150345.bak
+++ /dev/null
@@ -1,244 +0,0 @@
-import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
-import { clearAuth, guessUsernameFromToken, jwtExpSeconds, loadAuth, saveAuth } from "./authStorage";
-
-const AuthCtx = createContext(null);
-
-function apiBase() {
- // même domaine par défaut
- return (import.meta?.env?.VITE_API_BASE_URL || "").replace(/\/+$/, "");
-}
-
-async function apiFetch(path, opts = {}) {
- const url = apiBase() + path;
- const res = await fetch(url, {
- ...opts,
- headers: {
- "Content-Type": "application/json",
- ...(opts.headers || {}),
- },
- });
- return res;
-}
-
-async function whoami(accessToken) {
- const res = await apiFetch("/api/whoami", {
- method: "GET",
- headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
- });
- const j = await res.json().catch(() => null);
- return { ok: res.ok, status: res.status, json: j };
-}
-
-async function refreshTokens(refreshToken) {
- const res = await apiFetch("/api/refresh", {
- method: "POST",
- body: JSON.stringify({ refresh_token: refreshToken }),
- });
- const j = await res.json().catch(() => null);
- return { ok: res.ok, status: res.status, json: j };
-}
-
-function nowSec() {
- return Math.floor(Date.now() / 1000);
-}
-
-export function AuthProvider({ children }) {
- const [status, setStatus] = useState("loading"); // loading | anon | auth
- const [user, setUser] = useState(null); // { username }
- const [tokens, setTokens] = useState(null); // { access_token, refresh_token, ... }
- const [error, setError] = useState("");
- const refreshTimer = useRef(null);
-
- function setAnon(msg = "") {
- setStatus("anon");
- setUser(null);
- setTokens(null);
- setError(msg || "");
- clearAuth();
- }
-
- function setAuthFromTokens(t, usernameGuess = "") {
- setTokens(t);
- saveAuth({ ...t, obtained_at: Date.now() });
- const u = usernameGuess || guessUsernameFromToken(t?.access_token) || "";
- setUser(u ? { username: u } : null);
- setStatus("auth");
- setError("");
- }
-
- async function ensureFreshSession(existing) {
- if (!existing?.access_token) return { ok: false, reason: "no_access_token" };
-
- const exp = jwtExpSeconds(existing.access_token);
- const skew = 60; // seconds
- const isExpiredOrSoon = !exp || exp <= (nowSec() + skew);
-
- // 1) refresh si nécessaire
- if (isExpiredOrSoon) {
- if (!existing.refresh_token) return { ok: false, reason: "expired_no_refresh" };
-
- const rr = await refreshTokens(existing.refresh_token);
- if (!rr.ok || !rr.json?.access_token) {
- return { ok: false, reason: `refresh_failed_${rr.status}` };
- }
-
- const merged = {
- ...existing,
- ...rr.json,
- };
-
- // certains serveurs renvoient pas refresh_token à chaque fois
- if (!merged.refresh_token) merged.refresh_token = existing.refresh_token;
-
- setAuthFromTokens(merged);
- existing = merged;
- } else {
- // access token encore bon
- setAuthFromTokens(existing);
- }
-
- // 2) confirmer avec backend (whoami)
- const w = await whoami(existing.access_token);
- if (w.ok && w.json?.authenticated) {
- const uname = w.json.username || guessUsernameFromToken(existing.access_token) || "";
- setUser(uname ? { username: uname } : null);
- setStatus("auth");
- setError("");
- return { ok: true };
- }
-
- // Si whoami fail, on considère désync (token invalide pour backend)
- return { ok: false, reason: `whoami_${w.status}` };
- }
-
- function scheduleAutoRefresh(t) {
- if (refreshTimer.current) {
- clearInterval(refreshTimer.current);
- refreshTimer.current = null;
- }
- // check aux 30s, refresh quand exp < 90s
- refreshTimer.current = setInterval(async () => {
- const cur = loadAuth();
- if (!cur?.access_token) return;
-
- const exp = jwtExpSeconds(cur.access_token);
- if (!exp) return;
-
- if (exp <= nowSec() + 90) {
- if (!cur.refresh_token) {
- setAnon("Session expirée (pas de refresh_token).");
- return;
- }
- const rr = await refreshTokens(cur.refresh_token);
- if (!rr.ok || !rr.json?.access_token) {
- setAnon("Session expirée. Please login again.");
- return;
- }
- const merged = { ...cur, ...rr.json };
- if (!merged.refresh_token) merged.refresh_token = cur.refresh_token;
- setAuthFromTokens(merged);
- }
- }, 30000);
- }
-
- async function restore() {
- setStatus("loading");
- setError("");
- const saved = loadAuth();
- if (!saved?.access_token) {
- setAnon("");
- return;
- }
-
- const r = await ensureFreshSession(saved);
- if (!r.ok) {
- setAnon("Login desynced. Please login again.");
- return;
- }
-
- scheduleAutoRefresh(saved);
- }
-
- async function login(username, password) {
- setError("");
- setStatus("loading");
-
- const res = await apiFetch("/api/login", {
- method: "POST",
- body: JSON.stringify({ username, password }),
- });
-
- const j = await res.json().catch(() => null);
- if (!res.ok || !j?.access_token) {
- setAnon(j?.error_description || j?.error || "Login failed");
- return { ok: false, status: res.status, json: j };
- }
-
- const t = {
- ...j,
- obtained_at: Date.now(),
- };
-
- setAuthFromTokens(t);
- scheduleAutoRefresh(t);
-
- // confirm whoami (si ça fail -> desync)
- const w = await whoami(t.access_token);
- if (!w.ok || !w.json?.authenticated) {
- setAnon("Login desynced. Please login again.");
- return { ok: false, status: w.status, json: w.json };
- }
-
- const uname = w.json.username || guessUsernameFromToken(t.access_token) || "";
- setUser(uname ? { username: uname } : null);
- setStatus("auth");
- return { ok: true };
- }
-
- function logout() {
- if (refreshTimer.current) clearInterval(refreshTimer.current);
- refreshTimer.current = null;
- setAnon("");
- }
-
- useEffect(() => {
- restore();
-
- const onStorage = (e) => {
- if (e.key === "sociowire:auth") restore();
- };
- window.addEventListener("storage", onStorage);
- return () => window.removeEventListener("storage", onStorage);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- useEffect(() => {
- if (status === "auth" && tokens?.access_token) {
- scheduleAutoRefresh(tokens);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [status]);
-
- const value = useMemo(() => {
- return {
- status,
- user,
- tokens,
- error,
-
- isAuthed: status === "auth",
- accessToken: tokens?.access_token || "",
-
- login,
- logout,
- restore,
- setError,
- };
- }, [status, user, tokens, error]);
-
- return {children};
-}
-
-export function useAuth() {
- return useContext(AuthCtx);
-}
diff --git a/src/auth/AuthContext.jsx.bak.1766217948 b/src/auth/AuthContext.jsx.bak.1766217948
deleted file mode 100644
index 38e3ace..0000000
--- a/src/auth/AuthContext.jsx.bak.1766217948
+++ /dev/null
@@ -1,425 +0,0 @@
-import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
-import {
- clearAuth,
- guessUsernameFromToken,
- jwtExpSeconds,
- loadAuth,
- saveAuth,
- decodeJwtPayload,
-} from "./authStorage";
-import { parseAuthError } from "./authErrors";
-
-const AuthCtx = createContext(null);
-
-function apiBase() {
- return (import.meta?.env?.VITE_API_BASE_URL || "").replace(/\/+$/, "");
-}
-
-async function apiFetch(path, opts = {}) {
- const url = apiBase() + path;
- const res = await fetch(url, {
- credentials: "include",
- ...opts,
- headers: {
- ...(opts.headers || {}),
- },
- });
- return res;
-}
-
-function tokenEmailVerified(accessToken) {
- try {
- const p = decodeJwtPayload(accessToken);
- if (!p) return null;
- if (typeof p.email_verified === "boolean") return p.email_verified;
- if (typeof p.emailVerified === "boolean") return p.emailVerified;
- return null;
- } catch {
- return null;
- }
-}
-
-// tolerant whoami: accepts {authenticated:true} OR {username:""} OR {user:""} OR just ok+token
-async function whoami(accessToken) {
- const res = await apiFetch("/api/whoami", {
- method: "GET",
- headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
- });
-
- const text = await res.text().catch(() => "");
- let json = null;
- try {
- json = text ? JSON.parse(text) : null;
- } catch {
- json = null;
- }
-
- const uname =
- (json && (json.username || json.user || json.preferred_username)) ||
- guessUsernameFromToken(accessToken) ||
- "";
-
- const authed =
- !!res.ok &&
- (json?.authenticated === true ||
- json?.ok === true ||
- !!json?.username ||
- !!json?.user ||
- !!uname);
-
- return { ok: !!res.ok, status: res.status, json, text, authenticated: authed, username: uname };
-}
-
-// NEW: /api/me is the source of truth for email_verified
-async function me(accessToken) {
- const res = await apiFetch("/api/me", {
- method: "GET",
- headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
- });
-
- const text = await res.text().catch(() => "");
- let json = null;
- try {
- json = text ? JSON.parse(text) : null;
- } catch {
- json = null;
- }
-
- // Try multiple shapes
- const u = (json && (json.user || json.profile || json.me)) || json || null;
-
- const emailVerified =
- u && typeof u.email_verified === "boolean"
- ? u.email_verified
- : u && typeof u.emailVerified === "boolean"
- ? u.emailVerified
- : null;
-
- return {
- ok: !!res.ok,
- status: res.status,
- json,
- text,
- emailVerified,
- };
-}
-
-async function refreshTokens(refreshToken) {
- const res = await apiFetch("/api/refresh", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ refresh_token: refreshToken }),
- });
-
- const text = await res.text().catch(() => "");
- let json = null;
- try {
- json = text ? JSON.parse(text) : null;
- } catch {
- json = null;
- }
-
- return { ok: res.ok, status: res.status, json, text };
-}
-
-async function resendVerification(accessToken) {
- const res = await apiFetch("/api/resend-verification", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
- },
- body: JSON.stringify({}),
- });
-
- const text = await res.text().catch(() => "");
- let json = null;
- try {
- json = text ? JSON.parse(text) : null;
- } catch {
- json = null;
- }
-
- return { ok: res.ok, status: res.status, json, text };
-}
-
-function nowSec() {
- return Math.floor(Date.now() / 1000);
-}
-
-export function AuthProvider({ children }) {
- const [status, setStatus] = useState("loading"); // loading | anon | auth
- const [user, setUser] = useState(null); // { username }
- const [tokens, setTokens] = useState(null); // { access_token, refresh_token, ... }
-
- const [error, setError] = useState("");
- const [lastInfo, setLastInfo] = useState("");
- const [needsEmailVerification, setNeedsEmailVerification] = useState(false);
-
- const refreshTimer = useRef(null);
-
- function setAnon(msg = "") {
- setStatus("anon");
- setUser(null);
- setTokens(null);
- setError(msg || "");
- setLastInfo("");
- setNeedsEmailVerification(false);
- clearAuth();
- }
-
- function setAuthFromTokens(t, usernameGuess = "") {
- setTokens(t);
- saveAuth({ ...t, obtained_at: Date.now() });
-
- const u = usernameGuess || guessUsernameFromToken(t?.access_token) || "";
- setUser(u ? { username: u } : null);
-
- setStatus("auth");
- setError("");
- }
-
- async function refreshProfile(accessTokenOverride) {
- const at = accessTokenOverride || tokens?.access_token;
- if (!at) return { ok: False };
-
- // /api/me is the truth; token claim is fallback only
- const mr = await me(at);
- let verified = mr.emailVerified
-
- if (typeof verified !== "boolean") {
- const tv = tokenEmailVerified(at);
- if (typeof tv === "boolean") verified = tv;
- }
-
- if (typeof verified === "boolean") {
- setNeedsEmailVerification(!verified);
- return { ok: true, verified };
- }
-
- // If cannot determine, do not block UI aggressively
- setNeedsEmailVerification(false);
- return { ok: mr.ok, verified: null };
- }
-
- async function ensureFreshSession(existing) {
- if (!existing?.access_token) return { ok: false, reason: "no_access_token" };
-
- const exp = jwtExpSeconds(existing.access_token);
- const skew = 60;
- const isExpiredOrSoon = !exp || exp <= nowSec() + skew;
-
- if (isExpiredOrSoon) {
- if (!existing.refresh_token) return { ok: false, reason: "expired_no_refresh" };
-
- const rr = await refreshTokens(existing.refresh_token);
- if (!rr.ok || !rr.json?.access_token) {
- return { ok: false, reason: `refresh_failed_${rr.status}`, detail: rr.text };
- }
-
- const merged = { ...existing, ...rr.json };
- if (!merged.refresh_token) merged.refresh_token = existing.refresh_token;
-
- setAuthFromTokens(merged);
- existing = merged;
- } else {
- setAuthFromTokens(existing);
- }
-
- const w = await whoami(existing.access_token);
- if (w.ok && w.authenticated) {
- setUser(w.username ? { username: w.username } : null);
- setStatus("auth");
- setError("");
-
- // ✅ Update email verification state from backend
- await refreshProfile(existing.access_token);
-
- return { ok: true };
- }
-
- return { ok: false, reason: `whoami_${w.status}`, detail: w.text || "" };
- }
-
- function scheduleAutoRefresh() {
- if (refreshTimer.current) {
- clearInterval(refreshTimer.current);
- refreshTimer.current = null;
- }
-
- refreshTimer.current = setInterval(async () => {
- const cur = loadAuth();
- if (!cur?.access_token) return;
-
- const exp = jwtExpSeconds(cur.access_token);
- if (!exp) return;
-
- if (exp <= nowSec() + 90) {
- if (!cur.refresh_token) {
- setAnon("Session expirée (pas de refresh_token).");
- return;
- }
-
- const rr = await refreshTokens(cur.refresh_token);
- if (!rr.ok || !rr.json?.access_token) {
- setAnon("Session expirée. Please login again.");
- return;
- }
-
- const merged = { ...cur, ...rr.json };
- if (!merged.refresh_token) merged.refresh_token = cur.refresh_token;
-
- setAuthFromTokens(merged);
-
- // keep verification fresh too
- await refreshProfile(merged.access_token);
- } else {
- // lightweight: refresh verification occasionally without spamming
- // (every 30s interval already runs; but this call is only parsing token unless needed)
- const tv = tokenEmailVerified(cur.access_token);
- if (typeof tv === "boolean") setNeedsEmailVerification(!tv);
- }
- }, 30000);
- }
-
- async function restore() {
- setStatus("loading");
- setError("");
- setLastInfo("");
- const saved = loadAuth();
- if (!saved?.access_token) {
- setAnon("");
- return;
- }
-
- const r = await ensureFreshSession(saved);
- if (!r.ok) {
- setAnon("Login desynced. Please login again.");
- return;
- }
-
- scheduleAutoRefresh();
- }
-
- async function login(username, password) {
- setError("");
- setLastInfo("");
- setStatus("loading");
-
- const res = await apiFetch("/api/login", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ username, password }),
- });
-
- const text = await res.text().catch(() => "");
- let j = null;
- try {
- j = text ? JSON.parse(text) : null;
- } catch {
- j = null;
- }
-
- if (!res.ok || !j?.access_token) {
- const parsed = parseAuthError(text || "", res.status);
- setNeedsEmailVerification(parsed.code === "email_verification_required");
- setAnon(parsed.message || j?.error_description || j?.error || text || "Login failed");
- return { ok: false, status: res.status, json: j, text };
- }
-
- const t = { ...j, obtained_at: Date.now() };
- setAuthFromTokens(t);
- scheduleAutoRefresh();
-
- const w = await whoami(t.access_token);
- if (!w.ok || !w.authenticated) {
- setAnon("Login desynced. Please login again.");
- return { ok: false, status: w.status, json: w.json, text: w.text };
- }
-
- setUser(w.username ? { username: w.username } : null);
- setStatus("auth");
-
- // ✅ update verified state from backend
- await refreshProfile(t.access_token);
-
- return { ok: true };
- }
-
- function logout() {
- if (refreshTimer.current) clearInterval(refreshTimer.current);
- refreshTimer.current = null;
- setAnon("");
- }
-
- async function resendVerificationEmail() {
- const at = tokens?.access_token || "";
- if (!at) {
- setError("Not logged in.");
- return { ok: false };
- }
-
- setError("");
- setLastInfo("");
-
- const r = await resendVerification(at);
- if (!r.ok) {
- setError(r.json?.error || r.json?.message || r.text || `Resend failed (${r.status})`);
- return { ok: false, status: r.status };
- }
-
- setLastInfo("Verification email sent. Check inbox/spam, then refresh session (logout/login).");
- return { ok: true };
- }
-
- useEffect(() => {
- restore();
- const onStorage = (e) => {
- if (e.key === "sociowire:auth") restore();
- };
- window.addEventListener("storage", onStorage);
- return () => window.removeEventListener("storage", onStorage);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- // Backward-compatible fields expected by TopBar/MapView
- const value = useMemo(() => {
- const loading = status === "loading";
- const authenticated = status === "auth";
- const username = user?.username || "";
- const token = tokens?.access_token || "";
-
- return {
- // internal
- status,
- user,
- tokens,
- error,
-
- // expected by UI
- loading,
- authenticated,
- username,
- token,
-
- lastError: error || "",
- lastInfo: lastInfo || "",
- needsEmailVerification: !!needsEmailVerification,
- resendVerificationEmail,
-
- login,
- logout,
- restore,
- refreshProfile,
- setError,
- setLastInfo,
- setNeedsEmailVerification,
- };
- }, [status, user, tokens, error, lastInfo, needsEmailVerification]);
-
- return {children};
-}
-
-export function useAuth() {
- return useContext(AuthCtx);
-}
diff --git a/src/components/Layout/TopBar.jsx.bak.1766214623 b/src/components/Layout/TopBar.jsx.bak.1766214623
deleted file mode 100644
index 5a0e45b..0000000
--- a/src/components/Layout/TopBar.jsx.bak.1766214623
+++ /dev/null
@@ -1,287 +0,0 @@
-import React, { useEffect, useMemo, useState } from "react";
-import { useAuth } from "../../auth/AuthContext";
-import { registerUser } from "../../api/client";
-import AuthToast from "../Auth/AuthToast";
-
-const THEME_LABELS = { dark: "Dark", blue: "Blue", light: "Light" };
-
-function initialLetter(name = "") {
- const s = (name || "").trim();
- return s ? s[0].toUpperCase() : "G";
-}
-
-export default function TopBar({ theme = "dark", onChangeTheme }) {
- const {
- loading,
- authenticated,
- username,
- login,
- logout,
- needsEmailVerification,
- openKeycloak,
- lastError,
- lastErrorCode,
- lastInfo,
- clearError,
- clearInfo,
- } = useAuth();
-
- const [showAuth, setShowAuth] = useState(false);
- const [mode, setMode] = useState("login");
-
- 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 openModal = (nextMode) => {
- setMode(nextMode);
- setShowAuth(true);
- setSignupError("");
- setSignupInfo("");
- };
-
- const closeModal = () => {
- setShowAuth(false);
- setLoginPass("");
- setSignupError("");
- setSignupInfo("");
- };
-
- useEffect(() => {
- if (authenticated) setShowAuth(false);
- }, [authenticated]);
-
- const handleLoginSubmit = async (e) => {
- e.preventDefault();
- await login(loginUser, loginPass);
- };
-
- const handleSignupSubmit = async (e) => {
- e.preventDefault();
- setSignupError("");
- setSignupInfo("");
-
- if (!firstName.trim() || !lastName.trim() || !regUser.trim() || !regEmail.trim() || !regPass) {
- setSignupError("First name, last name, username, email and password are required.");
- return;
- }
-
- try {
- setSignupLoading(true);
-
- await registerUser({
- username: regUser.trim(),
- email: regEmail.trim(),
- password: regPass,
- firstName: firstName.trim(),
- lastName: lastName.trim(),
- });
-
- setSignupInfo("Account created. Check your inbox (spam) to verify your email, then login.");
- setMode("login");
- setLoginUser(regUser.trim());
- setLoginPass("");
- } catch (err) {
- console.error(err);
- setSignupError(err.message || "Registration error.");
- } finally {
- setSignupLoading(false);
- }
- };
-
- const toast = useMemo(() => {
- if (lastError) {
- const t = needsEmailVerification ? "warn" : "error";
- const title = needsEmailVerification ? "Vérification email requise" : "Connexion impossible";
- return { type: t, title, message: lastError };
- }
- if (lastInfo) {
- return { type: "success", title: "", message: lastInfo };
- }
- return null;
- }, [lastError, lastInfo, needsEmailVerification]);
-
- return (
- <>
- {/* Toast area (under topbar) */}
- {(toast?.message) && (
-
-
{
- if (lastError) clearError();
- if (lastInfo) clearInfo();
- }}
- />
- {needsEmailVerification && (
-
-
-
- )}
-
- )}
-
-
-
- {showAuth && (
-
-
e.stopPropagation()}>
-
-
-
-
-
-
- {mode === "login" ? (
-
- ) : (
-
- )}
-
-
- )}
- >
- );
-}
diff --git a/src/components/Layout/TopBar.jsx.bak.1766217948 b/src/components/Layout/TopBar.jsx.bak.1766217948
deleted file mode 100644
index 4fc365e..0000000
--- a/src/components/Layout/TopBar.jsx.bak.1766217948
+++ /dev/null
@@ -1,392 +0,0 @@
-import React, { useEffect, useMemo, useState } from "react";
-import { useAuth } from "../../auth/AuthContext";
-import { registerUser } from "../../api/client";
-import AuthToast from "../Auth/AuthToast";
-
-const THEME_LABELS = { dark: "Dark", blue: "Blue", light: "Light" };
-const LAST_EMAIL_KEY = "sociowire:lastEmail";
-
-function initialLetter(name = "") {
- const s = (name || "").trim();
- return s ? s[0].toUpperCase() : "G";
-}
-
-function looksLikeEmail(s = "") {
- return typeof s === "string" && /.+@.+\..+/.test(s.trim());
-}
-
-export default function TopBar({ theme = "dark", onChangeTheme }) {
- const {
- loading,
- authenticated,
- username,
- login,
- logout,
- needsEmailVerification,
- resendVerificationEmail,
- lastError,
- lastInfo,
- setError,
- setLastInfo,
- } = useAuth();
-
- const clearError = () => setError && setError("");
- const clearInfo = () => setLastInfo && setLastInfo("");
-
- const [showAuth, setShowAuth] = useState(false);
- const [mode, setMode] = useState("login");
-
- 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 openModal = (nextMode) => {
- setMode(nextMode);
- setShowAuth(true);
- setSignupError("");
- setSignupInfo("");
-
- // Prefill login with last known email (nice for email-verified return)
- if (nextMode === "login") {
- try {
- const last = localStorage.getItem(LAST_EMAIL_KEY) || "";
- if (!loginUser.trim() && last.trim()) setLoginUser(last.trim());
- } catch {}
- }
- };
-
- const closeModal = () => {
- setShowAuth(false);
- setLoginPass("");
- setSignupError("");
- setSignupInfo("");
- };
-
- useEffect(() => {
- if (authenticated) setShowAuth(false);
- }, [authenticated]);
-
- /* SW_URL_NOTICE:BEGIN */
- useEffect(() => {
- try {
- const qs = new URLSearchParams(window.location.search || "");
- const verified = qs.get("verified") || qs.get("email_verified");
- const signup = qs.get("signup");
- const notice = qs.get("notice");
- const email = (qs.get("email") || qs.get("user") || qs.get("username") || "").trim();
-
- if (email) {
- try { localStorage.setItem(LAST_EMAIL_KEY, email); } catch {}
- // If user uses email to login, prefill it
- if (!loginUser.trim()) setLoginUser(email);
- if (!regEmail.trim()) setRegEmail(email);
- }
-
- if (verified === "1" || verified === "true") {
- setSignupInfo(`✅ Email confirmé${email ? ` (${email})` : ""}. Tu peux te connecter.`);
- setMode("login");
- setShowAuth(true);
- } else if (signup === "1" || signup === "true") {
- setSignupInfo("✅ Compte créé. Vérifie tes emails (et le spam), puis connecte-toi.");
- setMode("login");
- setShowAuth(true);
- } else if (notice) {
- const msg = decodeURIComponent(notice);
- if (msg && msg.trim()) {
- setSignupInfo(msg.trim());
- setMode("login");
- setShowAuth(true);
- }
- }
-
- // clean URL (remove params without reload)
- if (verified || signup || notice || email) {
- const clean = window.location.pathname + (window.location.hash || "");
- window.history.replaceState({}, "", clean);
- }
- } catch (e) {
- // ignore
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
- /* SW_URL_NOTICE:END */
-
- const handleLoginSubmit = async (e) => {
- e.preventDefault();
- // store email-like login for next time
- try {
- if (looksLikeEmail(loginUser)) localStorage.setItem(LAST_EMAIL_KEY, loginUser.trim());
- } catch {}
- await login(loginUser, loginPass);
- };
-
- const handleSignupSubmit = async (e) => {
- e.preventDefault();
- setSignupError("");
- setSignupInfo("");
-
- if (!firstName.trim() || !lastName.trim() || !regUser.trim() || !regEmail.trim() || !regPass) {
- setSignupError("First name, last name, username, email and password are required.");
- return;
- }
-
- try {
- setSignupLoading(true);
-
- await registerUser({
- username: regUser.trim(),
- email: regEmail.trim(),
- password: regPass,
- firstName: firstName.trim(),
- lastName: lastName.trim(),
- });
-
- try { localStorage.setItem(LAST_EMAIL_KEY, regEmail.trim()); } catch {}
-
- setSignupInfo("✅ Account created. Check your inbox (spam) to verify your email, then login.");
- setMode("login");
-
- // Some people login with email, some with username => prefer email if present
- setLoginUser((regEmail.trim() || regUser.trim()));
- setLoginPass("");
- } catch (err) {
- console.error(err);
- setSignupError(err.message || "Registration error.");
- } finally {
- setSignupLoading(false);
- }
- };
-
- const toast = useMemo(() => {
- if (lastError) {
- const t = needsEmailVerification ? "warn" : "error";
- const title = needsEmailVerification ? "Vérification email requise" : "Connexion impossible";
- return { type: t, title, message: lastError };
- }
- if (lastInfo) return { type: "success", title: "", message: lastInfo };
- return null;
- }, [lastError, lastInfo, needsEmailVerification]);
-
- const authComboLabel = authenticated ? "Log off" : (loading ? "..." : "Log on");
- const authLine = authenticated ? "Online" : "Guest";
- const authHandle = authenticated ? `@${username || "user"}` : "anon";
-
- return (
- <>
- {/* Toast area (under topbar) */}
- {(toast?.message) && (
-
-
{
- if (lastError) clearError();
- if (lastInfo) clearInfo();
- }}
- />
-
- {needsEmailVerification && (
-
-
-
-
-
- )}
-
- )}
-
-
-
-
SW
-
-
SOCIOWIRE.com
-
Wired to life
-
-
-
-
-
- {["dark", "blue", "light"].map((t) => (
-
- ))}
-
-
- {/* ONE unified auth button (guest online / log on / log off) */}
-
-
- {!authenticated && (
-
- )}
-
-
-
- {showAuth && (
-
-
e.stopPropagation()}>
-
-
-
-
-
-
- {/* Nice notices (modal) */}
- {signupInfo ?
{signupInfo}
: null}
-
- {mode === "login" && needsEmailVerification ? (
-
- ⚠️ Ton compte doit confirmer l’email avant de se connecter. Vérifie ta boîte mail (et le spam),
- clique le lien, puis reviens ici.
-
- ) : null}
-
- {mode === "login" && lastError ? (
-
{lastError}
- ) : null}
-
- {mode === "login" && lastInfo ? (
-
{lastInfo}
- ) : null}
-
- {mode === "login" ? (
-
- ) : (
-
- )}
-
-
- )}
- >
- );
-}
diff --git a/src/components/Layout/TopBar.jsx.bak.20251219-163055 b/src/components/Layout/TopBar.jsx.bak.20251219-163055
deleted file mode 100644
index e562d76..0000000
--- a/src/components/Layout/TopBar.jsx.bak.20251219-163055
+++ /dev/null
@@ -1,275 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useAuth } from "../../auth/AuthContext";
-import { registerUser } from "../../api/client";
-
-const THEME_LABELS = { dark: "Dark", blue: "Blue", light: "Light" };
-
-export default function TopBar({ theme = "dark", onChangeTheme }) {
- const {
- loading,
- authenticated,
- username,
- login,
- logout,
- needsEmailVerification,
- resendVerificationEmail,
- lastError,
- lastInfo,
- } = useAuth();
-
- const [showAuth, setShowAuth] = useState(false);
- const [mode, setMode] = useState("login");
-
- 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);
-
- let authLabel = "AUTH: anon";
- if (loading) authLabel = "AUTH: loading…";
- else if (authenticated) authLabel = `AUTH: user=${username || "?"}`;
-
- const openModal = (nextMode) => {
- setMode(nextMode);
- setShowAuth(true);
- setSignupError("");
- setSignupInfo("");
- };
-
- const closeModal = () => {
- setShowAuth(false);
- setLoginPass("");
- setSignupError("");
- setSignupInfo("");
- };
-
- useEffect(() => {
- if (authenticated) setShowAuth(false);
- }, [authenticated]);
-
- const handleLoginSubmit = async (e) => {
- e.preventDefault();
- await login(loginUser, loginPass);
- };
-
- const handleSignupSubmit = async (e) => {
- e.preventDefault();
- setSignupError("");
- setSignupInfo("");
-
- if (!firstName.trim() || !lastName.trim() || !regUser.trim() || !regEmail.trim() || !regPass) {
- setSignupError("First name, last name, username, email and password are required.");
- return;
- }
-
- try {
- setSignupLoading(true);
-
- await registerUser({
- username: regUser.trim(),
- email: regEmail.trim(),
- password: regPass,
- firstName: firstName.trim(),
- lastName: lastName.trim(),
- });
-
- // IMPORTANT: si Verify Email est activé côté Keycloak, on ne tente PAS de login auto.
- setSignupInfo("Account created. Check your inbox (spam) to verify your email, then login.");
- setMode("login");
- setLoginUser(regUser.trim());
- setLoginPass("");
- } catch (err) {
- console.error(err);
- setSignupError(err.message || "Registration error.");
- } finally {
- setSignupLoading(false);
- }
- };
-
- return (
- <>
-
-
-
SW
-
-
SOCIOWIRE.com
-
Wired to life
-
-
- {authLabel}
- {lastError && (
-
- ({lastError})
-
- )}
-
-
- {authenticated && needsEmailVerification && (
-
- Verify email required.
- Posting is locked until verification.
-
-
- )}
-
- {!needsEmailVerification && lastInfo && (
-
- {lastInfo}
-
- )}
-
-
-
-
-
- {["dark", "blue", "light"].map((t) => (
-
- ))}
-
-
- {authenticated ? (
-
- {username}
-
-
- ) : (
-
-
-
-
- )}
-
-
-
- {showAuth && (
-
-
e.stopPropagation()}>
-
-
-
-
-
-
- {mode === "login" ? (
-
- ) : (
-
- )}
-
-
- )}
- >
- );
-}
diff --git a/src/components/Layout/TopBar.jsx.bak.20251219-164233 b/src/components/Layout/TopBar.jsx.bak.20251219-164233
deleted file mode 100644
index 3938917..0000000
--- a/src/components/Layout/TopBar.jsx.bak.20251219-164233
+++ /dev/null
@@ -1,390 +0,0 @@
-import React, { useState, useEffect, useMemo } from "react";
-import { useAuth } from "../../auth/AuthContext";
-import { registerUser } from "../../api/client";
-
-const THEME_LABELS = { dark: "Dark", blue: "Blue", light: "Light" };
-const LAST_SIGNUP_USER_KEY = "sociowire:lastSignupUser";
-
-function normalizeAuthMessage(raw) {
- const msg = String(raw || "").trim();
- if (!msg) return "";
-
- const lower = msg.toLowerCase();
-
- // Keycloak classic when email verification required
- if (
- lower.includes("not fully set up") ||
- lower.includes("verify") ||
- lower.includes("verification") ||
- lower.includes("required action") ||
- (lower.includes("email") && lower.includes("verified"))
- ) {
- return "Tu dois confirmer ton email avant de te connecter. Vérifie ta boîte mail (et le spam), puis réessaie.";
- }
-
- // Bad credentials
- if (
- lower.includes("invalid user credentials") ||
- lower.includes("bad credentials") ||
- (lower.includes("invalid_grant") && !lower.includes("not fully set up"))
- ) {
- return "Mauvais username ou mot de passe.";
- }
-
- // Fallback (shorten very long raw blobs)
- if (msg.length > 240) return msg.slice(0, 240) + "…";
- return msg;
-}
-
-export default function TopBar({ theme = "dark", onChangeTheme }) {
- const {
- loading,
- authenticated,
- username,
- login,
- logout,
- needsEmailVerification,
- resendVerificationEmail,
- lastError,
- lastInfo,
- } = useAuth();
-
- const [showAuth, setShowAuth] = useState(false);
- const [mode, setMode] = useState("login");
-
- 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 isVerifiedPage =
- typeof window !== "undefined" && window.location?.pathname === "/auth/verified";
-
- const prettyError = useMemo(() => normalizeAuthMessage(lastError), [lastError]);
-
- const openModal = (nextMode) => {
- setMode(nextMode);
- setShowAuth(true);
- setSignupError("");
- setSignupInfo("");
- };
-
- const closeModal = () => {
- setShowAuth(false);
- setLoginPass("");
- setSignupError("");
- setSignupInfo("");
- };
-
- // close modal automatically on auth
- useEffect(() => {
- if (authenticated) setShowAuth(false);
- }, [authenticated]);
-
- // ✅ If user lands on /auth/verified (after clicking email link),
- // open login modal + prefill username from last signup.
- useEffect(() => {
- if (!isVerifiedPage) return;
-
- setMode("login");
- setShowAuth(true);
-
- try {
- const lastU = localStorage.getItem(LAST_SIGNUP_USER_KEY) || "";
- if (lastU && !loginUser) setLoginUser(lastU);
- } catch {}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isVerifiedPage]);
-
- // ✅ After successful login, if still on /auth/verified, cleanly go back home
- useEffect(() => {
- if (!authenticated) return;
- if (typeof window === "undefined") return;
- const path = window.location?.pathname || "";
- if (path === "/auth/verified") {
- try {
- window.history.replaceState({}, "", "/");
- } catch {}
- }
- }, [authenticated]);
-
- const handleLoginSubmit = async (e) => {
- e.preventDefault();
- await login(loginUser, loginPass);
- };
-
- const handleSignupSubmit = async (e) => {
- e.preventDefault();
- setSignupError("");
- setSignupInfo("");
-
- if (!firstName.trim() || !lastName.trim() || !regUser.trim() || !regEmail.trim() || !regPass) {
- setSignupError("First name, last name, username, email and password are required.");
- return;
- }
-
- try {
- setSignupLoading(true);
-
- await registerUser({
- username: regUser.trim(),
- email: regEmail.trim(),
- password: regPass,
- firstName: firstName.trim(),
- lastName: lastName.trim(),
- });
-
- // Store last signup username so /auth/verified can prefill login.
- try {
- localStorage.setItem(LAST_SIGNUP_USER_KEY, regUser.trim());
- } catch {}
-
- // IMPORTANT: si Verify Email est activé côté Keycloak, on ne tente PAS de login auto.
- setSignupInfo("Compte créé ✅ Vérifie ton email (spam inclus), puis reviens te connecter.");
- setMode("login");
- setLoginUser(regUser.trim());
- setLoginPass("");
- } catch (err) {
- console.error(err);
- setSignupError(err.message || "Registration error.");
- } finally {
- setSignupLoading(false);
- }
- };
-
- const AuthPill = () => {
- if (loading) {
- return (
-
-
- Loading…
-
- );
- }
- if (authenticated) {
- return (
-
-
- Connecté
-
- );
- }
- return (
-
-
- Guest
-
- );
- };
-
- return (
- <>
-
-
- {showAuth && (
-
-
e.stopPropagation()}>
-
-
-
-
-
-
- {mode === "login" ? (
-
- ) : (
-
- )}
-
-
- )}
- >
- );
-}
diff --git a/src/components/Layout/TopBar.jsx.bak.fix.1766214994 b/src/components/Layout/TopBar.jsx.bak.fix.1766214994
deleted file mode 100644
index 3883b85..0000000
--- a/src/components/Layout/TopBar.jsx.bak.fix.1766214994
+++ /dev/null
@@ -1,332 +0,0 @@
-import React, { useEffect, useMemo, useState } from "react";
-import { useAuth } from "../../auth/AuthContext";
-import { registerUser } from "../../api/client";
-import AuthToast from "../Auth/AuthToast";
-
-const THEME_LABELS = { dark: "Dark", blue: "Blue", light: "Light" };
-
-function initialLetter(name = "") {
- const s = (name || "").trim();
- return s ? s[0].toUpperCase() : "G";
-}
-
-export default function TopBar({ theme = "dark", onChangeTheme }) {
- const {
- loading,
- authenticated,
- username,
- login,
- logout,
- needsEmailVerification,
- openKeycloak,
- lastError,
- lastErrorCode,
- lastInfo,
- clearError,
- clearInfo,
- } = useAuth();
-
- const [showAuth, setShowAuth] = useState(false);
- const [mode, setMode] = useState("login");
-
- 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 openModal = (nextMode) => {
- setMode(nextMode);
- setShowAuth(true);
- setSignupError("");
- setSignupInfo("");
- };
-
- const closeModal = () => {
- setShowAuth(false);
- setLoginPass("");
- setSignupError("");
- setSignupInfo("");
- };
-
- useEffect(() => {
- if (authenticated) setShowAuth(false);
- }, [authenticated]);
-
- /* SW_URL_NOTICE:BEGIN */
- useEffect(() => {
- try {
- const qs = new URLSearchParams(window.location.search || "");
- const verified = qs.get("verified") || qs.get("email_verified");
- const signup = qs.get("signup");
- const notice = qs.get("notice");
-
- if (verified === "1" || verified === "true") {
- setSignupInfo("✅ Email confirmé. Tu peux te connecter.");
- setMode("login");
- setShowAuth(true);
- } else if (signup === "1" || signup === "true") {
- setSignupInfo("✅ Compte créé. Vérifie tes emails (et le spam), puis connecte-toi.");
- setMode("login");
- setShowAuth(true);
- } else if (notice) {
- // allow custom notice (URL-encoded)
- const msg = decodeURIComponent(notice);
- if (msg && msg.trim()) {
- setSignupInfo(msg.trim());
- setMode("login");
- setShowAuth(true);
- }
- }
-
- // clean URL (remove params without reload)
- if (verified or signup or notice):
- const clean = window.location.pathname + (window.location.hash || "");
- window.history.replaceState({}, "", clean);
- } catch {
- // ignore
- }
- }, []);
- /* SW_URL_NOTICE:END */
-
-
- const handleLoginSubmit = async (e) => {
- e.preventDefault();
- await login(loginUser, loginPass);
- };
-
- const handleSignupSubmit = async (e) => {
- e.preventDefault();
- setSignupError("");
- setSignupInfo("");
-
- if (!firstName.trim() || !lastName.trim() || !regUser.trim() || !regEmail.trim() || !regPass) {
- setSignupError("First name, last name, username, email and password are required.");
- return;
- }
-
- try {
- setSignupLoading(true);
-
- await registerUser({
- username: regUser.trim(),
- email: regEmail.trim(),
- password: regPass,
- firstName: firstName.trim(),
- lastName: lastName.trim(),
- });
-
- setSignupInfo("Account created. Check your inbox (spam) to verify your email, then login.");
- setMode("login");
- setLoginUser(regUser.trim());
- setLoginPass("");
- } catch (err) {
- console.error(err);
- setSignupError(err.message || "Registration error.");
- } finally {
- setSignupLoading(false);
- }
- };
-
- const toast = useMemo(() => {
- if (lastError) {
- const t = needsEmailVerification ? "warn" : "error";
- const title = needsEmailVerification ? "Vérification email requise" : "Connexion impossible";
- return { type: t, title, message: lastError };
- }
- if (lastInfo) {
- return { type: "success", title: "", message: lastInfo };
- }
- return null;
- }, [lastError, lastInfo, needsEmailVerification]);
-
- return (
- <>
- {/* Toast area (under topbar) */}
- {(toast?.message) && (
-
-
{
- if (lastError) clearError();
- if (lastInfo) clearInfo();
- }}
- />
- {needsEmailVerification && (
-
-
-
- )}
-
- )}
-
-
-
- {showAuth && (
-
-
e.stopPropagation()}>
-
-
-
-
-
-
- {/* SW_MODAL_NOTICE:BEGIN */}
- {signupInfo ? (
-
- {signupInfo}
-
- ) : null}
- {/* SW_MODAL_NOTICE:END */}
-
- {mode === "login" ? (
-
- ) : (
-
- )}
-
-
- )}
- >
- );
-}
diff --git a/src/main.jsx.bak.1766292419 b/src/main.jsx.bak.1766292419
deleted file mode 100644
index 8295cfc..0000000
--- a/src/main.jsx.bak.1766292419
+++ /dev/null
@@ -1,27 +0,0 @@
-import "@fortawesome/fontawesome-free/css/all.min.css"; // local via npm (no CDN)
-
-import React from "react";
-import ReactDOM from "react-dom/client";
-import App from "./App.jsx";
-
-import "./styles/base.css";
-import "./styles/layout.css";
-import "./styles/topbar.css";
-import "./styles/map.css";
-import "./styles/overlays.css";
-import "./styles/posts.css";
-import "./styles/theme.css";
-import "./styles/auth-modal.css";
-import "./styles/auth-toast.css";
-import "./styles/filters.css";
-import "./styles/mapMarkers.css";
-
-import { AuthProvider } from "./auth/AuthContext.jsx";
-
-ReactDOM.createRoot(document.getElementById("root")).render(
-
-
-
-
-
-);
diff --git a/src/styles/auth-modal.css.bak.1766217948 b/src/styles/auth-modal.css.bak.1766217948
deleted file mode 100644
index 7b18c9a..0000000
--- a/src/styles/auth-modal.css.bak.1766217948
+++ /dev/null
@@ -1,139 +0,0 @@
-.auth-modal-backdrop {
- position: fixed;
- inset: 0;
- z-index: 60;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(2, 6, 23, 0.65);
- backdrop-filter: blur(8px);
-}
-
-.auth-modal {
- width: min(640px, 95vw);
- border-radius: 22px;
- padding: 1.1rem 1.25rem 1.35rem;
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.85);
- border: 1px solid rgba(148, 163, 184, 0.7);
-}
-
-body[data-theme="dark"] .auth-modal {
- background: radial-gradient(circle at top left, #0f172a, #020617 70%);
-}
-body[data-theme="blue"] .auth-modal {
- background: radial-gradient(circle at top left, #0ea5e9, #020617 65%);
- border-color: rgba(96, 165, 250, 0.9);
-}
-body[data-theme="light"] .auth-modal {
- background: radial-gradient(circle at top left, #ffffff, #e5e7eb 70%);
- color: #020617;
- border-color: rgba(148, 163, 184, 0.7);
-}
-
-.auth-modal-header { display:flex; align-items:center; gap:.5rem; margin-bottom:.9rem; }
-
-.auth-tab {
- padding: .45rem .95rem;
- border-radius: 999px;
- border: 1px solid transparent;
- background: transparent;
- font-size: .85rem;
- color: #e5e7eb;
- cursor: pointer;
-}
-.auth-tab-active {
- background: linear-gradient(135deg, #1d4ed8, #38bdf8);
- border-color: rgba(191, 219, 254, 0.9);
- color: #f9fafb;
-}
-body[data-theme="light"] .auth-tab { color:#0f172a; }
-body[data-theme="light"] .auth-tab-active { color:#f9fafb; }
-
-.auth-close {
- margin-left:auto;
- width:32px; height:32px;
- border-radius:999px;
- border:1px solid rgba(148, 163, 184, 0.6);
- background: rgba(15, 23, 42, 0.85);
- color:#e5e7eb;
- font-size:.8rem;
- cursor:pointer;
-}
-body[data-theme="light"] .auth-close { background:#e5e7eb; color:#020617; }
-
-.auth-form { display:flex; flex-direction:column; gap:.7rem; }
-.auth-row { display:flex; gap:.6rem; }
-.auth-row label { flex:1; }
-.auth-form label { display:flex; flex-direction:column; font-size:.8rem; gap:.25rem; }
-
-.auth-form input {
- border-radius: 999px;
- border: 1px solid rgba(51, 65, 85, 0.9);
- background: rgba(15, 23, 42, 0.96);
- color: #e5e7eb;
- padding: .55rem .85rem;
- font-size: .85rem;
-}
-body[data-theme="light"] .auth-form input {
- background:#f9fafb;
- color:#020617;
- border-color: rgba(148, 163, 184, 0.9);
-}
-
-.btn-full { width:100%; margin-top:.4rem; }
-
-.auth-error {
- margin-top:.25rem;
- font-size:.8rem;
- color:#fecaca;
- background: rgba(127, 29, 29, 0.55);
- border-radius:.5rem;
- padding:.4rem .6rem;
- border: 1px solid rgba(248, 113, 113, 0.7);
-}
-
-@media (max-width:480px){
- .auth-modal { padding:.95rem .9rem 1.1rem; }
- .auth-row { flex-direction:column; }
-}
-
-/* SW_AUTH_NOTICE:BEGIN */
-.auth-notice{
- margin: .2rem 0 .65rem;
- padding: .55rem .7rem;
- border-radius: 14px;
- font-size: .82rem;
- font-weight: 800;
- line-height: 1.2;
- border: 1px solid rgba(148,163,184,.35);
- background: rgba(2,6,23,.22);
- color: #e5e7eb;
-}
-
-.auth-notice-success{
- background: rgba(34,197,94,0.14);
- border-color: rgba(34,197,94,0.55);
- color: #d1fae5;
-}
-
-.auth-notice-error{
- background: rgba(248,113,113,0.14);
- border-color: rgba(248,113,113,0.55);
- color: #ffe4e6;
-}
-
-.auth-notice-warn{
- background: rgba(251,191,36,0.12);
- border-color: rgba(251,191,36,0.40);
- color: #fffbeb;
-}
-
-body[data-theme="light"] .auth-notice{
- background: rgba(255,255,255,0.92);
- border-color: rgba(148,163,184,0.75);
- color: #0b1220;
-}
-body[data-theme="light"] .auth-notice-success{ color:#0b1220; }
-body[data-theme="light"] .auth-notice-error{ color:#0b1220; }
-body[data-theme="light"] .auth-notice-warn{ color:#0b1220; }
-/* SW_AUTH_NOTICE:END */
diff --git a/src/styles/topbar.css.bak.1766214715 b/src/styles/topbar.css.bak.1766214715
deleted file mode 100644
index ddb2af1..0000000
--- a/src/styles/topbar.css.bak.1766214715
+++ /dev/null
@@ -1,251 +0,0 @@
-.topbar{
- position: sticky; top:0; z-index:100;
- width:100%;
- padding: .55rem .9rem;
- display:flex; align-items:center; justify-content:space-between; gap:.6rem;
- background: linear-gradient(135deg,#071225 0%,#0b2b5f 38%,#0e65c0 72%,#27a8ff 100%);
- border-bottom-left-radius:18px; border-bottom-right-radius:18px;
- box-shadow: 0 10px 24px rgba(0,0,0,.55), 0 0 18px rgba(56,189,248,.45);
- overflow:hidden;
-}
-
-.logo-circle{
- width:40px; height:40px; border-radius:50%; flex-shrink:0;
- background-image:url("/icons/logo-master.png");
- background-size:cover; background-position:center; background-repeat:no-repeat;
- box-shadow: 0 0 10px rgba(37,99,235,.8), 0 0 18px rgba(56,189,248,.6);
- color:transparent;
-}
-
-.title-block{ display:flex; flex-direction:column; line-height:1.05; min-width:0; }
-.main-title{ font-size:1.02rem; font-weight:800; letter-spacing:.04em; color:#f9fafb; }
-.sub-title{ font-size:.74rem; color:#dbeafe; opacity:.95; }
-
-.topbar-right{ display:flex; align-items:center; gap:.5rem; margin-left:auto; flex-wrap:wrap; }
-
-.theme-switch{ display:flex; gap:.28rem; }
-.theme-dot{
- width:22px; height:22px; border-radius:999px;
- border:1px solid rgba(148,163,184,.85);
- background:rgba(15,23,42,.9); color:#e5e7eb; font-size:.65rem;
- display:flex; align-items:center; justify-content:center; padding:0; cursor:pointer;
- box-shadow:0 0 6px rgba(15,23,42,.9); transition:transform 0.36s ease;
-}
-.theme-dot:hover{ transform:scale(1.06); }
-.theme-dot-active{ border-color:#facc15; box-shadow:0 0 10px rgba(250,204,21,.9),0 0 14px rgba(56,189,248,.55); }
-
-.btn-primary, .btn-secondary{
- padding:.32rem .85rem; font-size:.78rem; border-radius:999px; font-weight:700; cursor:pointer;
- white-space:nowrap; line-height:1;
-}
-.btn-primary{
- border:1px solid #22c55e;
- background:radial-gradient(circle at 30% 30%,#22c55e,#16a34a);
- color:#f9fafb;
- box-shadow:0 4px 10px rgba(22,163,74,.5),0 0 8px rgba(22,163,74,.6);
-}
-.btn-primary:disabled{ opacity:.6; cursor:default; }
-.btn-secondary{
- border:1px solid #bfdbfe;
- background:rgba(15,23,42,.25);
- color:#e0f2fe;
- box-shadow:0 3px 10px rgba(15,23,42,.6);
-}
-.btn-secondary:hover{ background:rgba(15,23,42,.45); }
-
-@media (max-width:640px){
- .topbar{ padding:.5rem .7rem; border-bottom-left-radius:16px; border-bottom-right-radius:16px; }
- .main-title{ font-size:1rem; }
- .sub-title{ font-size:.7rem; }
- .topbar-right{ width:100%; justify-content:flex-start; gap:.45rem; }
-}
-
-/* SW_AUTH_UI:BEGIN */
-.auth-strip{
- margin-top: 6px;
- display:flex;
- align-items:center;
- gap: .45rem;
- flex-wrap: wrap;
-}
-
-.auth-pill{
- display:inline-flex;
- align-items:center;
- gap: .35rem;
- padding: .18rem .55rem;
- border-radius: 999px;
- font-size: .70rem;
- font-weight: 900;
- letter-spacing: .02em;
- border: 1px solid rgba(148,163,184,.55);
- background: rgba(2,6,23,.25);
- color: #e5e7eb;
-}
-.auth-pill i{ font-size: .85rem; opacity:.95; }
-
-.auth-pill--ok{
- border-color: rgba(34,197,94,.65);
- background: rgba(34,197,94,.16);
-}
-.auth-pill--loading{
- border-color: rgba(56,189,248,.65);
- background: rgba(56,189,248,.14);
-}
-.auth-pill--anon{
- border-color: rgba(148,163,184,.55);
- background: rgba(2,6,23,.20);
-}
-
-.auth-banner{
- margin-top: 6px;
- padding: 6px 10px;
- border-radius: 14px;
- font-size: .72rem;
- font-weight: 800;
- display:flex;
- gap: 8px;
- align-items:center;
- flex-wrap:wrap;
-}
-.auth-banner--error{
- background: rgba(248,113,113,0.14);
- border: 1px solid rgba(248,113,113,0.55);
- color: #ffe4e6;
-}
-.auth-banner--info{
- background: rgba(56,189,248,0.14);
- border: 1px solid rgba(56,189,248,0.55);
- color: #e0f2fe;
-}
-.auth-banner--success{
- background: rgba(34,197,94,0.14);
- border: 1px solid rgba(34,197,94,0.55);
- color: #d1fae5;
-}
-
-.user-chip{
- display:inline-flex;
- align-items:center;
- gap: .45rem;
- padding: .22rem .55rem;
- border-radius: 999px;
- background: rgba(2,6,23,.25);
- border: 1px solid rgba(148,163,184,.55);
-}
-.user-avatar{
- width: 26px;
- height: 26px;
- border-radius: 999px;
- display:flex;
- align-items:center;
- justify-content:center;
- background: rgba(56,189,248,0.18);
- border: 1px solid rgba(56,189,248,0.35);
-}
-.user-avatar i{ font-size: 14px; }
-
-.user-name{
- font-size: .80rem;
- font-weight: 950;
- color: #f9fafb;
- max-width: 220px;
- overflow:hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-/* Light theme readability */
-body[data-theme="light"] .auth-pill{
- background: rgba(255,255,255,0.82);
- border-color: rgba(148,163,184,0.85);
- color: #0b1220;
-}
-body[data-theme="light"] .user-chip{
- background: rgba(255,255,255,0.82);
- border-color: rgba(148,163,184,0.85);
-}
-body[data-theme="light"] .user-name{ color:#0b1220; }
-body[data-theme="light"] .user-avatar{
- background: rgba(30,107,255,0.12);
- border-color: rgba(30,107,255,0.35);
- color:#0b1220;
-}
-/* SW_AUTH_UI:END */
-
-/* SW_AUTH_UI2:BEGIN */
-.sw-notice{
- margin: 10px 14px 0 14px;
- padding: 10px 12px;
- border-radius: 16px;
- font-size: .78rem;
- font-weight: 800;
- line-height: 1.2;
- border: 1px solid rgba(148,163,184,.35);
- background: rgba(2,6,23,.22);
- color: #e5e7eb;
-}
-.sw-notice--error{
- background: rgba(248,113,113,0.14);
- border-color: rgba(248,113,113,0.55);
- color: #ffe4e6;
-}
-.sw-notice--info{
- background: rgba(56,189,248,0.14);
- border-color: rgba(56,189,248,0.55);
- color: #e0f2fe;
-}
-.sw-notice--success{
- background: rgba(34,197,94,0.14);
- border-color: rgba(34,197,94,0.55);
- color: #d1fae5;
-}
-.sw-notice--warn{
- background: rgba(251,191,36,0.12);
- border-color: rgba(251,191,36,0.40);
- color: #fffbeb;
-}
-
-/* single user chip (no duplicates) */
-.user-chip{
- display:inline-flex;
- align-items:center;
- gap: .45rem;
- padding: .22rem .55rem;
- border-radius: 999px;
- background: rgba(2,6,23,.25);
- border: 1px solid rgba(148,163,184,.55);
-}
-.user-avatar{
- width: 26px;
- height: 26px;
- border-radius: 999px;
- display:flex;
- align-items:center;
- justify-content:center;
- background: rgba(56,189,248,0.18);
- border: 1px solid rgba(56,189,248,0.35);
-}
-.user-avatar i{ font-size: 14px; }
-.user-name{
- font-size: .82rem;
- font-weight: 950;
- color: #f9fafb;
- max-width: 180px;
- overflow:hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.user-badge{
- display:inline-flex;
- align-items:center;
- gap: .35rem;
- padding: .10rem .45rem;
- border-radius: 999px;
- font-size: .70rem;
- font-weight: 950;
- border: 1px solid rgba(34,197,94,.55);
- background: rgba(34,197,94,.14);
- color: #d1fae5;
-}
-/* SW_AUTH_UI2:END */
diff --git a/src/styles/topbar.css.bak.1766217948 b/src/styles/topbar.css.bak.1766217948
deleted file mode 100644
index af2ccea..0000000
--- a/src/styles/topbar.css.bak.1766217948
+++ /dev/null
@@ -1,425 +0,0 @@
-.topbar{
- position: sticky; top:0; z-index:100;
- width:100%;
- padding: .55rem .9rem;
- display:flex; align-items:center; justify-content:space-between; gap:.6rem;
- background: linear-gradient(135deg,#071225 0%,#0b2b5f 38%,#0e65c0 72%,#27a8ff 100%);
- border-bottom-left-radius:18px; border-bottom-right-radius:18px;
- box-shadow: 0 10px 24px rgba(0,0,0,.55), 0 0 18px rgba(56,189,248,.45);
- overflow:hidden;
-}
-
-.logo-circle{
- width:40px; height:40px; border-radius:50%; flex-shrink:0;
- background-image:url("/icons/logo-master.png");
- background-size:cover; background-position:center; background-repeat:no-repeat;
- box-shadow: 0 0 10px rgba(37,99,235,.8), 0 0 18px rgba(56,189,248,.6);
- color:transparent;
-}
-
-.title-block{ display:flex; flex-direction:column; line-height:1.05; min-width:0; }
-.main-title{ font-size:1.02rem; font-weight:800; letter-spacing:.04em; color:#f9fafb; }
-.sub-title{ font-size:.74rem; color:#dbeafe; opacity:.95; }
-
-.topbar-right{ display:flex; align-items:center; gap:.5rem; margin-left:auto; flex-wrap:wrap; }
-
-.theme-switch{ display:flex; gap:.28rem; }
-.theme-dot{
- width:22px; height:22px; border-radius:999px;
- border:1px solid rgba(148,163,184,.85);
- background:rgba(15,23,42,.9); color:#e5e7eb; font-size:.65rem;
- display:flex; align-items:center; justify-content:center; padding:0; cursor:pointer;
- box-shadow:0 0 6px rgba(15,23,42,.9); transition:transform 0.36s ease;
-}
-.theme-dot:hover{ transform:scale(1.06); }
-.theme-dot-active{ border-color:#facc15; box-shadow:0 0 10px rgba(250,204,21,.9),0 0 14px rgba(56,189,248,.55); }
-
-.btn-primary, .btn-secondary{
- padding:.32rem .85rem; font-size:.78rem; border-radius:999px; font-weight:700; cursor:pointer;
- white-space:nowrap; line-height:1;
-}
-.btn-primary{
- border:1px solid #22c55e;
- background:radial-gradient(circle at 30% 30%,#22c55e,#16a34a);
- color:#f9fafb;
- box-shadow:0 4px 10px rgba(22,163,74,.5),0 0 8px rgba(22,163,74,.6);
-}
-.btn-primary:disabled{ opacity:.6; cursor:default; }
-.btn-secondary{
- border:1px solid #bfdbfe;
- background:rgba(15,23,42,.25);
- color:#e0f2fe;
- box-shadow:0 3px 10px rgba(15,23,42,.6);
-}
-.btn-secondary:hover{ background:rgba(15,23,42,.45); }
-
-@media (max-width:640px){
- .topbar{ padding:.5rem .7rem; border-bottom-left-radius:16px; border-bottom-right-radius:16px; }
- .main-title{ font-size:1rem; }
- .sub-title{ font-size:.7rem; }
- .topbar-right{ width:100%; justify-content:flex-start; gap:.45rem; }
-}
-
-/* SW_AUTH_UI:BEGIN */
-.auth-strip{
- margin-top: 6px;
- display:flex;
- align-items:center;
- gap: .45rem;
- flex-wrap: wrap;
-}
-
-.auth-pill{
- display:inline-flex;
- align-items:center;
- gap: .35rem;
- padding: .18rem .55rem;
- border-radius: 999px;
- font-size: .70rem;
- font-weight: 900;
- letter-spacing: .02em;
- border: 1px solid rgba(148,163,184,.55);
- background: rgba(2,6,23,.25);
- color: #e5e7eb;
-}
-.auth-pill i{ font-size: .85rem; opacity:.95; }
-
-.auth-pill--ok{
- border-color: rgba(34,197,94,.65);
- background: rgba(34,197,94,.16);
-}
-.auth-pill--loading{
- border-color: rgba(56,189,248,.65);
- background: rgba(56,189,248,.14);
-}
-.auth-pill--anon{
- border-color: rgba(148,163,184,.55);
- background: rgba(2,6,23,.20);
-}
-
-.auth-banner{
- margin-top: 6px;
- padding: 6px 10px;
- border-radius: 14px;
- font-size: .72rem;
- font-weight: 800;
- display:flex;
- gap: 8px;
- align-items:center;
- flex-wrap:wrap;
-}
-.auth-banner--error{
- background: rgba(248,113,113,0.14);
- border: 1px solid rgba(248,113,113,0.55);
- color: #ffe4e6;
-}
-.auth-banner--info{
- background: rgba(56,189,248,0.14);
- border: 1px solid rgba(56,189,248,0.55);
- color: #e0f2fe;
-}
-.auth-banner--success{
- background: rgba(34,197,94,0.14);
- border: 1px solid rgba(34,197,94,0.55);
- color: #d1fae5;
-}
-
-.user-chip{
- display:inline-flex;
- align-items:center;
- gap: .45rem;
- padding: .22rem .55rem;
- border-radius: 999px;
- background: rgba(2,6,23,.25);
- border: 1px solid rgba(148,163,184,.55);
-}
-.user-avatar{
- width: 26px;
- height: 26px;
- border-radius: 999px;
- display:flex;
- align-items:center;
- justify-content:center;
- background: rgba(56,189,248,0.18);
- border: 1px solid rgba(56,189,248,0.35);
-}
-.user-avatar i{ font-size: 14px; }
-
-.user-name{
- font-size: .80rem;
- font-weight: 950;
- color: #f9fafb;
- max-width: 220px;
- overflow:hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-/* Light theme readability */
-body[data-theme="light"] .auth-pill{
- background: rgba(255,255,255,0.82);
- border-color: rgba(148,163,184,0.85);
- color: #0b1220;
-}
-body[data-theme="light"] .user-chip{
- background: rgba(255,255,255,0.82);
- border-color: rgba(148,163,184,0.85);
-}
-body[data-theme="light"] .user-name{ color:#0b1220; }
-body[data-theme="light"] .user-avatar{
- background: rgba(30,107,255,0.12);
- border-color: rgba(30,107,255,0.35);
- color:#0b1220;
-}
-/* SW_AUTH_UI:END */
-
-/* SW_AUTH_UI2:BEGIN */
-.sw-notice{
- margin: 10px 14px 0 14px;
- padding: 10px 12px;
- border-radius: 16px;
- font-size: .78rem;
- font-weight: 800;
- line-height: 1.2;
- border: 1px solid rgba(148,163,184,.35);
- background: rgba(2,6,23,.22);
- color: #e5e7eb;
-}
-.sw-notice--error{
- background: rgba(248,113,113,0.14);
- border-color: rgba(248,113,113,0.55);
- color: #ffe4e6;
-}
-.sw-notice--info{
- background: rgba(56,189,248,0.14);
- border-color: rgba(56,189,248,0.55);
- color: #e0f2fe;
-}
-.sw-notice--success{
- background: rgba(34,197,94,0.14);
- border-color: rgba(34,197,94,0.55);
- color: #d1fae5;
-}
-.sw-notice--warn{
- background: rgba(251,191,36,0.12);
- border-color: rgba(251,191,36,0.40);
- color: #fffbeb;
-}
-
-/* single user chip (no duplicates) */
-.user-chip{
- display:inline-flex;
- align-items:center;
- gap: .45rem;
- padding: .22rem .55rem;
- border-radius: 999px;
- background: rgba(2,6,23,.25);
- border: 1px solid rgba(148,163,184,.55);
-}
-.user-avatar{
- width: 26px;
- height: 26px;
- border-radius: 999px;
- display:flex;
- align-items:center;
- justify-content:center;
- background: rgba(56,189,248,0.18);
- border: 1px solid rgba(56,189,248,0.35);
-}
-.user-avatar i{ font-size: 14px; }
-.user-name{
- font-size: .82rem;
- font-weight: 950;
- color: #f9fafb;
- max-width: 180px;
- overflow:hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.user-badge{
- display:inline-flex;
- align-items:center;
- gap: .35rem;
- padding: .10rem .45rem;
- border-radius: 999px;
- font-size: .70rem;
- font-weight: 950;
- border: 1px solid rgba(34,197,94,.55);
- background: rgba(34,197,94,.14);
- color: #d1fae5;
-}
-/* SW_AUTH_UI2:END */
-
-
-/* SW_TOPBAR_POLISH:BEGIN */
-
-/* Make right side look like a clean “glass dock” */
-.topbar-right{
- padding: .28rem .35rem;
- border-radius: 999px;
- background: rgba(2,6,23,.18);
- border: 1px solid rgba(255,255,255,.14);
- backdrop-filter: blur(10px);
-}
-
-/* User chip used by TopBar.jsx (missing styles before) */
-.sw-userchip{
- display:flex;
- align-items:center;
- gap:.5rem;
- padding:.28rem .55rem;
- border-radius: 999px;
- background: rgba(2,6,23,.22);
- border: 1px solid rgba(148,163,184,.45);
- box-shadow: 0 4px 14px rgba(0,0,0,.25);
-}
-
-.sw-avatar{
- width: 30px;
- height: 30px;
- border-radius: 999px;
- display:grid;
- place-items:center;
- background: rgba(56,189,248,0.18);
- border: 1px solid rgba(56,189,248,0.35);
- color: #e5e7eb;
- font-weight: 950;
- overflow:hidden;
-}
-.sw-avatar i{ font-size: 14px; }
-
-.sw-usertext{
- display:flex;
- flex-direction:column;
- line-height: 1.05;
- min-width: 0;
-}
-.sw-userline{
- font-size: .72rem;
- font-weight: 950;
- color: #f9fafb;
-}
-.sw-userhandle{
- font-size: .68rem;
- font-weight: 800;
- color: #dbeafe;
- opacity: .92;
- max-width: 160px;
- overflow:hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-/* Mobile: dock becomes full width and nice */
-@media (max-width:640px){
- .topbar-right{
- width:100%;
- border-radius: 16px;
- justify-content: flex-start;
- }
-}
-
-/* Light theme readability */
-body[data-theme="light"] .topbar-right{
- background: rgba(255,255,255,.55);
- border-color: rgba(148,163,184,.55);
-}
-body[data-theme="light"] .sw-userchip{
- background: rgba(255,255,255,.80);
- border-color: rgba(148,163,184,.70);
- color: #0b1220;
-}
-body[data-theme="light"] .sw-userline,
-body[data-theme="light"] .sw-userhandle{
- color:#0b1220;
-}
-body[data-theme="light"] .sw-avatar{
- background: rgba(30,107,255,0.10);
- border-color: rgba(30,107,255,0.35);
- color:#0b1220;
-}
-
-/* SW_TOPBAR_POLISH:END */
-
-/* SW_TOPBAR_OVERFLOW_FIX:BEGIN */
-.topbar{
- flex-wrap: wrap; /* ✅ allow wrap instead of overflowing right */
- row-gap: .45rem;
-}
-
-.topbar-left{
- display:flex;
- align-items:center;
- gap: .65rem;
- min-width: 0; /* ✅ allow shrinking */
-}
-
-.title-block{ min-width:0; }
-.main-title, .sub-title{
- max-width: 58vw;
- overflow:hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.topbar-right{
- min-width: 0;
- max-width: 100%;
- flex: 1 1 auto;
- justify-content: flex-end;
- flex-wrap: wrap; /* ✅ wrap buttons instead of clipping */
- gap: .45rem;
-}
-
-@media (max-width:640px){
- .main-title, .sub-title{ max-width: 86vw; }
- .topbar-right{
- justify-content: flex-start;
- width: 100%;
- }
-}
-
-/* Unified auth button (guest online / log on / log off) */
-.sw-auth-combo{
- display:flex;
- align-items:center;
- gap:.5rem;
- padding:.25rem .55rem;
- border-radius: 999px;
- border: 1px solid rgba(148,163,184,.55);
- background: rgba(2,6,23,.22);
- color: #e5e7eb;
- cursor:pointer;
- max-width: 100%;
- box-shadow: 0 4px 14px rgba(0,0,0,.22);
-}
-
-.sw-auth-combo .sw-usertext{
- min-width: 0;
-}
-
-.sw-auth-combo .sw-userhandle{
- max-width: 120px;
- overflow:hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.sw-auth-action{
- margin-left: .25rem;
- padding: .22rem .55rem;
- border-radius: 999px;
- font-weight: 950;
- font-size: .70rem;
- border: 1px solid rgba(56,189,248,.35);
- background: rgba(56,189,248,.14);
- color: #dbeafe;
- white-space: nowrap;
-}
-
-.sw-auth-combo--on .sw-auth-action{
- border-color: rgba(34,197,94,.45);
- background: rgba(34,197,94,.16);
- color: #d1fae5;
-}
-/* SW_TOPBAR_OVERFLOW_FIX:END */
diff --git a/src/styles/topbar.css.bak.20251219-163055 b/src/styles/topbar.css.bak.20251219-163055
deleted file mode 100644
index aa73bf4..0000000
--- a/src/styles/topbar.css.bak.20251219-163055
+++ /dev/null
@@ -1,61 +0,0 @@
-.topbar{
- position: sticky; top:0; z-index:100;
- width:100%;
- padding: .55rem .9rem;
- display:flex; align-items:center; justify-content:space-between; gap:.6rem;
- background: linear-gradient(135deg,#071225 0%,#0b2b5f 38%,#0e65c0 72%,#27a8ff 100%);
- border-bottom-left-radius:18px; border-bottom-right-radius:18px;
- box-shadow: 0 10px 24px rgba(0,0,0,.55), 0 0 18px rgba(56,189,248,.45);
- overflow:hidden;
-}
-
-.logo-circle{
- width:40px; height:40px; border-radius:50%; flex-shrink:0;
- background-image:url("/icons/logo-master.png");
- background-size:cover; background-position:center; background-repeat:no-repeat;
- box-shadow: 0 0 10px rgba(37,99,235,.8), 0 0 18px rgba(56,189,248,.6);
- color:transparent;
-}
-
-.title-block{ display:flex; flex-direction:column; line-height:1.05; min-width:0; }
-.main-title{ font-size:1.02rem; font-weight:800; letter-spacing:.04em; color:#f9fafb; }
-.sub-title{ font-size:.74rem; color:#dbeafe; opacity:.95; }
-
-.topbar-right{ display:flex; align-items:center; gap:.5rem; margin-left:auto; flex-wrap:wrap; }
-
-.theme-switch{ display:flex; gap:.28rem; }
-.theme-dot{
- width:22px; height:22px; border-radius:999px;
- border:1px solid rgba(148,163,184,.85);
- background:rgba(15,23,42,.9); color:#e5e7eb; font-size:.65rem;
- display:flex; align-items:center; justify-content:center; padding:0; cursor:pointer;
- box-shadow:0 0 6px rgba(15,23,42,.9); transition:transform 0.36s ease;
-}
-.theme-dot:hover{ transform:scale(1.06); }
-.theme-dot-active{ border-color:#facc15; box-shadow:0 0 10px rgba(250,204,21,.9),0 0 14px rgba(56,189,248,.55); }
-
-.btn-primary, .btn-secondary{
- padding:.32rem .85rem; font-size:.78rem; border-radius:999px; font-weight:700; cursor:pointer;
- white-space:nowrap; line-height:1;
-}
-.btn-primary{
- border:1px solid #22c55e;
- background:radial-gradient(circle at 30% 30%,#22c55e,#16a34a);
- color:#f9fafb;
- box-shadow:0 4px 10px rgba(22,163,74,.5),0 0 8px rgba(22,163,74,.6);
-}
-.btn-primary:disabled{ opacity:.6; cursor:default; }
-.btn-secondary{
- border:1px solid #bfdbfe;
- background:rgba(15,23,42,.25);
- color:#e0f2fe;
- box-shadow:0 3px 10px rgba(15,23,42,.6);
-}
-.btn-secondary:hover{ background:rgba(15,23,42,.45); }
-
-@media (max-width:640px){
- .topbar{ padding:.5rem .7rem; border-bottom-left-radius:16px; border-bottom-right-radius:16px; }
- .main-title{ font-size:1rem; }
- .sub-title{ font-size:.7rem; }
- .topbar-right{ width:100%; justify-content:flex-start; gap:.45rem; }
-}
diff --git a/src/styles/topbar.css.bak.20251219-164233 b/src/styles/topbar.css.bak.20251219-164233
deleted file mode 100644
index bb73d50..0000000
--- a/src/styles/topbar.css.bak.20251219-164233
+++ /dev/null
@@ -1,174 +0,0 @@
-.topbar{
- position: sticky; top:0; z-index:100;
- width:100%;
- padding: .55rem .9rem;
- display:flex; align-items:center; justify-content:space-between; gap:.6rem;
- background: linear-gradient(135deg,#071225 0%,#0b2b5f 38%,#0e65c0 72%,#27a8ff 100%);
- border-bottom-left-radius:18px; border-bottom-right-radius:18px;
- box-shadow: 0 10px 24px rgba(0,0,0,.55), 0 0 18px rgba(56,189,248,.45);
- overflow:hidden;
-}
-
-.logo-circle{
- width:40px; height:40px; border-radius:50%; flex-shrink:0;
- background-image:url("/icons/logo-master.png");
- background-size:cover; background-position:center; background-repeat:no-repeat;
- box-shadow: 0 0 10px rgba(37,99,235,.8), 0 0 18px rgba(56,189,248,.6);
- color:transparent;
-}
-
-.title-block{ display:flex; flex-direction:column; line-height:1.05; min-width:0; }
-.main-title{ font-size:1.02rem; font-weight:800; letter-spacing:.04em; color:#f9fafb; }
-.sub-title{ font-size:.74rem; color:#dbeafe; opacity:.95; }
-
-.topbar-right{ display:flex; align-items:center; gap:.5rem; margin-left:auto; flex-wrap:wrap; }
-
-.theme-switch{ display:flex; gap:.28rem; }
-.theme-dot{
- width:22px; height:22px; border-radius:999px;
- border:1px solid rgba(148,163,184,.85);
- background:rgba(15,23,42,.9); color:#e5e7eb; font-size:.65rem;
- display:flex; align-items:center; justify-content:center; padding:0; cursor:pointer;
- box-shadow:0 0 6px rgba(15,23,42,.9); transition:transform 0.36s ease;
-}
-.theme-dot:hover{ transform:scale(1.06); }
-.theme-dot-active{ border-color:#facc15; box-shadow:0 0 10px rgba(250,204,21,.9),0 0 14px rgba(56,189,248,.55); }
-
-.btn-primary, .btn-secondary{
- padding:.32rem .85rem; font-size:.78rem; border-radius:999px; font-weight:700; cursor:pointer;
- white-space:nowrap; line-height:1;
-}
-.btn-primary{
- border:1px solid #22c55e;
- background:radial-gradient(circle at 30% 30%,#22c55e,#16a34a);
- color:#f9fafb;
- box-shadow:0 4px 10px rgba(22,163,74,.5),0 0 8px rgba(22,163,74,.6);
-}
-.btn-primary:disabled{ opacity:.6; cursor:default; }
-.btn-secondary{
- border:1px solid #bfdbfe;
- background:rgba(15,23,42,.25);
- color:#e0f2fe;
- box-shadow:0 3px 10px rgba(15,23,42,.6);
-}
-.btn-secondary:hover{ background:rgba(15,23,42,.45); }
-
-@media (max-width:640px){
- .topbar{ padding:.5rem .7rem; border-bottom-left-radius:16px; border-bottom-right-radius:16px; }
- .main-title{ font-size:1rem; }
- .sub-title{ font-size:.7rem; }
- .topbar-right{ width:100%; justify-content:flex-start; gap:.45rem; }
-}
-
-/* SW_AUTH_UI:BEGIN */
-.auth-strip{
- margin-top: 6px;
- display:flex;
- align-items:center;
- gap: .45rem;
- flex-wrap: wrap;
-}
-
-.auth-pill{
- display:inline-flex;
- align-items:center;
- gap: .35rem;
- padding: .18rem .55rem;
- border-radius: 999px;
- font-size: .70rem;
- font-weight: 900;
- letter-spacing: .02em;
- border: 1px solid rgba(148,163,184,.55);
- background: rgba(2,6,23,.25);
- color: #e5e7eb;
-}
-.auth-pill i{ font-size: .85rem; opacity:.95; }
-
-.auth-pill--ok{
- border-color: rgba(34,197,94,.65);
- background: rgba(34,197,94,.16);
-}
-.auth-pill--loading{
- border-color: rgba(56,189,248,.65);
- background: rgba(56,189,248,.14);
-}
-.auth-pill--anon{
- border-color: rgba(148,163,184,.55);
- background: rgba(2,6,23,.20);
-}
-
-.auth-banner{
- margin-top: 6px;
- padding: 6px 10px;
- border-radius: 14px;
- font-size: .72rem;
- font-weight: 800;
- display:flex;
- gap: 8px;
- align-items:center;
- flex-wrap:wrap;
-}
-.auth-banner--error{
- background: rgba(248,113,113,0.14);
- border: 1px solid rgba(248,113,113,0.55);
- color: #ffe4e6;
-}
-.auth-banner--info{
- background: rgba(56,189,248,0.14);
- border: 1px solid rgba(56,189,248,0.55);
- color: #e0f2fe;
-}
-.auth-banner--success{
- background: rgba(34,197,94,0.14);
- border: 1px solid rgba(34,197,94,0.55);
- color: #d1fae5;
-}
-
-.user-chip{
- display:inline-flex;
- align-items:center;
- gap: .45rem;
- padding: .22rem .55rem;
- border-radius: 999px;
- background: rgba(2,6,23,.25);
- border: 1px solid rgba(148,163,184,.55);
-}
-.user-avatar{
- width: 26px;
- height: 26px;
- border-radius: 999px;
- display:flex;
- align-items:center;
- justify-content:center;
- background: rgba(56,189,248,0.18);
- border: 1px solid rgba(56,189,248,0.35);
-}
-.user-avatar i{ font-size: 14px; }
-
-.user-name{
- font-size: .80rem;
- font-weight: 950;
- color: #f9fafb;
- max-width: 220px;
- overflow:hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-/* Light theme readability */
-body[data-theme="light"] .auth-pill{
- background: rgba(255,255,255,0.82);
- border-color: rgba(148,163,184,0.85);
- color: #0b1220;
-}
-body[data-theme="light"] .user-chip{
- background: rgba(255,255,255,0.82);
- border-color: rgba(148,163,184,0.85);
-}
-body[data-theme="light"] .user-name{ color:#0b1220; }
-body[data-theme="light"] .user-avatar{
- background: rgba(30,107,255,0.12);
- border-color: rgba(30,107,255,0.35);
- color:#0b1220;
-}
-/* SW_AUTH_UI:END */