feat(nav): smooth page transitions, icon-free navigation, cross-page ultra-fast persistence, and optimized glassmorphism
This commit is contained in:
1 parent
164e3a6bbc
commit
29017e84ba
18 files changed
+410
-177
No files matched your search
@@ -68,5 +68,5 @@ Project Dedicated page
|
||||
- fix the gallery page modal view the description is not readable
|
||||
- in the certificates if the picture is portrait and it has many information the picture is not visible, make the modal scrollable and responsive -->
|
||||
|
||||
- fix that on mobile view in all of the pages in navigation it has huge space at top, keep the home page layout
|
||||
- fix that in landscape or desktop view that the active color indicator jumps to home to where you click a tab, and fix that the navigation bar zooming out in landscape view
|
||||
<!-- - fix that on mobile view in all of the pages in navigation it has huge space at top, keep the home page layout
|
||||
- fix that in landscape or desktop view that the active color indicator jumps to home to where you click a tab, and fix that the navigation bar zooming out in landscape view -->
|
||||
@@ -107,11 +107,11 @@ export default function About({
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="about" className="py-12 sm:py-16 relative">
|
||||
<section id="about" className="pt-3 sm:pt-12 pb-12 sm:pb-16 relative">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 relative z-10">
|
||||
|
||||
{/* Section Header */}
|
||||
<ScrollReveal variant="fade-up" className="mb-10">
|
||||
<ScrollReveal variant="fade-up" className="mb-5 sm:mb-10">
|
||||
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 pb-4 border-b border-border-custom">
|
||||
<div>
|
||||
<h2 className="text-3xl sm:text-4xl font-bold font-heading text-foreground-custom tracking-tight">
|
||||
|
||||
@@ -283,6 +283,8 @@ export default function Background() {
|
||||
};
|
||||
checkTier();
|
||||
window.addEventListener('tier-change', checkTier);
|
||||
document.addEventListener('astro:after-swap', checkTier);
|
||||
document.addEventListener('astro:page-load', checkTier);
|
||||
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
setReducedMotion(true);
|
||||
@@ -290,6 +292,8 @@ export default function Background() {
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('tier-change', checkTier);
|
||||
document.removeEventListener('astro:after-swap', checkTier);
|
||||
document.removeEventListener('astro:page-load', checkTier);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -809,6 +813,7 @@ export default function Background() {
|
||||
animationFrameId = requestAnimationFrame(renderLoop);
|
||||
|
||||
if (!isVisible) return;
|
||||
if (document.documentElement.dataset.tier === 'low' || localStorage.getItem('liteMode') === 'true') return;
|
||||
|
||||
const tSec = now * 0.001;
|
||||
|
||||
|
||||
@@ -316,11 +316,11 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
const hasBothTypes = documentCerts.length > 0 && skillBadges.length > 0;
|
||||
|
||||
return (
|
||||
<section id="certifications" className="pt-6 sm:pt-10 pb-16 sm:pb-20 relative">
|
||||
<section id="certifications" className="pt-3 sm:pt-10 pb-16 sm:pb-20 relative">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 relative z-10">
|
||||
|
||||
{/* Section Header */}
|
||||
<ScrollReveal variant="fade-up" className="mb-8">
|
||||
<ScrollReveal variant="fade-up" className="mb-4 sm:mb-8">
|
||||
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4 pb-4 border-b border-border-custom">
|
||||
<div>
|
||||
<h2 className="text-3xl sm:text-4xl font-bold font-heading text-foreground-custom tracking-tight">
|
||||
@@ -332,7 +332,7 @@ export default function Certifications({ certifications, extraCerts = [], sectio
|
||||
|
||||
{/* ─── Category Filter Tabs (Shown when both document certificates and skill badges exist) ─── */}
|
||||
{hasBothTypes && (
|
||||
<ScrollReveal variant="fade-up" delay={50} className="mb-8">
|
||||
<ScrollReveal variant="fade-up" delay={50} className="mb-5 sm:mb-8">
|
||||
<div className="flex flex-wrap items-center gap-2 p-1.5 rounded-2xl bg-foreground-custom/[0.03] border border-border-custom w-fit">
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@@ -175,18 +175,18 @@ export default function ContactForm({
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="contact" className="py-24 relative overflow-hidden">
|
||||
<div className="container mx-auto px-6 max-w-6xl relative z-10">
|
||||
<section id="contact" className="pt-2 sm:pt-12 md:pt-16 pb-16 sm:pb-24 relative overflow-hidden">
|
||||
<div className="container mx-auto px-4 sm:px-6 max-w-6xl relative z-10">
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-12 lg:gap-8 items-start">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8 lg:gap-8 items-start">
|
||||
|
||||
{/* Left Side: Info */}
|
||||
<div className="lg:col-span-2 flex flex-col justify-center h-full">
|
||||
<ScrollReveal variant="fade-right">
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-black font-heading mb-5 leading-tight tracking-tight text-foreground-custom block">
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-black font-heading mb-3 sm:mb-5 leading-tight tracking-tight text-foreground-custom block">
|
||||
{contactHeading}
|
||||
</h2>
|
||||
<p className="text-muted-foreground-custom text-base md:text-lg mb-8 leading-relaxed">
|
||||
<p className="text-muted-foreground-custom text-base md:text-lg mb-5 sm:mb-8 leading-relaxed">
|
||||
{contactSubtitle}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -23,28 +23,32 @@ export default function ControlsDropdown() {
|
||||
|
||||
setIsTouchDevice(window.matchMedia('(hover: none) and (pointer: coarse)').matches);
|
||||
|
||||
// Detect theme preferences: System default or manual stored
|
||||
// Detect theme preferences: Dark default or manual stored
|
||||
const stored = localStorage.getItem('theme') as ThemeMode | null;
|
||||
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
if (stored === 'light' || stored === 'dark') {
|
||||
setThemeMode(stored);
|
||||
setResolvedTheme(stored);
|
||||
if (stored === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
} else {
|
||||
// Default: auto-detect system theme!
|
||||
if (stored === 'light') {
|
||||
setThemeMode('light');
|
||||
setResolvedTheme('light');
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', '#fafafa');
|
||||
} else if (stored === 'system') {
|
||||
setThemeMode('system');
|
||||
const detectedTheme = systemDark ? 'dark' : 'light';
|
||||
setResolvedTheme(detectedTheme);
|
||||
if (systemDark) {
|
||||
if (detectedTheme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', '#090a0f');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', '#fafafa');
|
||||
}
|
||||
} else {
|
||||
// Default: dark mode signature theme
|
||||
setThemeMode('dark');
|
||||
setResolvedTheme('dark');
|
||||
document.documentElement.classList.add('dark');
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', '#090a0f');
|
||||
}
|
||||
|
||||
const checkLite = () => {
|
||||
@@ -52,6 +56,8 @@ export default function ControlsDropdown() {
|
||||
};
|
||||
checkLite();
|
||||
window.addEventListener('tier-change', checkLite);
|
||||
document.addEventListener('astro:after-swap', checkLite);
|
||||
document.addEventListener('astro:page-load', checkLite);
|
||||
|
||||
const savedMag = localStorage.getItem('magnifierEnabled');
|
||||
setMagnifierEnabled(savedMag === 'true');
|
||||
@@ -93,6 +99,8 @@ export default function ControlsDropdown() {
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('tier-change', checkLite);
|
||||
document.removeEventListener('astro:after-swap', checkLite);
|
||||
document.removeEventListener('astro:page-load', checkLite);
|
||||
if (darkQuery.removeEventListener) {
|
||||
darkQuery.removeEventListener('change', handleSystemThemeChange);
|
||||
}
|
||||
@@ -126,24 +134,26 @@ export default function ControlsDropdown() {
|
||||
haptic.tap();
|
||||
setThemeMode(mode);
|
||||
|
||||
let resolved: 'light' | 'dark' = 'dark';
|
||||
if (mode === 'system') {
|
||||
localStorage.removeItem('theme');
|
||||
localStorage.setItem('theme', 'system');
|
||||
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
setResolvedTheme(systemDark ? 'dark' : 'light');
|
||||
if (systemDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
resolved = systemDark ? 'dark' : 'light';
|
||||
} else {
|
||||
localStorage.setItem('theme', mode);
|
||||
setResolvedTheme(mode);
|
||||
if (mode === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
resolved = mode;
|
||||
}
|
||||
|
||||
setResolvedTheme(resolved);
|
||||
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
|
||||
if (resolved === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
metaThemeColor?.setAttribute('content', '#090a0f');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
metaThemeColor?.setAttribute('content', '#fafafa');
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('theme-change', { detail: { theme: resolved } }));
|
||||
};
|
||||
|
||||
const toggleMagnifier = () => {
|
||||
|
||||
+96
-45
@@ -4,28 +4,55 @@ import haptic from '../lib/haptics';
|
||||
const ControlsDropdown = lazy(() => import('./ControlsDropdown'));
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Home', href: '/', icon: 'fa-solid fa-house' },
|
||||
{ label: 'Projects', href: '/projects', icon: 'fa-solid fa-diagram-project' },
|
||||
{ label: 'About', href: '/about', icon: 'fa-solid fa-user' },
|
||||
{ label: 'Certifications', href: '/certifications', icon: 'fa-solid fa-award' },
|
||||
{ label: 'Gallery', href: '/gallery', icon: 'fa-solid fa-images' },
|
||||
{ label: 'Contact', href: '/contact', icon: 'fa-solid fa-envelope' },
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'Projects', href: '/projects' },
|
||||
{ label: 'About', href: '/about' },
|
||||
{ label: 'Certifications', href: '/certifications' },
|
||||
{ label: 'Gallery', href: '/gallery' },
|
||||
{ label: 'Contact', href: '/contact' },
|
||||
];
|
||||
|
||||
interface NavbarProps {
|
||||
fullName?: string;
|
||||
logoImage?: string;
|
||||
currentPath?: string;
|
||||
}
|
||||
|
||||
export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.png' }: NavbarProps) {
|
||||
const [activePath, setActivePath] = useState('/');
|
||||
export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.png', currentPath }: NavbarProps) {
|
||||
const [activePath, setActivePath] = useState<string>(() => {
|
||||
if (currentPath) return currentPath;
|
||||
if (typeof window !== 'undefined') return window.location.pathname;
|
||||
return '/';
|
||||
});
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (currentPath) {
|
||||
setActivePath(currentPath);
|
||||
} else if (typeof window !== 'undefined') {
|
||||
setActivePath(window.location.pathname);
|
||||
}
|
||||
}, [currentPath]);
|
||||
|
||||
// Sync route and close drawer on Astro page transitions & browser navigation
|
||||
useEffect(() => {
|
||||
const handleRoute = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
setActivePath(window.location.pathname);
|
||||
}
|
||||
setDrawerOpen(false);
|
||||
};
|
||||
|
||||
document.addEventListener('astro:page-load', handleRoute);
|
||||
document.addEventListener('astro:after-swap', handleRoute);
|
||||
window.addEventListener('popstate', handleRoute);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('astro:page-load', handleRoute);
|
||||
document.removeEventListener('astro:after-swap', handleRoute);
|
||||
window.removeEventListener('popstate', handleRoute);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -34,7 +61,7 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
// Close drawer on route change / resize
|
||||
// Close drawer on resize
|
||||
useEffect(() => {
|
||||
const onResize = () => { if (window.innerWidth >= 768) setDrawerOpen(false); };
|
||||
window.addEventListener('resize', onResize, { passive: true });
|
||||
@@ -54,20 +81,26 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
|
||||
}, [drawerOpen]);
|
||||
|
||||
const isActive = (href: string) => {
|
||||
if (href === '/') return activePath === '/';
|
||||
return activePath === href || activePath.startsWith(href + '/');
|
||||
const current = (activePath || '').replace(/\/+$/, '') || '/';
|
||||
const target = (href || '').replace(/\/+$/, '') || '/';
|
||||
if (target === '/') return current === '/';
|
||||
return current === target || current.startsWith(target + '/');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ── Desktop / Landscape Fixed Left Sidebar (Permanent, no hamburger, no minimize/maximize) ── */}
|
||||
<aside
|
||||
className="hidden md:flex fixed top-0 left-0 bottom-0 z-40 w-64 h-screen flex-col justify-between border-r border-border-custom bg-background-custom/95 dark:bg-[#090a0f]/95 backdrop-blur-md p-5 select-none"
|
||||
className="hidden md:flex fixed top-0 left-0 bottom-0 z-40 w-64 h-full flex-col border-r border-border-custom bg-background-custom/95 dark:bg-[#090a0f]/95 backdrop-blur-md select-none overflow-hidden overscroll-contain"
|
||||
aria-label="Desktop sidebar navigation"
|
||||
>
|
||||
{/* Top: Brand Header - Only name, no icon and no full-stack developer label */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<a href="/" className="flex items-center group focus:outline-none py-1">
|
||||
<div className="shrink-0 px-5 pt-5 pb-3 flex flex-col gap-3">
|
||||
<a
|
||||
href="/"
|
||||
onClick={() => setActivePath('/')}
|
||||
className="flex items-center group focus:outline-none py-0.5"
|
||||
>
|
||||
<span className="font-heading font-bold text-base tracking-tight text-foreground-custom group-hover:text-primary-custom transition-colors truncate">
|
||||
{fullName}
|
||||
</span>
|
||||
@@ -76,34 +109,43 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
|
||||
<div className="h-px w-full bg-border-custom/80" />
|
||||
</div>
|
||||
|
||||
{/* Center: Vertical Navigation Links */}
|
||||
<nav className="flex flex-col gap-1 py-4 overflow-y-auto flex-1 my-2" aria-label="Sidebar main links">
|
||||
{/* Center: Vertical Navigation Links (Text-only, no icons, min-h-0 prevents flexbox twitch loop) */}
|
||||
<nav
|
||||
className="flex-1 min-h-0 overflow-y-auto px-4 py-2 flex flex-col gap-1"
|
||||
aria-label="Sidebar main links"
|
||||
>
|
||||
{navItems.map((item) => {
|
||||
const active = isActive(item.href);
|
||||
return (
|
||||
<a
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
className={`flex items-center gap-3 px-3.5 py-2.5 rounded-xl text-xs tracking-wide transition-all duration-200 ${
|
||||
onClick={() => setActivePath(item.href)}
|
||||
className={`group flex items-center justify-between px-3.5 py-2.5 rounded-xl text-sm transition-all duration-200 shrink-0 ${
|
||||
active
|
||||
? 'text-foreground-custom font-bold'
|
||||
? 'text-foreground-custom font-semibold bg-foreground-custom/[0.04]'
|
||||
: 'text-muted-foreground-custom hover:text-foreground-custom hover:bg-foreground-custom/5 font-medium'
|
||||
}`}
|
||||
>
|
||||
<i className={`${item.icon} text-sm w-4 text-center shrink-0 transition-colors ${active ? 'text-primary-custom' : 'text-muted-foreground-custom/70'}`} />
|
||||
<span className="truncate">{item.label}</span>
|
||||
{active && (
|
||||
<span className="ml-auto w-1.5 h-1.5 rounded-full bg-primary-custom shrink-0" />
|
||||
)}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full transition-all duration-200 ${
|
||||
active
|
||||
? 'bg-primary-custom scale-100 shadow-[0_0_8px_var(--primary-color)]'
|
||||
: 'bg-transparent scale-0'
|
||||
}`}
|
||||
/>
|
||||
<span className="truncate">{item.label}</span>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Bottom: Controls & Status */}
|
||||
<div className="flex flex-col gap-3 pt-4 border-t border-border-custom/80">
|
||||
<div className="shrink-0 px-5 py-4 border-t border-border-custom/80 flex flex-col gap-3 mt-auto bg-background-custom/40 dark:bg-[#090a0f]/40">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[11px] font-mono text-muted-foreground-custom">
|
||||
<span className="text-xs font-mono text-muted-foreground-custom">
|
||||
Preferences
|
||||
</span>
|
||||
<Suspense fallback={<div className="w-8 h-8 rounded-full bg-foreground-custom/10 animate-pulse" />}>
|
||||
@@ -113,18 +155,18 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
|
||||
|
||||
<a
|
||||
href="/contact"
|
||||
className="flex items-center justify-center gap-2 w-full px-3.5 py-2.5 rounded-xl text-xs font-bold uppercase tracking-wider bg-primary-custom text-white hover:bg-primary-custom/90 active:scale-98 transition-all shadow-sm"
|
||||
onClick={() => setActivePath('/contact')}
|
||||
className="flex items-center justify-center w-full px-4 py-2.5 rounded-xl text-xs font-bold uppercase tracking-wider bg-primary-custom text-white hover:bg-primary-custom/90 active:scale-98 transition-all shadow-sm"
|
||||
>
|
||||
<span>Get in Touch</span>
|
||||
<i className="fa-solid fa-arrow-right text-[10px]" />
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* ── Mobile Sticky Top Header (Only on screens < md) ── */}
|
||||
<header
|
||||
className={`md:hidden nav-enter fixed top-0 inset-x-0 z-50 glass-nav transition-all duration-300 ${scrolled || drawerOpen ? 'shadow-md bg-background-custom/95 dark:bg-[#090a0f]/95 backdrop-blur-md' : ''}`}
|
||||
style={{ height: '60px' }}
|
||||
className={`md:hidden fixed top-0 inset-x-0 z-50 glass-nav transition-all duration-300 touch-manipulation ${scrolled || drawerOpen ? 'shadow-md bg-background-custom/95 dark:bg-[#090a0f]/95 backdrop-blur-md' : ''}`}
|
||||
style={{ height: '60px', touchAction: 'manipulation' }}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 h-full flex items-center justify-between gap-3">
|
||||
{/* Brand - Only Name, no icon */}
|
||||
@@ -168,10 +210,10 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
|
||||
{/* ── Mobile Navigation Full-Screen Overlay (From under top navbar to bottom) ── */}
|
||||
{drawerOpen && (
|
||||
<div
|
||||
className="md:hidden fixed inset-x-0 top-[60px] bottom-0 z-40 bg-background-custom/98 dark:bg-[#090a0f]/98 backdrop-blur-xl border-t border-border-custom flex flex-col justify-between overflow-hidden animate-modal-fade select-none"
|
||||
className="md:hidden fixed inset-x-0 top-[60px] bottom-0 z-40 bg-background-custom dark:bg-[#090a0f] border-t border-border-custom flex flex-col justify-between overflow-hidden animate-modal-fade select-none"
|
||||
style={{ height: 'calc(100dvh - 60px)' }}
|
||||
>
|
||||
{/* Scrollable Navigation Links */}
|
||||
{/* Scrollable Navigation Links (Text-only, no icons, subtle active indicator dot) */}
|
||||
<nav className="flex-1 overflow-y-auto px-5 py-6 flex flex-col gap-1.5" aria-label="Mobile navigation links">
|
||||
{navItems.map((item) => {
|
||||
const active = isActive(item.href);
|
||||
@@ -179,18 +221,26 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
|
||||
<a
|
||||
key={item.label}
|
||||
href={item.href}
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className={`flex items-center justify-between px-4 py-3.5 rounded-2xl text-base transition-all duration-200 ${
|
||||
onClick={() => {
|
||||
setActivePath(item.href);
|
||||
setDrawerOpen(false);
|
||||
}}
|
||||
className={`group flex items-center justify-between px-4 py-3 rounded-xl text-base transition-all duration-200 ${
|
||||
active
|
||||
? 'text-primary-custom font-bold bg-primary-custom/10 border border-primary-custom/25'
|
||||
? 'text-foreground-custom font-bold bg-foreground-custom/[0.04]'
|
||||
: 'text-muted-foreground-custom hover:text-foreground-custom hover:bg-foreground-custom/5 font-medium'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3.5">
|
||||
<i className={`${item.icon} text-base w-5 text-center transition-colors ${active ? 'text-primary-custom' : 'text-muted-foreground-custom/70'}`} />
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full transition-all duration-200 ${
|
||||
active
|
||||
? 'bg-primary-custom scale-100 shadow-[0_0_8px_var(--primary-color)]'
|
||||
: 'bg-transparent scale-0'
|
||||
}`}
|
||||
/>
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
{active && <span className="w-2 h-2 rounded-full bg-primary-custom shrink-0 shadow-xs" />}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
@@ -200,23 +250,24 @@ export default function Navbar({ fullName = 'Cris Charles', logoImage = '/logo.p
|
||||
<div className="p-5 border-t border-border-custom bg-foreground-custom/[0.02] flex flex-col gap-3.5 shrink-0">
|
||||
{/* Preferences / Settings row */}
|
||||
<div className="flex items-center justify-between px-1 py-1">
|
||||
<span className="text-xs font-mono text-muted-foreground-custom flex items-center gap-2">
|
||||
<i className="fa-solid fa-sliders text-primary-custom" />
|
||||
<span>Preferences & Settings</span>
|
||||
<span className="text-xs font-mono text-muted-foreground-custom">
|
||||
Preferences & Settings
|
||||
</span>
|
||||
<Suspense fallback={<div className="w-8 h-8 rounded-full bg-foreground-custom/10 animate-pulse" />}>
|
||||
<ControlsDropdown />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
{/* Mobile Contact Button */}
|
||||
{/* Mobile Contact Button (Clean text, no icon) */}
|
||||
<a
|
||||
href="/contact"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
className="flex items-center justify-center gap-2 w-full px-4 py-3 rounded-xl text-xs font-bold uppercase tracking-wider bg-primary-custom text-white hover:bg-primary-custom/90 active:scale-98 transition-all shadow-md"
|
||||
onClick={() => {
|
||||
setActivePath('/contact');
|
||||
setDrawerOpen(false);
|
||||
}}
|
||||
className="flex items-center justify-center w-full px-4 py-3 rounded-xl text-xs font-bold uppercase tracking-wider bg-primary-custom text-white hover:bg-primary-custom/90 active:scale-98 transition-all shadow-md"
|
||||
>
|
||||
<span>Get in Touch</span>
|
||||
<i className="fa-solid fa-arrow-right text-[10px]" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -156,7 +156,7 @@ export default function PhotoGallery({ items }: PhotoGalleryProps) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* ─── Filter Bar ─── */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8 pb-4 border-b border-border-custom">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4 mb-4 sm:mb-8 pb-3 sm:pb-4 border-b border-border-custom">
|
||||
<div>
|
||||
<h2 className="text-2xl sm:text-3xl font-bold font-heading text-foreground-custom tracking-tight">
|
||||
Photo Gallery
|
||||
|
||||
@@ -250,7 +250,7 @@ export default function PortfolioAssistantFAB() {
|
||||
return (
|
||||
<>
|
||||
{/* ─── Floating Action Button (FAB) ─── */}
|
||||
<div className="fixed bottom-6 right-5 sm:right-7 z-40">
|
||||
<div className="fixed bottom-4 sm:bottom-6 right-4 sm:right-7 z-40">
|
||||
<button
|
||||
onClick={() => {
|
||||
haptic.tap();
|
||||
@@ -258,7 +258,7 @@ export default function PortfolioAssistantFAB() {
|
||||
}}
|
||||
aria-label={isOpen ? "Close Portfolio Assistant" : "Open Portfolio Assistant"}
|
||||
aria-expanded={isOpen}
|
||||
className="relative group p-3.5 rounded-2xl bg-primary-custom text-white shadow-xl hover:bg-primary-custom/90 hover:scale-105 active:scale-95 transition-all flex items-center gap-2 cursor-pointer border border-white/20"
|
||||
className="relative group p-3 sm:p-3.5 rounded-2xl bg-primary-custom text-white shadow-xl hover:bg-primary-custom/90 hover:scale-105 active:scale-95 transition-all flex items-center gap-2 cursor-pointer border border-white/20"
|
||||
>
|
||||
{/* Animated ping circle */}
|
||||
{!isOpen && (
|
||||
@@ -274,7 +274,7 @@ export default function PortfolioAssistantFAB() {
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a.75.75 0 0 1-1.154-.63 4.887 4.887 0 0 0 1.28-3.138A7.834 7.834 0 0 1 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline text-xs font-bold font-heading pr-0.5">
|
||||
<span className="hidden md:inline landscape:hidden lg:landscape:inline text-xs font-bold font-heading pr-0.5">
|
||||
Portfolio Assistant
|
||||
</span>
|
||||
</>
|
||||
|
||||
+107
-42
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import { ClientRouter } from 'astro:transitions';
|
||||
import BackToTop from '../components/BackToTop';
|
||||
import Background from '../components/Background';
|
||||
import SpotlightLens from '../components/SpotlightLens';
|
||||
@@ -27,10 +28,10 @@ const isRootPage = Astro.url.pathname === '/';
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<link rel="apple-touch-icon" href="/logo.png" />
|
||||
<!-- PWA & Mobile Web App Manifest -->
|
||||
@@ -47,40 +48,56 @@ const isRootPage = Astro.url.pathname === '/';
|
||||
<!-- FontAwesome 6 Icons -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- Theme Initialization Script (Prevents FOUC & auto-detects system theme) -->
|
||||
<!-- Astro ClientRouter for seamless SPA page transitions (no reload, no twitch) -->
|
||||
<ClientRouter />
|
||||
|
||||
<!-- Theme Initialization Script (Prevents FOUC & preserves theme on Astro transitions) -->
|
||||
<script is:inline>
|
||||
(function() {
|
||||
function getResolvedTheme() {
|
||||
var storedTheme = localStorage.getItem('theme');
|
||||
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (storedTheme === 'dark') return 'dark';
|
||||
if (storedTheme === 'light') return 'light';
|
||||
// Default: auto-detect system theme (storedTheme is null or 'system')
|
||||
return prefersDark ? 'dark' : 'light';
|
||||
if (storedTheme === 'dark') return 'dark';
|
||||
if (storedTheme === 'system') {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
// Default: dark mode signature theme
|
||||
return 'dark';
|
||||
}
|
||||
|
||||
function applyTheme() {
|
||||
function applyTheme(doc) {
|
||||
var targetDoc = doc || document;
|
||||
var theme = getResolvedTheme();
|
||||
var metaThemeColor = targetDoc.querySelector('meta[name="theme-color"]');
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
targetDoc.documentElement.classList.add('dark');
|
||||
if (metaThemeColor) metaThemeColor.setAttribute('content', '#090a0f');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
targetDoc.documentElement.classList.remove('dark');
|
||||
if (metaThemeColor) metaThemeColor.setAttribute('content', '#fafafa');
|
||||
}
|
||||
}
|
||||
|
||||
applyTheme();
|
||||
applyTheme(document);
|
||||
|
||||
// Automatically adapt if user has not set a manual override or selected system
|
||||
// Crucial for Astro ClientRouter: re-apply theme on before-swap, after-swap and page-load
|
||||
document.addEventListener('astro:before-swap', function(e) {
|
||||
if (e.newDocument) applyTheme(e.newDocument);
|
||||
});
|
||||
document.addEventListener('astro:after-swap', function() {
|
||||
applyTheme(document);
|
||||
});
|
||||
document.addEventListener('astro:page-load', function() {
|
||||
applyTheme(document);
|
||||
});
|
||||
|
||||
// Automatically adapt if user explicitly selected system
|
||||
try {
|
||||
var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
var listener = function(e) {
|
||||
var stored = localStorage.getItem('theme');
|
||||
if (!stored || stored === 'system') {
|
||||
if (e.matches) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
if (stored === 'system') {
|
||||
applyTheme(document);
|
||||
window.dispatchEvent(new CustomEvent('theme-change', { detail: { theme: e.matches ? 'dark' : 'light' } }));
|
||||
}
|
||||
};
|
||||
@@ -98,37 +115,85 @@ const isRootPage = Astro.url.pathname === '/';
|
||||
<!-- Orientation Stamp Script (Prevents nav layout shift) -->
|
||||
<script is:inline>
|
||||
(function() {
|
||||
function setOrientation() {
|
||||
function setOrientation(doc) {
|
||||
var targetDoc = doc || document;
|
||||
const isPortrait = window.matchMedia('(orientation: portrait)').matches;
|
||||
const isNarrowLandscape = !isPortrait && window.innerWidth < 1024;
|
||||
document.documentElement.dataset.orientation = isPortrait
|
||||
targetDoc.documentElement.dataset.orientation = isPortrait
|
||||
? 'portrait'
|
||||
: isNarrowLandscape
|
||||
? 'landscape-compact'
|
||||
: 'landscape';
|
||||
}
|
||||
setOrientation();
|
||||
window.addEventListener('resize', setOrientation, { passive: true });
|
||||
window.addEventListener('orientationchange', setOrientation, { passive: true });
|
||||
setOrientation(document);
|
||||
window.addEventListener('resize', function() { setOrientation(document); }, { passive: true });
|
||||
window.addEventListener('orientationchange', function() { setOrientation(document); }, { passive: true });
|
||||
document.addEventListener('astro:before-swap', function(e) {
|
||||
if (e.newDocument) setOrientation(e.newDocument);
|
||||
});
|
||||
document.addEventListener('astro:after-swap', function() {
|
||||
setOrientation(document);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<!-- Performance Tier & Lite Mode Detection -->
|
||||
<!-- Performance Tier, Ultra-Fast Design & Lite Mode Detection (Persists across all page transitions) -->
|
||||
<script is:inline>
|
||||
(function() {
|
||||
var manualLite = localStorage.getItem('liteMode');
|
||||
var tier = 'mid';
|
||||
if (manualLite === 'true') {
|
||||
tier = 'low';
|
||||
} else {
|
||||
// Lite mode default is disabled (Off) unless explicitly enabled by user
|
||||
var cores = navigator.hardwareConcurrency || 4;
|
||||
if (cores >= 8 && window.devicePixelRatio >= 2) {
|
||||
tier = 'high';
|
||||
function applyTier(doc) {
|
||||
var targetDoc = doc || document;
|
||||
var manualLite = localStorage.getItem('liteMode');
|
||||
var tier = 'mid';
|
||||
if (manualLite === 'true') {
|
||||
tier = 'low';
|
||||
} else {
|
||||
tier = 'mid';
|
||||
// Lite mode default is disabled (Off) unless explicitly enabled by user
|
||||
var cores = navigator.hardwareConcurrency || 4;
|
||||
if (cores >= 8 && window.devicePixelRatio >= 2) {
|
||||
tier = 'high';
|
||||
} else {
|
||||
tier = 'mid';
|
||||
}
|
||||
}
|
||||
targetDoc.documentElement.dataset.tier = tier;
|
||||
return tier;
|
||||
}
|
||||
|
||||
function applyFontZoom(doc) {
|
||||
var targetDoc = doc || document;
|
||||
var savedZoom = localStorage.getItem('pageZoom');
|
||||
if (savedZoom) {
|
||||
var zoomVal = parseInt(savedZoom, 10);
|
||||
if (!isNaN(zoomVal) && zoomVal >= 80 && zoomVal <= 130) {
|
||||
targetDoc.documentElement.style.fontSize = ((zoomVal / 100) * 15) + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
document.documentElement.dataset.tier = tier;
|
||||
|
||||
function initPreferences(doc) {
|
||||
var targetDoc = doc || document;
|
||||
var tier = applyTier(targetDoc);
|
||||
applyFontZoom(targetDoc);
|
||||
return tier;
|
||||
}
|
||||
|
||||
initPreferences(document);
|
||||
|
||||
// Crucial for Astro ClientRouter: Apply to e.newDocument BEFORE swap to prevent attribute loss
|
||||
document.addEventListener('astro:before-swap', function(e) {
|
||||
if (e.newDocument) {
|
||||
initPreferences(e.newDocument);
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure Ultra-Fast Design & Zoom persist across ALL pages during client router transitions
|
||||
document.addEventListener('astro:after-swap', function() {
|
||||
var tier = initPreferences(document);
|
||||
window.dispatchEvent(new CustomEvent('tier-change', { detail: { tier: tier } }));
|
||||
});
|
||||
document.addEventListener('astro:page-load', function() {
|
||||
var tier = initPreferences(document);
|
||||
window.dispatchEvent(new CustomEvent('tier-change', { detail: { tier: tier } }));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -144,16 +209,16 @@ const isRootPage = Astro.url.pathname === '/';
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background-custom text-foreground-custom bg-gradient-mesh min-h-screen flex flex-col selection:bg-primary-custom/30">
|
||||
<Background client:load />
|
||||
<div id="magnify-content" class="w-full min-h-screen flex flex-col pt-[60px] md:pt-0 md:pl-64">
|
||||
<Background client:load transition:persist="main-bg" />
|
||||
<div id="magnify-content" class="w-full max-w-full min-h-screen flex flex-col pt-[60px] md:pt-0 md:pl-64 overflow-x-clip">
|
||||
<div class="hidden glass-card glass-nav text-glow glow-effect reveal-fade-up reveal-fade-down reveal-fade-left reveal-fade-right reveal-scale-up"></div>
|
||||
<slot />
|
||||
</div>
|
||||
<BackToTop client:idle />
|
||||
<PortfolioAssistantFAB client:idle />
|
||||
<SpotlightLens client:load />
|
||||
<NetworkStatus client:load />
|
||||
<LagNotification client:load />
|
||||
<BackToTop client:idle transition:persist="back-to-top" />
|
||||
<PortfolioAssistantFAB client:idle transition:persist="portfolio-assistant" />
|
||||
<SpotlightLens client:load transition:persist="spotlight-lens" />
|
||||
<NetworkStatus client:load transition:persist="network-status" />
|
||||
<LagNotification client:load transition:persist="lag-notification" />
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
@@ -18,7 +18,7 @@ const socialLinks = await getSocialLinks();
|
||||
description="Learn more about Cris Charles Garcia's professional history, technical expertise, education, and developer biography."
|
||||
image={settings.heroImage || settings.logoImage || '/logo.png'}
|
||||
>
|
||||
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
|
||||
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} currentPath={Astro.url.pathname} client:load transition:persist="main-navbar" />
|
||||
|
||||
<main class="flex-1 pb-16">
|
||||
<AboutDetails settings={settings} experience={experience} education={education} client:load />
|
||||
|
||||
@@ -15,7 +15,7 @@ const socialLinks = await getSocialLinks();
|
||||
description="Verified credentials and certifications earned by Cris Charles Garcia from Cisco and other industry-recognized programs."
|
||||
image={settings.heroImage || settings.logoImage || '/logo.png'}
|
||||
>
|
||||
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
|
||||
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} currentPath={Astro.url.pathname} client:load transition:persist="main-navbar" />
|
||||
|
||||
<main class="flex-1 min-h-screen pb-16">
|
||||
<Certifications certifications={certifications} client:load />
|
||||
|
||||
@@ -14,9 +14,9 @@ const socialLinks = await getSocialLinks();
|
||||
description="Get in touch with Cris Charles Garcia for opportunities, consulting, or project collaborations."
|
||||
image={settings.heroImage || settings.logoImage || '/logo.png'}
|
||||
>
|
||||
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
|
||||
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} currentPath={Astro.url.pathname} client:load transition:persist="main-navbar" />
|
||||
|
||||
<main class="flex-1 pt-6 sm:pt-10 pb-16">
|
||||
<main class="flex-1 pt-2 sm:pt-6 pb-16">
|
||||
<ContactForm
|
||||
socialLinks={socialLinks}
|
||||
contactHeading={settings.contactHeading}
|
||||
|
||||
@@ -15,9 +15,9 @@ const socialLinks = await getSocialLinks();
|
||||
description="Visual showcase and photo archive of project milestones, developer activities, and academic experiences of Cris Charles Garcia."
|
||||
image={settings.heroImage || settings.logoImage || '/logo.png'}
|
||||
>
|
||||
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
|
||||
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} currentPath={Astro.url.pathname} client:load transition:persist="main-navbar" />
|
||||
|
||||
<main class="flex-1 min-h-screen pt-6 sm:pt-8 pb-16 px-4 sm:px-6">
|
||||
<main class="flex-1 min-h-screen pt-3 sm:pt-8 pb-16 px-4 sm:px-6">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<PhotoGallery items={galleryItems} client:load />
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@ const socialLinks = await getSocialLinks();
|
||||
image={settings.heroImage || settings.logoImage || '/logo.png'}
|
||||
>
|
||||
<!-- Navigation bar -->
|
||||
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
|
||||
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} currentPath={Astro.url.pathname} client:load transition:persist="main-navbar" />
|
||||
|
||||
<!-- Main sections -->
|
||||
<main class="flex-1">
|
||||
|
||||
@@ -15,10 +15,10 @@ const socialLinks = await getSocialLinks();
|
||||
description="View all full-stack development and AI projects developed by Cris Charles Garcia."
|
||||
image={settings.heroImage || settings.logoImage || '/logo.png'}
|
||||
>
|
||||
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
|
||||
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} currentPath={Astro.url.pathname} client:load transition:persist="main-navbar" />
|
||||
|
||||
<main class="flex-1 pt-4 sm:pt-6 pb-16">
|
||||
<div class="container mx-auto px-4 sm:px-6 max-w-5xl text-center mb-3 sm:mb-5">
|
||||
<main class="flex-1 pt-2 sm:pt-6 pb-16">
|
||||
<div class="container mx-auto px-4 sm:px-6 max-w-5xl text-center mb-2 sm:mb-5">
|
||||
<h1 class="text-2xl sm:text-3xl md:text-4xl font-bold font-heading tracking-tight text-foreground-custom">
|
||||
All Work
|
||||
</h1>
|
||||
|
||||
@@ -25,9 +25,9 @@ const nextProject = currentIndex < allProjects.length - 1 ? allProjects[currentI
|
||||
description={project.summary}
|
||||
image={project.coverImage || settings.heroImage || settings.logoImage || '/logo.png'}
|
||||
>
|
||||
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} client:load />
|
||||
<Navbar fullName={settings.fullName} logoImage={settings.logoImage} currentPath="/projects" client:load transition:persist="main-navbar" />
|
||||
|
||||
<main class="flex-1 pt-6 sm:pt-10 pb-20 relative overflow-hidden">
|
||||
<main class="flex-1 pt-3 sm:pt-8 pb-20 relative overflow-hidden">
|
||||
<!-- Subtle ambient background glows (no enclosing border) -->
|
||||
<div class="absolute inset-0 z-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute -top-32 left-1/3 w-[600px] h-[500px] rounded-full bg-primary-custom/5 blur-3xl"></div>
|
||||
@@ -37,7 +37,7 @@ const nextProject = currentIndex < allProjects.length - 1 ? allProjects[currentI
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 max-w-6xl relative z-10">
|
||||
|
||||
{/* Top Navigation & Meta Breadcrumb */}
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-8">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-4 sm:mb-8">
|
||||
<a
|
||||
href="/projects"
|
||||
class="inline-flex items-center gap-2 text-xs font-mono font-medium text-muted-foreground-custom hover:text-foreground-custom transition-colors group"
|
||||
|
||||
+138
-36
@@ -20,11 +20,14 @@
|
||||
--border-hover-color: rgba(37, 99, 235, 0.3);
|
||||
--nav-bg: rgba(250, 250, 250, 0.8);
|
||||
|
||||
/* Minimalist Glass Surfaces (No thick borders or heavy background fills) */
|
||||
--glass-bg: rgba(255, 255, 255, 0.55);
|
||||
--glass-border: rgba(15, 17, 23, 0.05);
|
||||
/* ─── Optimized Glassmorphism Tokens (Desktop / Base) ─── */
|
||||
--glass-bg: rgba(255, 255, 255, 0.90);
|
||||
--glass-border: rgba(15, 17, 23, 0.08);
|
||||
--glass-blur: 16px;
|
||||
--glass-shadow: 0 4px 20px -4px rgba(0, 0, 0, 0.03);
|
||||
--glass-saturate: 140%;
|
||||
--glass-shadow: 0 4px 24px -4px rgba(0, 0, 0, 0.06);
|
||||
--glass-shadow-hover: 0 12px 32px -4px rgba(0, 0, 0, 0.1);
|
||||
--glass-highlight: inset 0 1px 0 0 rgba(255, 255, 255, 0.85);
|
||||
|
||||
--bento-gap: 1.25rem;
|
||||
--bento-radius: 24px;
|
||||
@@ -42,26 +45,78 @@
|
||||
--secondary-color: #38bdf8;
|
||||
--muted-color: rgba(241, 245, 249, 0.04);
|
||||
--muted-fg-color: #94a3b8;
|
||||
--border-color: rgba(241, 245, 249, 0.06);
|
||||
--border-hover-color: rgba(59, 130, 246, 0.35);
|
||||
--nav-bg: rgba(9, 10, 15, 0.8);
|
||||
--border-color: rgba(241, 245, 249, 0.08);
|
||||
--border-hover-color: rgba(59, 130, 246, 0.4);
|
||||
--nav-bg: rgba(9, 10, 15, 0.92);
|
||||
|
||||
--glass-bg: rgba(255, 255, 255, 0.03);
|
||||
--glass-border: rgba(255, 255, 255, 0.06);
|
||||
--glass-shadow: 0 8px 32px -8px rgba(0, 0, 0, 0.4);
|
||||
/* ─── Optimized Dark Glass Surfaces (Crisp contrast over 3D background) ─── */
|
||||
--glass-bg: rgba(13, 16, 26, 0.90);
|
||||
--glass-border: rgba(255, 255, 255, 0.09);
|
||||
--glass-blur: 16px;
|
||||
--glass-saturate: 140%;
|
||||
--glass-shadow: 0 8px 32px -8px rgba(0, 0, 0, 0.6);
|
||||
--glass-shadow-hover: 0 16px 40px -6px rgba(0, 0, 0, 0.75);
|
||||
--glass-highlight: inset 0 1px 0 0 rgba(255, 255, 255, 0.08);
|
||||
|
||||
--primary: 217 91% 60%;
|
||||
--background: 230 25% 5%;
|
||||
--foreground: 210 40% 98%;
|
||||
}
|
||||
|
||||
/* ─── Device-Tiered Glassmorphism Optimization (Mobile & Tablet GPU Savings) ─── */
|
||||
@media (max-width: 767px) {
|
||||
:root {
|
||||
--glass-blur: 10px;
|
||||
--glass-saturate: 120%;
|
||||
--glass-bg: rgba(255, 255, 255, 0.94);
|
||||
--glass-shadow: 0 2px 14px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.dark {
|
||||
--glass-blur: 10px;
|
||||
--glass-saturate: 120%;
|
||||
--glass-bg: rgba(13, 16, 26, 0.93);
|
||||
--glass-shadow: 0 4px 20px -4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 1023px) {
|
||||
:root {
|
||||
--glass-blur: 13px;
|
||||
--glass-saturate: 130%;
|
||||
}
|
||||
.dark {
|
||||
--glass-blur: 13px;
|
||||
--glass-saturate: 130%;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
overflow-y: scroll;
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
/* Smooth, flicker-free Astro View Transitions */
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation-duration: 0.15s;
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
html, body {
|
||||
overflow-x: hidden;
|
||||
max-width: 100vw;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--fg-color);
|
||||
font-family: var(--font-body);
|
||||
font-size: 15px;
|
||||
line-height: 1.65;
|
||||
overflow-x: hidden;
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
}
|
||||
|
||||
@@ -93,43 +148,86 @@
|
||||
|
||||
@variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* ─── Minimalist Glassmorphism Bento Cells ─── */
|
||||
/* ─── High-Performance Glassmorphism (Calibrated across all devices) ─── */
|
||||
.bento-card, .glass-card {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--bento-radius);
|
||||
box-shadow: var(--glass-shadow);
|
||||
transition: border-color 0.2s ease, transform 0.2s ease, background-color 0.2s ease;
|
||||
box-shadow: var(--glass-shadow), var(--glass-highlight);
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
|
||||
backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
|
||||
|
||||
/* GPU Compositing layer (prevents full-screen repaints during 3D background scrolling) */
|
||||
isolation: isolate;
|
||||
transform: translate3d(0, 0, 0);
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
|
||||
transition: border-color 0.25s ease, transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), background-color 0.25s ease, box-shadow 0.25s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
html[data-tier='mid'] .bento-card,
|
||||
html[data-tier='high'] .bento-card,
|
||||
html[data-tier='mid'] .glass-card,
|
||||
html[data-tier='high'] .glass-card {
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(140%);
|
||||
backdrop-filter: blur(var(--glass-blur)) saturate(140%);
|
||||
/* Mouse Hover States (Desktop / pointer-capable only, prevents sticky hover on mobile touch) */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.bento-card:hover, .glass-card:hover {
|
||||
border-color: var(--border-hover-color);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
transform: translateY(-2px) translateZ(0);
|
||||
box-shadow: var(--glass-shadow-hover), var(--glass-highlight);
|
||||
}
|
||||
|
||||
.dark .bento-card:hover, .dark .glass-card:hover {
|
||||
border-color: var(--border-hover-color);
|
||||
background: rgba(18, 22, 36, 0.95);
|
||||
transform: translateY(-2px) translateZ(0);
|
||||
box-shadow: var(--glass-shadow-hover), var(--glass-highlight);
|
||||
}
|
||||
}
|
||||
|
||||
.bento-card:hover, .glass-card:hover {
|
||||
border-color: var(--border-hover-color);
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.dark .bento-card:hover, .dark .glass-card:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
/* Mobile & Touch Device Tap Feedback */
|
||||
@media (hover: none) {
|
||||
.bento-card:active, .glass-card:active {
|
||||
transform: scale(0.995) translateZ(0);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/* Glass Navigation Bar */
|
||||
.glass-nav {
|
||||
background: var(--nav-bg);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
box-shadow: 0 4px 20px -2px rgba(0, 0, 0, 0.12), var(--glass-highlight);
|
||||
isolation: isolate;
|
||||
transform: translate3d(0, 0, 0);
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
|
||||
backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
|
||||
}
|
||||
|
||||
html[data-tier='mid'] .glass-nav,
|
||||
html[data-tier='high'] .glass-nav {
|
||||
-webkit-backdrop-filter: blur(20px) saturate(160%);
|
||||
backdrop-filter: blur(20px) saturate(160%);
|
||||
/* Fallback for Browsers Without Backdrop-Filter Support */
|
||||
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
|
||||
.bento-card, .glass-card {
|
||||
background: #ffffff !important;
|
||||
}
|
||||
.dark .bento-card, .dark .glass-card {
|
||||
background: #0f1118 !important;
|
||||
}
|
||||
.glass-nav {
|
||||
background: #fafafa !important;
|
||||
}
|
||||
.dark .glass-nav {
|
||||
background: #090a0f !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility: User Prefers Reduced Transparency */
|
||||
@media (prefers-reduced-transparency: reduce) {
|
||||
.bento-card, .glass-card, .glass-nav {
|
||||
-webkit-backdrop-filter: none !important;
|
||||
backdrop-filter: none !important;
|
||||
background: var(--bg-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bento-grid {
|
||||
@@ -229,6 +327,10 @@ html[data-tier='low'] {
|
||||
--glass-shadow: none !important;
|
||||
}
|
||||
|
||||
html[data-tier='low'] canvas {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 1. Global Performance & Complete Animation/Transition Stripping */
|
||||
html[data-tier='low'] *,
|
||||
html[data-tier='low'] *::before,
|
||||
@@ -270,14 +372,14 @@ html[data-tier='low']:not(.dark) {
|
||||
}
|
||||
|
||||
html[data-tier='low'].dark {
|
||||
--bg-color: #0c0d12;
|
||||
--fg-color: #f3f4f6;
|
||||
--bg-color: #090a0f;
|
||||
--fg-color: #f1f5f9;
|
||||
--border-color: #374151;
|
||||
--border-hover-color: #6b7280;
|
||||
--muted-color: #1a1d26;
|
||||
--muted-fg-color: #9ca3af;
|
||||
--muted-fg-color: #94a3b8;
|
||||
--primary-color: #3b82f6;
|
||||
--nav-bg: #0c0d12;
|
||||
--nav-bg: #090a0f;
|
||||
}
|
||||
|
||||
html[data-tier='low'] body {
|
||||
|
||||
Reference in new issue
Block a user