-
-
-
+
+
+
+ {t("publicProfile")}
+
+
+
+
+
+
+ {user.username}
+
+
+ {user.motto || {t("noMotto")}}
+
+
+ {user.mail || t("noEmail")}
+
+
-
-
-
-
-
-
- {t("securityDescription")}
-
-
- {t("twoFactorLink")}
-
-
-
-
+
+
+ {/* Motto form */}
+
+
+
+
+
+ {t("changeMotto")}
+
+
+
+
+ {t("changeMottoSubtitle")}
+
+
+
+
+
+
+ {/* Security card */}
+
+
+
+
+
+ {t("securityTitle")}
+
+
+
+
+ {t("securityDescription")}
+
+
+ {t("twoFactorLink")}
+
+
+
+
+
+ {/* Sessions link */}
+
+
+
+
+
+
+
+
+
+ Session Management
+
+
+ View active sessions and sign out everywhere
+
+
+
+
+ →
+
+
+
+
+
);
}
diff --git a/src/app/globals.css b/src/app/globals.css
index 7aba17bc..9e4200e9 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1742,6 +1742,69 @@ details[open] > summary .details-open\:rotate-180 {
border-radius: calc(var(--radius-lg) - 1px);
}
+/* Input focus glow */
+.input-glow:focus {
+ outline: none;
+ border-color: var(--color-primary) !important;
+ box-shadow:
+ 0 0 0 3px color-mix(in srgb, var(--color-primary) 15%, transparent),
+ 0 0 20px color-mix(in srgb, var(--color-primary) 10%, transparent);
+}
+.input-glow {
+ transition:
+ border-color 0.25s ease,
+ box-shadow 0.25s ease;
+}
+
+/* Typing cursor blink */
+@keyframes blink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0; }
+}
+.typing-cursor::after {
+ content: "|";
+ animation: blink 0.8s step-end infinite;
+ margin-left: 2px;
+ font-weight: 300;
+ color: var(--color-primary);
+}
+
+/* Number count-up */
+@keyframes count-up {
+ from { opacity: 0; transform: translateY(12px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+.animate-count {
+ animation: count-up 0.6s ease-out both;
+}
+
+/* Premium button shine */
+.btn-shine {
+ position: relative;
+ overflow: hidden;
+}
+.btn-shine::after {
+ content: "";
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background: linear-gradient(
+ transparent 0%,
+ transparent 35%,
+ color-mix(in srgb, white 8%, transparent) 50%,
+ transparent 65%,
+ transparent 100%
+ );
+ transform: rotate(25deg);
+ transition: all 0.5s ease;
+ pointer-events: none;
+}
+.btn-shine:hover::after {
+ left: 100%;
+}
+
/* Staggered children animation */
.stagger-children > * {
animation: fade-in-up 0.5s ease-out both;
diff --git a/src/components/ambient-orbs.tsx b/src/components/ambient-orbs.tsx
new file mode 100644
index 00000000..b0b2e928
--- /dev/null
+++ b/src/components/ambient-orbs.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+interface Orb {
+ id: number;
+ x: number;
+ y: number;
+ size: number;
+ delay: number;
+ duration: number;
+ color: string;
+}
+
+export function AmbientOrbs() {
+ const [orbs, setOrbs] = useState
([]);
+
+ useEffect(() => {
+ const colors = [
+ "var(--color-primary)",
+ "var(--color-accent)",
+ "color-mix(in srgb, var(--color-primary) 60%, var(--color-accent))",
+ ];
+ const items: Orb[] = Array.from({ length: 3 }, (_, i) => ({
+ id: i,
+ x: 15 + Math.random() * 70,
+ y: 10 + Math.random() * 70,
+ size: 120 + Math.random() * 180,
+ delay: Math.random() * 3,
+ duration: 8 + Math.random() * 6,
+ color: colors[i],
+ }));
+ setOrbs(items);
+ }, []);
+
+ if (orbs.length === 0) return null;
+
+ return (
+
+ {orbs.map((orb) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/animated-counter.tsx b/src/components/animated-counter.tsx
new file mode 100644
index 00000000..f0fd055b
--- /dev/null
+++ b/src/components/animated-counter.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import { motion, useInView } from "framer-motion";
+import { useEffect, useRef, useState } from "react";
+
+export function AnimatedCounter({
+ value,
+ suffix = "",
+}: {
+ value: number;
+ suffix?: string;
+}) {
+ const ref = useRef(null);
+ const isInView = useInView(ref, { once: true, margin: "-50px" });
+ const [display, setDisplay] = useState(0);
+
+ useEffect(() => {
+ if (!isInView) return;
+ const duration = 1200;
+ const steps = 30;
+ const increment = value / steps;
+ let current = 0;
+ let step = 0;
+ const id = setInterval(() => {
+ step++;
+ current = Math.min(Math.round(increment * step), value);
+ setDisplay(current);
+ if (step >= steps) {
+ clearInterval(id);
+ setDisplay(value);
+ }
+ }, duration / steps);
+ return () => clearInterval(id);
+ }, [isInView, value]);
+
+ return (
+
+ {display.toLocaleString()}{suffix}
+
+ );
+}
diff --git a/src/components/auth/home-login-form.tsx b/src/components/auth/home-login-form.tsx
index 89a16fc7..c9559d90 100644
--- a/src/components/auth/home-login-form.tsx
+++ b/src/components/auth/home-login-form.tsx
@@ -18,6 +18,7 @@ export function HomeLoginForm({
} = {}) {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
+ const [showPassword, setShowPassword] = useState(false);
const [code, setCode] = useState("");
const [needs2fa, setNeeds2fa] = useState(false);
const [error, setError] = useState(null);
@@ -87,7 +88,7 @@ export function HomeLoginForm({
placeholder="Your username"
autoComplete="username"
disabled={needs2fa}
- className="w-full rounded-xl border-2 px-4 py-3 text-sm font-medium transition-all focus:outline-none disabled:opacity-50"
+ className="input-glow w-full rounded-xl border-2 px-4 py-3 text-sm font-medium transition-all focus:outline-none disabled:opacity-50"
style={{
backgroundColor: "var(--color-background)",
color: "var(--color-text-readable)",
@@ -100,31 +101,44 @@ export function HomeLoginForm({
-
-
setPassword(e.target.value)}
- placeholder="Your password"
- autoComplete="current-password"
- disabled={needs2fa}
- className="w-full rounded-xl border-2 px-4 py-3 text-sm font-medium transition-all focus:outline-none disabled:opacity-50"
- style={{
- backgroundColor: "var(--color-background)",
- color: "var(--color-text-readable)",
- borderColor: needs2fa
- ? "color-mix(in srgb, var(--color-text-muted) 15%, transparent)"
- : "color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
- }}
- required
- />
+
+
+
+
+ setPassword(e.target.value)}
+ placeholder="Your password"
+ autoComplete="current-password"
+ disabled={needs2fa}
+ className="input-glow w-full rounded-xl border-2 px-4 py-3 text-sm font-medium transition-all focus:outline-none disabled:opacity-50"
+ style={{
+ backgroundColor: "var(--color-background)",
+ color: "var(--color-text-readable)",
+ borderColor: needs2fa
+ ? "color-mix(in srgb, var(--color-text-muted) 15%, transparent)"
+ : "color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
+ }}
+ required
+ />
+
+
{needs2fa ? (
@@ -169,7 +183,7 @@ export function HomeLoginForm({