diff --git a/src/app/(site)/login/page.tsx b/src/app/(site)/login/page.tsx
index 3eec0a4e..408f3532 100644
--- a/src/app/(site)/login/page.tsx
+++ b/src/app/(site)/login/page.tsx
@@ -72,6 +72,21 @@ export default async function LoginPage() {
className="absolute inset-0 overflow-hidden"
>
+
+
@@ -88,16 +103,24 @@ export default async function LoginPage() {
className="inline-flex items-center gap-2 px-3.5 py-1 rounded-full text-xs font-bold uppercase tracking-wider mt-4 mb-3 border"
style={{
borderColor:
- "color-mix(in srgb, var(--color-primary) 18%, transparent)",
+ "color-mix(in srgb, var(--color-primary) 30%, transparent)",
background:
- "color-mix(in srgb, var(--color-primary) 10%, transparent)",
+ "color-mix(in srgb, var(--color-surface) 72%, transparent)",
color:
"var(--color-primary-readable, var(--color-primary))",
+ boxShadow:
+ "0 8px 20px -12px color-mix(in srgb, var(--color-primary) 70%, transparent)",
}}
>
-
-
+
+
{t("usersOnline", { count: online })}
@@ -111,7 +134,7 @@ export default async function LoginPage() {
{t("welcomeBack")}
{t("welcomeBackSub", { hotelName })}
@@ -130,26 +153,19 @@ export default async function LoginPage() {
{recentUsers.map((u) => (
-
-
- {u.username}
-
+
+
+
+
+
{u.username}
))}
@@ -167,13 +183,8 @@ export default async function LoginPage() {
{latestUsers.map((u) => (
-
- {u.username}
-
+ {u.username}
))}
@@ -196,26 +202,29 @@ export default async function LoginPage() {
{/* Right: form */}
-
-
+
- {t("subtitle")}
-
-
-
+
+ {t("subtitle")}
+
+
+
+
diff --git a/src/app/(site)/page.tsx b/src/app/(site)/page.tsx
index 99cd006e..bc8788b6 100644
--- a/src/app/(site)/page.tsx
+++ b/src/app/(site)/page.tsx
@@ -1,4 +1,5 @@
import { count, desc, eq } from "drizzle-orm";
+import { ChevronDown } from "lucide-react";
import type { Metadata } from "next";
import { headers } from "next/headers";
import Image from "next/image";
@@ -484,6 +485,29 @@ export default async function Home() {
"linear-gradient(180deg, rgba(8,11,24,0.74) 0%, rgba(8,11,24,0.55) 38%, rgba(8,11,24,0.38) 62%, rgba(8,11,24,0.55) 85%, color-mix(in srgb, var(--color-background) 94%, transparent) 100%)",
}}
/>
+ {/* Drifting brand-tinted aurora behind the copy */}
+
{recentUsers !== null && recentUsers.length > 0 && (
-
+
{recentUsers.slice(0, 4).map((u) => (
{tp("recentUsers")}
@@ -577,14 +609,7 @@ export default async function Home() {
- {th("login")} →
+ {th("login")}
+
+ →
+
@@ -633,6 +657,28 @@ export default async function Home() {
+ {/* Scroll cue */}
+
+
{/* Floating mascot — decorative, hidden on smaller screens. */}
- {exploreLinks.map((l) => (
-
+ {exploreLinks.map((l, i) => (
+
+
{l.label}
@@ -787,64 +832,73 @@ export default async function Home() {
-
- {[
- { value: users, label: tp("statsCitizens"), icon: ICON_FRIENDS },
- { value: rooms, label: tp("statsRooms"), icon: ICON_CATALOG },
- {
- value: totalPhotos,
- label: tp("statsPhotos"),
- icon: ICON_CAMERA,
- },
- {
- value: articles?.length ?? null,
- label: tp("statsArticles"),
- icon: ICON_ARTICLE,
- },
- ].map((s) => (
-
+
+
+ {[
+ {
+ value: users,
+ label: tp("statsCitizens"),
+ icon: ICON_FRIENDS,
+ },
+ { value: rooms, label: tp("statsRooms"), icon: ICON_CATALOG },
+ {
+ value: totalPhotos,
+ label: tp("statsPhotos"),
+ icon: ICON_CAMERA,
+ },
+ {
+ value: articles?.length ?? null,
+ label: tp("statsArticles"),
+ icon: ICON_ARTICLE,
+ },
+ ].map((s) => (
-
+
+
+
+
+ {s.value === null ? (
+
{te("unavailable")}
+ ) : (
+
+ )}
+
+
+ {s.label}
+
-
- {s.value === null ? (
-
{te("unavailable")}
- ) : (
-
- )}
-
-
- {s.label}
-
-
- ))}
+ ))}
+
@@ -1011,6 +1065,28 @@ export default async function Home() {
className="brand-halo left-1/2 top-0 h-72 w-[620px] max-w-full -translate-x-1/2 -translate-y-1/3"
style={{ opacity: 0.6 }}
/>
+
{tp("ctaJoin")}
-
+
→
{tp("browseCommunity")}
+
diff --git a/src/app/(site)/register/page.tsx b/src/app/(site)/register/page.tsx
index 81a751dc..fb261541 100644
--- a/src/app/(site)/register/page.tsx
+++ b/src/app/(site)/register/page.tsx
@@ -82,6 +82,21 @@ export default async function RegisterPage() {
className="absolute inset-0 overflow-hidden"
>
+
+
@@ -102,16 +117,24 @@ export default async function RegisterPage() {
className="inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-2 border"
style={{
borderColor:
- "color-mix(in srgb, var(--color-primary) 18%, transparent)",
+ "color-mix(in srgb, var(--color-primary) 30%, transparent)",
background:
- "color-mix(in srgb, var(--color-primary) 10%, transparent)",
+ "color-mix(in srgb, var(--color-surface) 72%, transparent)",
color:
"var(--color-primary-readable, var(--color-primary))",
+ boxShadow:
+ "0 8px 20px -12px color-mix(in srgb, var(--color-primary) 70%, transparent)",
}}
>
-
-
+
+
{tpr("usersOnline", { count: online })}
@@ -148,26 +171,18 @@ export default async function RegisterPage() {
-
-
- {u.username}
-
+
+
+
+
+
{u.username}
))}
@@ -186,13 +201,7 @@ export default async function RegisterPage() {
-
- {u.username}
-
+ {u.username}
))}
@@ -215,26 +219,29 @@ export default async function RegisterPage() {
{/* Right: form */}
-
-
+
- {tpr("subtitle")}
-
-
-
+
+ {tpr("subtitle")}
+
+
+
+
diff --git a/src/app/globals.css b/src/app/globals.css
index 7bf30a6e..5be90f83 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -2162,14 +2162,14 @@ details[open] > summary .details-open\:rotate-180 {
/* Glass morphism */
.glass {
background: color-mix(in srgb, var(--color-surface) 70%, transparent);
- backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
+ backdrop-filter: blur(20px);
border: 1px solid color-mix(in srgb, var(--color-text-muted) 10%, transparent);
}
.glass-strong {
background: color-mix(in srgb, var(--color-surface) 85%, transparent);
- backdrop-filter: blur(32px);
-webkit-backdrop-filter: blur(32px);
+ backdrop-filter: blur(32px);
border: 1px solid color-mix(in srgb, var(--color-text-muted) 12%, transparent);
}
@@ -2532,3 +2532,308 @@ html {
letter-spacing: 0.12em;
color: var(--color-text-muted);
}
+
+/* ── Public page polish: buttons ──────────────────────────────────── */
+.btn-brand {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ border-radius: 999px;
+ background: linear-gradient(
+ 120deg,
+ var(--color-primary) 0%,
+ color-mix(in srgb, var(--color-primary) 72%, var(--color-accent)) 55%,
+ color-mix(in srgb, var(--color-accent) 85%, var(--color-primary)) 100%
+ );
+ color: var(--color-primary-foreground);
+ box-shadow:
+ 0 10px 26px -12px color-mix(in srgb, var(--color-primary) 75%, transparent),
+ inset 0 1px 0 0 color-mix(in srgb, white 35%, transparent);
+ transition:
+ transform 0.22s ease,
+ box-shadow 0.22s ease,
+ filter 0.22s ease;
+}
+.btn-brand:hover {
+ transform: translateY(-2px);
+ filter: brightness(1.05);
+ box-shadow:
+ 0 18px 36px -14px color-mix(in srgb, var(--color-primary) 85%, transparent),
+ inset 0 1px 0 0 color-mix(in srgb, white 45%, transparent);
+}
+.btn-brand:active {
+ transform: translateY(0) scale(0.985);
+}
+.btn-brand:focus-visible {
+ outline: none;
+ box-shadow:
+ 0 0 0 3px color-mix(in srgb, var(--color-primary) 35%, transparent),
+ 0 14px 32px -14px color-mix(in srgb, var(--color-primary) 80%, transparent);
+}
+
+/* Frosted CTA meant to sit on top of the dark hero image. */
+.btn-glass-dark {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ border-radius: 999px;
+ color: #fff;
+ background: color-mix(in srgb, #ffffff 10%, transparent);
+ border: 1px solid color-mix(in srgb, #ffffff 34%, transparent);
+ -webkit-backdrop-filter: blur(12px);
+ backdrop-filter: blur(12px);
+ box-shadow: inset 0 1px 0 0 color-mix(in srgb, #ffffff 25%, transparent);
+ transition:
+ background 0.22s ease,
+ border-color 0.22s ease,
+ transform 0.22s ease;
+}
+.btn-glass-dark:hover {
+ background: color-mix(in srgb, #ffffff 20%, transparent);
+ border-color: color-mix(in srgb, #ffffff 60%, transparent);
+ transform: translateY(-2px);
+}
+.btn-glass-dark:active {
+ transform: translateY(0);
+}
+.btn-glass-dark:focus-visible {
+ outline: none;
+ border-color: #fff;
+ box-shadow: 0 0 0 3px
+ color-mix(in srgb, var(--color-primary) 55%, transparent);
+}
+
+/* ── Public page polish: auth fields ──────────────────────────────── */
+.auth-label {
+ display: block;
+ font-size: 11px;
+ font-weight: 800;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--color-text-muted);
+ margin-bottom: 0.4rem;
+}
+.auth-input {
+ width: 100%;
+ border-radius: 14px;
+ border: 1.5px solid
+ color-mix(in srgb, var(--color-text-muted) 22%, transparent);
+ background: color-mix(
+ in srgb,
+ var(--color-background) 88%,
+ var(--color-surface)
+ );
+ padding: 0.78rem 1rem;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--color-text-readable);
+ transition:
+ border-color 0.2s ease,
+ box-shadow 0.2s ease,
+ background-color 0.2s ease;
+}
+.auth-input::placeholder {
+ color: color-mix(in srgb, var(--color-text-muted) 75%, transparent);
+ font-weight: 500;
+}
+.auth-input:hover:not(:disabled):not(:focus) {
+ border-color: color-mix(in srgb, var(--color-primary) 45%, transparent);
+}
+.auth-input:focus {
+ outline: none;
+ border-color: var(--color-primary);
+ background: var(--color-surface);
+ box-shadow:
+ 0 0 0 3px color-mix(in srgb, var(--color-primary) 16%, transparent),
+ 0 0 24px -8px color-mix(in srgb, var(--color-primary) 60%, transparent);
+}
+.auth-input:disabled {
+ opacity: 0.55;
+ cursor: not-allowed;
+}
+.auth-input.has-icon {
+ padding-left: 2.75rem;
+}
+.auth-input.has-action {
+ padding-right: 2.75rem;
+}
+
+/* Alert box used for form errors. */
+.auth-alert {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+ border-radius: 14px;
+ padding: 0.7rem 0.85rem;
+ font-size: 0.85rem;
+ font-weight: 700;
+ color: color-mix(
+ in srgb,
+ var(--color-danger) 85%,
+ var(--color-text-readable)
+ );
+ background: color-mix(in srgb, var(--color-danger) 10%, transparent);
+ border: 1px solid color-mix(in srgb, var(--color-danger) 32%, transparent);
+}
+
+/* ── Public page polish: explore pills ────────────────────────────── */
+.explore-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.6rem 1.15rem;
+ border-radius: 999px;
+ font-size: 0.85rem;
+ font-weight: 700;
+ color: var(--color-text-readable);
+ background: color-mix(in srgb, var(--color-surface) 55%, transparent);
+ border: 1px solid color-mix(in srgb, var(--color-text-muted) 16%, transparent);
+ -webkit-backdrop-filter: blur(10px);
+ backdrop-filter: blur(10px);
+ transition:
+ background 0.25s ease,
+ border-color 0.25s ease,
+ transform 0.25s ease,
+ box-shadow 0.25s ease;
+}
+.explore-pill:hover {
+ background: color-mix(
+ in srgb,
+ var(--color-primary) 16%,
+ var(--color-surface)
+ );
+ border-color: color-mix(in srgb, var(--color-primary) 50%, transparent);
+ transform: translateY(-2px);
+ box-shadow: 0 12px 26px -14px
+ color-mix(in srgb, var(--color-primary) 80%, transparent);
+}
+.explore-pill .pill-arrow {
+ opacity: 0;
+ transform: translateX(-4px);
+ transition:
+ opacity 0.25s ease,
+ transform 0.25s ease;
+}
+.explore-pill:hover .pill-arrow {
+ opacity: 1;
+ transform: translateX(0);
+}
+.explore-pill:focus-visible {
+ outline: none;
+ border-color: var(--color-primary);
+ box-shadow: 0 0 0 3px
+ color-mix(in srgb, var(--color-primary) 20%, transparent);
+}
+
+/* ── Public page polish: avatar tiles ─────────────────────────────── */
+.avatar-tile {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.4rem;
+ padding: 0.55rem 0.3rem;
+ border-radius: 14px;
+ background: color-mix(in srgb, var(--color-primary) 6%, transparent);
+ border: 1px solid color-mix(in srgb, var(--color-primary) 14%, transparent);
+ transition:
+ transform 0.25s ease,
+ border-color 0.25s ease,
+ box-shadow 0.25s ease,
+ background 0.25s ease;
+}
+.avatar-tile:hover {
+ transform: translateY(-3px);
+ background: color-mix(in srgb, var(--color-primary) 12%, transparent);
+ border-color: color-mix(in srgb, var(--color-primary) 45%, transparent);
+ box-shadow: 0 12px 24px -14px
+ color-mix(in srgb, var(--color-primary) 85%, transparent);
+}
+.avatar-name {
+ font-size: 9.5px;
+ font-weight: 800;
+ letter-spacing: 0.04em;
+ color: var(--color-text-muted);
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.avatar-tile:hover .avatar-name {
+ color: var(--color-text-readable);
+}
+.avatar-online {
+ width: 8px;
+ height: 8px;
+ border-radius: 999px;
+ background: var(--color-accent);
+ box-shadow: 0 0 0 2px var(--color-surface);
+}
+
+/* ── Public page polish: drifting aurora blobs ────────────────────── */
+.aurora-blob {
+ position: absolute;
+ border-radius: 999px;
+ filter: blur(70px);
+ pointer-events: none;
+ will-change: transform;
+ animation: aurora-drift 16s ease-in-out infinite alternate;
+}
+@keyframes aurora-drift {
+ from {
+ transform: translate3d(0, 0, 0) scale(1);
+ opacity: 0.55;
+ }
+ to {
+ transform: translate3d(30px, -24px, 0) scale(1.15);
+ opacity: 0.85;
+ }
+}
+
+/* Small bobbing scroll cue at the bottom of the hero. */
+.hero-cue {
+ animation: float 2.4s ease-in-out infinite;
+ will-change: transform;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .aurora-blob,
+ .hero-cue {
+ animation: none;
+ }
+ .btn-brand:hover,
+ .btn-glass-dark:hover,
+ .explore-pill:hover,
+ .avatar-tile:hover {
+ transform: none;
+ }
+}
+
+/* Dividers between the four counters in the stats panel. */
+.stat-cell {
+ position: relative;
+}
+@media (min-width: 1024px) {
+ .stat-cell:not(:first-child)::before {
+ content: "";
+ position: absolute;
+ left: -0.5rem;
+ top: 18%;
+ height: 64%;
+ width: 1px;
+ background: color-mix(in srgb, var(--color-text-muted) 20%, transparent);
+ }
+}
+@media (max-width: 1023px) {
+ .stat-cell:nth-child(3)::before {
+ content: "";
+ position: absolute;
+ left: -0.5rem;
+ top: 10%;
+ height: 80%;
+ width: 1px;
+ background: color-mix(in srgb, var(--color-text-muted) 20%, transparent);
+ }
+}
diff --git a/src/components/auth/auth-top-bar.tsx b/src/components/auth/auth-top-bar.tsx
index 6ab9fcb9..5ef15c12 100644
--- a/src/components/auth/auth-top-bar.tsx
+++ b/src/components/auth/auth-top-bar.tsx
@@ -23,14 +23,14 @@ export function AuthTopBar({
return (
) : (
-
+
+
+
)}
{showHotelName ? (
{hotelName}
@@ -66,10 +77,10 @@ export function AuthTopBar({
) : null}
diff --git a/src/components/auth/login-form.tsx b/src/components/auth/login-form.tsx
index 41b6aa8b..28bb3d61 100644
--- a/src/components/auth/login-form.tsx
+++ b/src/components/auth/login-form.tsx
@@ -1,5 +1,6 @@
"use client";
+import { KeyRound, Lock, UserRound } from "lucide-react";
import { signIn } from "next-auth/react";
import { useTranslations } from "next-intl";
import { type FormEvent, useId, useState } from "react";
@@ -9,6 +10,7 @@ import {
CaptchaWidget,
readCaptchaToken,
} from "@/components/auth/captcha-widget";
+import { PasswordToggle } from "@/components/auth/password-toggle";
import Link from "@/components/link";
import { signInWithTransientRetry } from "@/lib/auth/sign-in-retry";
@@ -49,7 +51,6 @@ export function LoginForm({
const [pending, setPending] = useState(false);
const showFooter = variant === "page";
- const labelled = variant === "compact";
const lockCredentials = needs2fa ? "opacity-50 pointer-events-none" : "";
async function onSubmit(e: FormEvent
) {
@@ -103,8 +104,14 @@ export function LoginForm({
<>
{needs2fa && (
{t("subtitle2fa")}
@@ -115,113 +122,89 @@ export function LoginForm({
aria-label={t("signIn")}
className="relative flex flex-col gap-4"
>
-
-
-
+
{t("password")}
+
setPassword(e.target.value)}
- className="input-glow w-full rounded-xl border-2 px-4 py-3 text-sm font-medium transition-all focus:outline-none"
- style={{
- backgroundColor: "var(--color-background)",
- color: "var(--color-text-readable)",
- borderColor:
- "color-mix(in srgb, var(--color-text-muted) 25%, transparent)",
- }}
+ className="auth-input has-icon has-action"
required
/>
-
+
setShowPassword((v) => !v)}
+ labelShow={t("showPassword")}
+ labelHide={t("hidePassword")}
+ />
{password.length > 0 && (
-
-
+
+
{[1, 2, 3, 4, 5].map((i) => (
))}
{strength.label}
@@ -388,38 +323,30 @@ export function RegisterForm({
)}
-
+
{t("confirmPassword")}
+
-
+
setShowConfirm((v) => !v)}
+ labelShow={t("showPassword")}
+ labelHide={t("hidePassword")}
+ />
@@ -556,15 +483,13 @@ export function RegisterForm({
});
}
}}
- className={`btn-shine w-full rounded-xl font-extrabold text-sm py-3.5 transition-all duration-200 hover:scale-[1.02] active:scale-95 shadow-lg disabled:opacity-60 ${
- termsAccepted ? "" : "opacity-70 saturate-[0.85] hover:scale-100"
+ className={`btn-brand btn-shine w-full py-3.5 text-sm font-extrabold uppercase tracking-wider disabled:opacity-60 ${
+ termsAccepted ? "" : "opacity-70 saturate-[0.85]"
}`}
style={{
- background: "var(--color-primary)",
- color: "var(--color-primary-foreground)",
boxShadow: termsAccepted
- ? "0 4px 24px color-mix(in srgb, var(--color-primary) 35%, transparent)"
- : "0 2px 10px color-mix(in srgb, var(--color-primary) 12%, transparent)",
+ ? undefined
+ : "0 4px 14px -8px color-mix(in srgb, var(--color-primary) 50%, transparent)",
}}
>
{isPending || signingIn ? (
@@ -597,14 +522,25 @@ export function RegisterForm({
)}
-
+
+
{t("alreadyHaveAccount")}