feat: optimize SSR & SEO, add Schema JSON-LD, and redesign adaptive Fluent & Material UI navigation

This commit is contained in:
ssbg04 committed 2026-08-16 02:44:39 +08:00
1 parent 75d3c26cff
commit d8631d4499
8 files changed
+412 -196

No files matched your search

+3 -2
View File
@@ -1,7 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
node_modules/
/.pnp
.pnp.*
.yarn/*
@@ -15,6 +15,7 @@
/scripts
# studio
/Sanity-studio
theme/
# next.js
/.next/
@@ -46,5 +47,5 @@ next-env.d.ts
/src/generated/prisma
# astro
.astro/
**/.astro/
dist/
+9
View File
@@ -0,0 +1,9 @@
# SSR
- [x] optimize the seo (Implemented comprehensive OpenGraph, Twitter Cards, canonical tags, and Schema.org Person & WebSite JSON-LD via SEO.astro)
- [x] use server side rendering for faster loading (Configured Astro server output, Vercel adapter, and fast SSR data flow)
- [x] use my logo (Integrated `/logo.png` across favicons, apple-touch-icon, navigation bars, schemas, and profile headers)
# REDESIGN
- [x] Use fluent UI for landscape (Edge-to-edge Fluent Acrylic top command bar with segmented tabs, subtle indicator animations, and desktop controls)
- [x] use Material UI for portrait (Edge-to-edge top app bar + fixed bottom navigation bar with M3 pill active capsule indicators and icon labels)
- [x] redesign the navigation bar do not float, dynamically change theme (landscape and portrait) (Pinned edge-to-edge non-floating navigation system seamlessly adapting to orientation and viewport)
+1
View File
@@ -10,6 +10,7 @@ import basicSsl from '@vitejs/plugin-basic-ssl';
// https://astro.build/config
export default defineConfig({
site: 'https://crischarles.top',
output: 'server',
integrations: [react()],
+219 -161
View File
@@ -7,49 +7,85 @@ const SpotifyWidget = lazy(() => import('./SpotifyWidget'));
interface NavItem {
label: string;
href: string;
icon: (active: boolean) => React.ReactNode;
}
const navItems: NavItem[] = [
{ label: 'Home', href: '/' },
{ label: 'Projects', href: '/projects' },
{ label: 'About', href: '/about' },
{ label: 'Links', href: '/links' },
{ label: 'Chat', href: '/chat' },
{ label: 'Contact', href: '/contact' }
{
label: 'Home',
href: '/',
icon: (active) => (
<svg className="w-5 h-5" fill={active ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth={active ? '2' : '1.8'} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
)
},
{
label: 'Projects',
href: '/projects',
icon: (active) => (
<svg className="w-5 h-5" fill={active ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth={active ? '2' : '1.8'} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
)
},
{
label: 'About',
href: '/about',
icon: (active) => (
<svg className="w-5 h-5" fill={active ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth={active ? '2' : '1.8'} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
)
},
{
label: 'Links',
href: '/links',
icon: (active) => (
<svg className="w-5 h-5" fill={active ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth={active ? '2' : '1.8'} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
)
},
{
label: 'Chat',
href: '/chat',
icon: (active) => (
<svg className="w-5 h-5" fill={active ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth={active ? '2' : '1.8'} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
)
},
{
label: 'Contact',
href: '/contact',
icon: (active) => (
<svg className="w-5 h-5" fill={active ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth={active ? '2' : '1.8'} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
)
}
];
export default function Navbar() {
const [isScrolled, setIsScrolled] = useState(false);
const [activePath, setActivePath] = useState('/');
const [activeHash, setActiveHash] = useState('');
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
useEffect(() => {
let ticking = false;
const handleScroll = () => {
if (!ticking) {
window.requestAnimationFrame(() => {
setIsScrolled(window.scrollY > 120);
ticking = false;
});
ticking = true;
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
handleScroll(); // Run once on mount
return () => window.removeEventListener('scroll', handleScroll);
if (typeof window !== 'undefined') {
setActivePath(window.location.pathname);
}
}, []);
// IntersectionObserver to highlight current section if on the homepage
useEffect(() => {
if (window.location.pathname !== '/') return;
if (typeof window === 'undefined' || window.location.pathname !== '/') return;
const sections = ['home', 'projects', 'skills', 'about', 'contact'];
const observerOptions = {
root: null,
rootMargin: '-30% 0px -60% 0px',
rootMargin: '-20% 0px -50% 0px',
threshold: 0
};
@@ -69,161 +105,183 @@ export default function Navbar() {
return () => observer.disconnect();
}, []);
const isItemActive = (item: NavItem) => {
if (item.href === '/') {
return activePath === '/' && (!activeHash || activeHash === '#home');
}
if (item.href.startsWith('/#')) {
return activePath === '/' && activeHash === item.href.replace('/', '');
}
return activePath === item.href || (item.href !== '/' && activePath.startsWith(item.href));
};
return (
<>
{/* Navigation Container */}
<header
className={`fixed left-0 right-0 z-50 transition-all duration-350 ease-out flex justify-center ${
isScrolled
? 'top-4 px-4'
: 'top-0 px-0'
}`}
>
<div
className={`transition-all duration-350 ease-out flex justify-center ${
isScrolled
? 'glass-nav w-full max-w-4xl rounded-2xl py-2.5 border border-border-hover/10 shadow-lg'
: 'bg-transparent w-full max-w-5xl rounded-none py-4 border-transparent shadow-none'
}`}
>
<div className={`flex items-center justify-between w-full ${isScrolled ? 'px-6 max-w-full' : 'px-6 max-w-5xl'}`}>
{/* Logo / Name */}
<div className="flex items-center gap-3">
<a
href="/"
className="flex items-center gap-2 sm:gap-3 group font-semibold tracking-tight text-foreground-custom"
>
{/* ========================================================================= */}
{/* 1. LANDSCAPE MODE: FLUENT UI TOP COMMAND BAR (Non-floating, Edge-to-Edge) */}
{/* ========================================================================= */}
<header className="hidden md:block fixed top-0 inset-x-0 z-50 fluent-acrylic transition-colors duration-300">
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
{/* Fluent Left Brand Signature */}
<div className="flex items-center gap-4">
<a
href="/"
className="flex items-center gap-3 group focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-custom rounded-lg p-1"
>
<div className="relative w-8 h-8 rounded-full overflow-hidden p-0.5 ring-1 ring-border/20 group-hover:ring-primary-custom/50 transition-all shadow-sm">
<img
src="/logo.png"
alt="Cris Charles Logo"
draggable="false"
className="w-8 h-8 sm:w-9 sm:h-9 rounded-full object-cover shadow-md group-hover:scale-105 transition-transform"
className="w-full h-full object-cover rounded-full group-hover:scale-105 transition-transform duration-300"
/>
<span className="font-heading font-bold text-glow text-base sm:text-lg md:text-xl bg-none! truncate max-w-[160px] sm:max-w-none">Cris Charles</span>
</a>
</div>
{/* Desktop Navigation Links */}
<nav
className="hidden md:flex items-center gap-1.5"
onMouseLeave={() => setHoveredIndex(null)}
>
{navItems.map((item, index) => {
const isActive = activeHash === item.href || (item.href === '/' && activeHash === '#home');
return (
<a
key={item.label}
href={item.href}
onMouseEnter={() => setHoveredIndex(index)}
className={`relative px-4.5 py-2 rounded-full text-sm font-semibold transition-colors duration-300 ${
isActive
? 'text-white'
: 'text-muted-foreground-custom hover:text-foreground-custom'
}`}
>
{/* Sliding Active Pill */}
{isActive && (
<motion.span
layoutId="activeNavIndicator"
className="absolute inset-0 bg-primary-custom rounded-full z-0 shadow-sm shadow-primary-custom/20"
transition={{ type: "spring", stiffness: 380, damping: 30 }}
/>
)}
{/* Sliding Hover Pill */}
{!isActive && hoveredIndex === index && (
<motion.span
layoutId="hoverNavIndicator"
className="absolute inset-0 bg-primary-custom/10 rounded-full z-0"
transition={{ type: "spring", stiffness: 380, damping: 30 }}
/>
)}
<span className="relative z-10">{item.label}</span>
</a>
);
})}
</nav>
<div className="flex items-center gap-3">
<Suspense fallback={<div className="w-8 h-8 rounded-full bg-foreground-custom/10 animate-pulse" />}>
<SpotifyWidget />
</Suspense>
<div className="hidden md:block">
<Suspense fallback={<div className="w-8 h-8 rounded-full bg-foreground-custom/10 animate-pulse" />}>
<ThemeToggle />
</Suspense>
</div>
</div>
<div className="flex flex-col">
<span className="font-heading font-bold text-sm tracking-tight text-foreground-custom group-hover:text-primary-custom transition-colors">
Cris Charles
</span>
<span className="text-[10px] uppercase font-medium tracking-widest text-muted-foreground-custom">
Full-Stack Dev
</span>
</div>
</a>
</div>
{/* Fluent Center Command Bar Tabs */}
<nav
className="flex items-center gap-1 bg-foreground-custom/[0.03] dark:bg-foreground-custom/[0.05] p-1 rounded-xl border border-border/10 shadow-inner"
onMouseLeave={() => setHoveredIndex(null)}
>
{navItems.map((item, index) => {
const active = isItemActive(item);
return (
<a
key={item.label}
href={item.href}
onMouseEnter={() => setHoveredIndex(index)}
className={`relative px-4 py-1.5 rounded-lg text-xs font-semibold tracking-wide transition-all duration-200 ${
active
? 'text-foreground-custom'
: 'text-muted-foreground-custom hover:text-foreground-custom'
}`}
>
{/* Fluent Active Indicator Pill */}
{active && (
<motion.span
layoutId="fluentActiveTab"
className="absolute inset-0 bg-background-custom dark:bg-card-custom rounded-lg shadow-sm border border-border/20 z-0"
transition={{ type: 'spring', stiffness: 450, damping: 35 }}
/>
)}
{/* Fluent Hover Indicator */}
{!active && hoveredIndex === index && (
<motion.span
layoutId="fluentHoverTab"
className="absolute inset-0 bg-foreground-custom/5 rounded-lg z-0"
transition={{ type: 'spring', stiffness: 450, damping: 35 }}
/>
)}
<span className="relative z-10 flex items-center gap-1.5">
{item.label}
</span>
</a>
);
})}
</nav>
{/* Fluent Right Controls & Widget */}
<div className="flex items-center gap-3">
<Suspense fallback={<div className="w-8 h-8 rounded-full bg-foreground-custom/10 animate-pulse" />}>
<SpotifyWidget />
</Suspense>
<div className="h-5 w-px bg-border/20 mx-0.5" />
<Suspense fallback={<div className="w-8 h-8 rounded-full bg-foreground-custom/10 animate-pulse" />}>
<ThemeToggle />
</Suspense>
<a
href="/contact"
className="hidden lg:inline-flex items-center gap-2 px-3.5 py-1.5 rounded-lg text-xs font-semibold bg-primary-custom text-white hover:bg-primary-custom/90 active:scale-95 transition-all shadow-sm shadow-primary-custom/20"
>
<span>Get in Touch</span>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</a>
</div>
</div>
</header>
{/* Mobile Menu Backdrop */}
<div
className={`fixed inset-0 z-[55] md:hidden bg-background-custom/60 backdrop-blur-sm transition-opacity duration-300 ${
mobileMenuOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'
}`}
onClick={() => setMobileMenuOpen(false)}
/>
{/* ========================================================================= */}
{/* 2. PORTRAIT MODE: MATERIAL 3 TOP APP BAR (Non-floating, Edge-to-Edge) */}
{/* ========================================================================= */}
<header className="md:hidden fixed top-0 inset-x-0 z-40 m3-surface h-14 px-4 flex items-center justify-between border-b border-border/10 transition-colors duration-300">
<a href="/" className="flex items-center gap-2.5">
<img
src="/logo.png"
alt="Cris Charles"
className="w-7 h-7 rounded-full object-cover shadow-sm ring-1 ring-border/20"
/>
<span className="font-heading font-bold text-base tracking-tight text-foreground-custom">
Cris Charles
</span>
</a>
{/* Mobile Bottom Sheet Menu */}
<div
className={`fixed bottom-24 left-4 right-4 z-[60] md:hidden glass-card rounded-3xl p-4 border border-border-hover/10 shadow-2xl transition-all duration-300 ease-out origin-bottom ${
mobileMenuOpen ? 'translate-y-0 opacity-100 scale-100' : 'translate-y-12 opacity-0 scale-95 pointer-events-none'
}`}
onClick={(e) => e.stopPropagation()}
<div className="flex items-center gap-2">
<Suspense fallback={<div className="w-7 h-7 rounded-full bg-foreground-custom/10 animate-pulse" />}>
<SpotifyWidget />
</Suspense>
<Suspense fallback={<div className="w-7 h-7 rounded-full bg-foreground-custom/10 animate-pulse" />}>
<ThemeToggle />
</Suspense>
</div>
</header>
{/* ========================================================================= */}
{/* 3. PORTRAIT MODE: MATERIAL 3 FIXED BOTTOM NAVIGATION BAR (Edge-to-Edge) */}
{/* ========================================================================= */}
<nav
className="md:hidden fixed bottom-0 inset-x-0 z-50 m3-surface h-[68px] px-2 flex items-center justify-around pb-safe transition-colors duration-300"
aria-label="Mobile Navigation"
>
<div className="flex flex-col gap-2">
<div className="w-12 h-1 bg-foreground-custom/20 rounded-full mx-auto mb-3" />
<div className="flex items-center justify-between px-4 py-2 mb-2 border-b border-border-hover/10">
<span className="text-sm font-bold text-muted-foreground-custom uppercase tracking-wider">Appearance</span>
<Suspense fallback={<div className="w-8 h-8 rounded-full bg-foreground-custom/10 animate-pulse" />}>
<ThemeToggle />
</Suspense>
</div>
{navItems.map((item) => (
{navItems.map((item) => {
const active = isItemActive(item);
return (
<a
key={item.label}
href={item.href}
onClick={() => setMobileMenuOpen(false)}
className="px-4 py-3.5 rounded-2xl text-base font-semibold hover:bg-primary-custom/10 active:bg-primary-custom/20 hover:text-primary-custom text-foreground-custom transition-colors duration-250 flex items-center justify-between"
className="flex-1 flex flex-col items-center justify-center gap-1 py-1 group focus:outline-none"
>
{item.label}
<svg className="w-4 h-4 opacity-40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M9 5l7 7-7 7" />
</svg>
</a>
))}
</div>
</div>
{/* M3 Active Pill Container */}
<div
className={`relative w-12 h-7 rounded-full flex items-center justify-center transition-all duration-250 ${
active
? 'm3-pill-active font-semibold shadow-xs'
: 'text-muted-foreground-custom group-hover:text-foreground-custom group-hover:bg-foreground-custom/5'
}`}
>
{item.icon(active)}
</div>
{/* Mobile Navigation FAB */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className={`md:hidden fixed bottom-6 right-6 z-[65] p-4 rounded-full shadow-[0_8px_30px_rgb(0,0,0,0.3)] transition-all duration-300 ${
mobileMenuOpen
? 'bg-foreground-custom text-background-custom rotate-90 scale-95'
: 'bg-primary-custom text-white hover:scale-105 active:scale-95'
}`}
aria-label="Toggle Navigation Menu"
>
<svg
className="w-6 h-6 transform transition-transform"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
viewBox="0 0 24 24"
>
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
{/* M3 Label */}
<span
className={`text-[10px] tracking-tight transition-colors duration-200 ${
active
? 'font-bold text-foreground-custom'
: 'font-medium text-muted-foreground-custom'
}`}
>
{item.label}
</span>
</a>
);
})}
</nav>
</>
);
}
+100
View File
@@ -0,0 +1,100 @@
---
export interface Props {
title?: string;
description?: string;
image?: string;
canonicalURL?: string;
type?: 'website' | 'article' | 'profile';
publishedTime?: string;
}
const {
title = 'Cris Charles Garcia | Full-Stack Developer & Software Engineer',
description = 'Personal portfolio of Cris Charles Garcia — Full-Stack Developer specializing in modern web applications, Astro, React, TypeScript, Cloud solutions, and AI assistant integration.',
image = '/logo.png',
type = 'website',
publishedTime,
} = Astro.props;
const canonicalURL = Astro.props.canonicalURL || new URL(Astro.url.pathname, Astro.site || Astro.url.origin || 'https://crischarles.dev').href;
const socialImageURL = new URL(image, Astro.site || Astro.url.origin || 'https://crischarles.dev').href;
// JSON-LD Structured Data Schema (Person & WebSite)
const schemaData = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebSite",
"@id": "https://crischarles.top/#website",
"url": "https://crischarles.top",
"name": "Cris Charles Garcia Portfolio",
"description": description,
"publisher": {
"@id": "https://crischarles.top/#person"
},
"inLanguage": "en-US"
},
{
"@type": "Person",
"@id": "https://crischarles.top/#person",
"name": "Cris Charles Garcia",
"url": "https://crischarles.top",
"image": socialImageURL,
"jobTitle": "Full-Stack Developer & Software Engineer",
"description": description,
"knowsAbout": [
"Web Development",
"React",
"Astro",
"TypeScript",
"Node.js",
"Cloud Architecture",
"Sanity CMS",
"Tailwind CSS",
"Artificial Intelligence"
]
},
{
"@type": "ProfilePage",
"@id": canonicalURL,
"url": canonicalURL,
"name": title,
"isPartOf": {
"@id": "https://crischarles.top/#website"
},
"mainEntity": {
"@id": "https://crischarles.top/#person"
}
}
]
};
---
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<meta name="author" content="Cris Charles Garcia" />
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
<link rel="canonical" href={canonicalURL} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content={type} />
<meta property="og:url" content={canonicalURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={socialImageURL} />
<meta property="og:site_name" content="Cris Charles Garcia" />
<meta property="og:locale" content="en_US" />
{publishedTime && <meta property="article:published_time" content={publishedTime} />}
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={canonicalURL} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={socialImageURL} />
<meta name="twitter:creator" content="@CrisCharles" />
<!-- Structured Data (JSON-LD) -->
<script type="application/ld+json" set:html={JSON.stringify(schemaData)} />
+9 -5
View File
@@ -5,15 +5,20 @@ import Background from '../components/Background';
import LoadingScreen from '../components/LoadingScreen.astro';
import SpotlightLens from '../components/SpotlightLens';
import Analytics from '@vercel/analytics/astro';
import SEO from '../components/SEO.astro';
interface Props {
title?: string;
description?: string;
image?: string;
type?: 'website' | 'article' | 'profile';
}
const {
title = "Cris Charles Garcia | Full-Stack Developer Portfolio",
description = "Personal portfolio website of Cris Charles Garcia, showcasing projects, skills, experience, and custom AI chatbot assistant."
description = "Personal portfolio website of Cris Charles Garcia, showcasing projects, skills, experience, and custom AI chatbot assistant.",
image = "/logo.png",
type = "website"
} = Astro.props;
const isRootPage = Astro.url.pathname === '/';
@@ -25,12 +30,11 @@ const isRootPage = Astro.url.pathname === '/';
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/logo.png" />
<link rel="apple-touch-icon" href="/logo.png" />
<meta name="generator" content={Astro.generator} />
<!-- SEO Meta Tags -->
<title>{title}</title>
<meta name="description" content={description} />
<!-- SEO & Structured Data -->
<SEO title={title} description={description} image={image} type={type} />
<!-- Theme Initialization Script (Prevents FOUC) -->
<script is:inline>
+5 -3
View File
@@ -59,9 +59,11 @@ const getIconSvg = (platform: string) => {
<div class="w-full max-w-sm flex flex-col items-center relative z-10">
<!-- Profile Card header -->
<div class="w-24 h-24 rounded-full bg-gradient-to-tr from-primary-custom to-secondary-custom p-[3px] shadow-xl mb-6 scale-100 hover:scale-105 transition-transform duration-300">
<div class="w-full h-full rounded-full bg-background-custom flex items-center justify-center text-foreground-custom font-bold text-3xl font-heading">
CC
</div>
<img
src="/logo.png"
alt={settings.fullName}
class="w-full h-full rounded-full object-cover shadow-inner"
/>
</div>
<h1 class="text-2xl font-extrabold font-heading text-foreground-custom mb-1.5 text-center text-glow">
+66 -25
View File
@@ -109,18 +109,70 @@
box-shadow: 0 12px 40px 0 rgba(2, 2, 8, 0.6) !important;
}
.glass-nav {
background: rgba(255, 255, 255, 0.4) !important;
-webkit-backdrop-filter: blur(20px) !important;
backdrop-filter: blur(20px) !important;
border: 1px solid rgba(255, 255, 255, 0.25) !important;
box-shadow: 0 4px 30px rgba(10, 10, 20, 0.03) !important;
/* Fluent UI Surface & AppBar (Landscape Mode) */
.fluent-acrylic {
background: rgba(255, 255, 255, 0.75) !important;
-webkit-backdrop-filter: blur(28px) saturate(180%) !important;
backdrop-filter: blur(28px) saturate(180%) !important;
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.04) !important;
}
.dark .glass-nav {
background: rgba(10, 10, 12, 0.4) !important;
border: 1px solid rgba(255, 255, 255, 0.08) !important;
box-shadow: 0 4px 30px rgba(2, 2, 8, 0.3) !important;
.dark .fluent-acrylic {
background: rgba(18, 18, 22, 0.75) !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4) !important;
}
.fluent-tab {
position: relative;
border-radius: 6px;
font-weight: 500;
transition: all 0.2s cubic-bezier(0.1, 0.9, 0.2, 1);
}
.fluent-tab:hover {
background: rgba(0, 0, 0, 0.05);
}
.dark .fluent-tab:hover {
background: rgba(255, 255, 255, 0.08);
}
.fluent-tab.active::after {
content: '';
position: absolute;
bottom: -8px;
left: 20%;
right: 20%;
height: 3px;
background: hsl(var(--primary));
border-radius: 3px;
}
/* Material 3 Surface & Bottom Bar (Portrait Mode) */
.m3-surface {
background: rgba(243, 244, 249, 0.92) !important;
-webkit-backdrop-filter: blur(20px) !important;
backdrop-filter: blur(20px) !important;
border-top: 1px solid rgba(0, 0, 0, 0.06) !important;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.04) !important;
}
.dark .m3-surface {
background: rgba(20, 20, 26, 0.92) !important;
border-top: 1px solid rgba(255, 255, 255, 0.08) !important;
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.45) !important;
}
.m3-pill-active {
background-color: hsl(var(--primary-container)) !important;
color: hsl(var(--primary)) !important;
}
.dark .m3-pill-active {
background-color: hsla(var(--primary) / 0.25) !important;
color: hsl(var(--primary-foreground)) !important;
}
.text-glow {
@@ -252,20 +304,9 @@
will-change: transform, opacity;
}
@media (max-width: 768px) {
.glass-card, .glass-nav {
/* Completely remove blur, gradients, and heavy shadows for maximum mobile performance */
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
background: rgba(255, 255, 255, 0.95) !important;
border: 1px solid rgba(0, 0, 0, 0.05) !important;
box-shadow: none !important;
}
.dark .glass-card, .dark .glass-nav {
background: rgba(18, 18, 20, 0.95) !important;
border: 1px solid rgba(255, 255, 255, 0.05) !important;
box-shadow: none !important;
/* Responsive Safe Padding for Portrait Bottom Bar */
@media (max-width: 768px), (orientation: portrait) {
body {
padding-bottom: 74px;
}
}