From 0f8b5887826f629b61a5a03aec9f4f7fa86406a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Montes=20Linares?= <128893712+aMonteSl@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:23:41 +0200 Subject: [PATCH] feat(ui): polish visuals, motion and support across devices Typography and layout - Load Inter through next/font: the CSS asked for it but never loaded it, so every OS fell back to its own system font. - Hero: CTAs before the long bio so they sit above the fold on 1366x768 and 1024x768 laptops; the bio is left-aligned on phones; balanced headings. - Section headers share the max-w-6xl content column (no 32px offset at >=1600px); featured project media aligns to the top of its card. - Skill cards: "View group" pinned to the bottom, container-query header so narrow cards keep their titles on one or two lines. - Journey: the "today" label no longer covers the next year's label. - Sidebar fits 1366x768 screens (smaller portrait and rows when short). Motion - MotionConfig reducedMotion="user" app-wide and no more `animate ? fadeInUp() : {}` spreads, which left elements stuck at opacity 0 for reduced-motion visitors after hydration. - Hero entrance is pure CSS, so the first screen paints before hydration; the preload now targets the portrait actually rendered (hero-320). - Softer, longer scroll-in entrances; real staggers instead of dead staggerChildren blocks; no duplicate entrance on secondary projects. - Journey timeline draws in when it scrolls into view; infinite pulses are gated (in view, horizontal layout, reduced motion); the ongoing node breathes instead of blinking. - No `transition-all` on framer-animated elements; non-clickable cards no longer lift on hover. Performance - Hero featured rotation drives its progress bar with a MotionValue and pauses off-screen (it re-rendered the Hero every frame, forever). - Silk background: dpr 1, no MSAA, low-power GPU, ~30fps on-demand loop, reactive reduced-motion check, transparent fallback (no gradient pop-in). - Drop the stale hand-written @react-three/fiber type shims. Devices - Notch-aware page gutter (px-gutter) for viewport-fit=cover, safe-area insets on header, drawer and floating menu button. - Footer clears the floating menu button below xl; 44px touch targets on footer links, DOI chip and tablet header nav; tech tags at 11px. - Base CSS moved to @layer base: one focus ring per element instead of outline + ring with 2px corners; ~150 lines of dead utilities removed. - Manifest theme/background colour matches the site (#040304). Co-Authored-By: Claude Opus 5.5 --- public/favicons/site.webmanifest | 4 +- src/app/es/page.tsx | 4 +- src/app/globals.css | 275 +++++------------- src/app/layout.tsx | 17 +- src/app/page.tsx | 4 +- src/components/common/Footer.tsx | 4 +- src/components/common/LocalizedShell.tsx | 18 +- src/components/ui/Container.tsx | 2 +- src/components/ui/SectionHeading.tsx | 7 +- src/components/ui/layout.tsx | 7 +- .../certifications/CertificationsSection.tsx | 23 +- src/features/contact/ContactCTASection.tsx | 13 +- .../journey/ParallelStreamsSection.tsx | 74 +++-- src/features/journey/StreamCard.tsx | 10 +- src/features/journey/StreamLegend.tsx | 3 +- src/features/landing/AnimatedBackground.tsx | 49 ++-- src/features/landing/FeaturedProjectCard.tsx | 55 +++- src/features/landing/Hero.tsx | 54 ++-- src/features/landing/Silk.tsx | 70 ++++- src/features/landing/useFeaturedRotation.ts | 133 +++++---- src/features/morphNav/MorphDrawer.tsx | 6 +- src/features/morphNav/MorphHeader.tsx | 8 +- src/features/morphNav/SidebarPanel.tsx | 5 +- src/features/morphNav/layout.ts | 3 + .../projects/FeaturedProjectsSection.tsx | 14 +- .../components/FeaturedProjectCard.tsx | 11 +- .../components/SecondaryProjectCard.tsx | 5 +- src/features/projects/components/TechTag.tsx | 2 +- .../skills/components/BentoCategoryCard.tsx | 22 +- .../testimonials/TestimonialsSection.tsx | 28 +- src/features/uses/UsesPageClient.tsx | 7 +- src/lib/motion.ts | 86 +----- src/types/fiber.d.ts | 31 -- src/types/three-fiber.d.ts | 10 - 34 files changed, 429 insertions(+), 635 deletions(-) delete mode 100644 src/types/fiber.d.ts delete mode 100644 src/types/three-fiber.d.ts diff --git a/public/favicons/site.webmanifest b/public/favicons/site.webmanifest index d01c9397..857c6631 100644 --- a/public/favicons/site.webmanifest +++ b/public/favicons/site.webmanifest @@ -15,7 +15,7 @@ "purpose": "maskable" } ], - "theme_color": "#403e3e", - "background_color": "#403e3e", + "theme_color": "#040304", + "background_color": "#040304", "display": "standalone" } \ No newline at end of file diff --git a/src/app/es/page.tsx b/src/app/es/page.tsx index 80f70abf..8df9bb75 100644 --- a/src/app/es/page.tsx +++ b/src/app/es/page.tsx @@ -1,14 +1,14 @@ import type { Metadata } from 'next' import { JsonLd, LocalizedShell } from '@/components/common' import { HomePageClient } from '@/features/landing' -import { SIDEBAR_CONTENT_OFFSET_CLASS } from '@/features/morphNav/layout' +import { FLOATING_MENU_CLEARANCE_CLASS, SIDEBAR_CONTENT_OFFSET_CLASS } from '@/features/morphNav/layout' import { buildHomeJsonLd, buildHomeMetadata } from '@/lib/seo' export const metadata: Metadata = buildHomeMetadata('es') export default function SpanishHome() { return ( - + diff --git a/src/app/globals.css b/src/app/globals.css index 77770526..35207dce 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -5,6 +5,25 @@ --breakpoint-xs: 26.25rem; } +/* + * Page gutter (16/24/32px) that also clears landscape notches: the viewport uses + * viewport-fit=cover, so content would otherwise slide under the notch. + */ +@utility px-gutter { + padding-left: max(1rem, env(safe-area-inset-left)); + padding-right: max(1rem, env(safe-area-inset-right)); + + @media (width >= 40rem) { + padding-left: max(1.5rem, env(safe-area-inset-left)); + padding-right: max(1.5rem, env(safe-area-inset-right)); + } + + @media (width >= 64rem) { + padding-left: max(2rem, env(safe-area-inset-left)); + padding-right: max(2rem, env(safe-area-inset-right)); + } +} + /* CSS Variables for theming */ :root { --bg: #040304; @@ -43,7 +62,7 @@ body { radial-gradient(circle at 70% 8%, rgba(166, 107, 87, 0.12), transparent 32rem), linear-gradient(180deg, #060403 0%, var(--bg) 36%, #070504 100%); color: var(--fg); - font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + font-family: var(--font-inter), Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; line-height: 1.6; letter-spacing: 0.01em; position: relative; @@ -96,11 +115,37 @@ body::before { border-bottom: 1px solid; } -/* Focus styles for accessibility */ -*:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - border-radius: 2px; +/* + * Base rules live in the base layer so Tailwind utilities can override them + * (e.g. `focus-visible:outline-none` + a ring, or an explicit image height). + */ +@layer base { + /* Default focus ring for anything that does not draw its own */ + *:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } + + ::selection { + background-color: rgba(220, 162, 147, 0.3); + color: var(--fg); + } + + img { + image-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + max-width: 100%; + height: auto; + } + + /* Photos, portraits and carousel frames fill their aspect box */ + img[class*="object-cover"], + img[class*="object-contain"] { + display: block; + width: 100%; + height: 100%; + } } /* Skip to content link */ @@ -122,91 +167,6 @@ body::before { top: 6px; } -/* Utility classes */ -.container { - width: 100%; - max-width: 1200px; - margin: 0 auto; - padding: 0 1rem; -} - -@media (min-width: 640px) { - .container { - padding: 0 2rem; - } -} - -.section { - padding: 4rem 0; -} - -@media (min-width: 768px) { - .section { - padding: 6rem 0; - } -} - -/* Selection styling */ -::selection { - background-color: rgba(220, 162, 147, 0.3); - color: var(--fg); -} - -/* Image quality optimization - Prevent blurry images across entire project */ -img { - /* High quality rendering for all resolutions */ - image-rendering: -webkit-optimize-contrast; - image-rendering: auto; - - /* Smooth font rendering */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - /* Prevent layout shifts */ - max-width: 100%; - height: auto; - - /* Disable blur filter by default */ - filter: none; -} - -/* For images that need smooth scaling (photos, portraits, carousels) */ -img[class*="object-cover"], -img[class*="object-contain"], -motion img, -[data-framer-component-type="motion.img"] img { - image-rendering: auto; - - /* Ensure proper scaling */ - display: block; - width: 100%; - height: 100%; - - /* Smooth transitions - prevents flashing */ - will-change: opacity; - backface-visibility: hidden; - -webkit-backface-visibility: hidden; -} - -/* Avatar images - extra sharp but smooth */ -.avatar img, -[class*="Avatar"] img { - image-rendering: -webkit-optimize-contrast; - image-rendering: auto; -} - -/* Carousel and gallery images - optimized for smooth viewing and transitions */ -.carousel img, -.gallery img, -[class*="Carousel"] img, -[class*="Gallery"] img { - image-rendering: auto; - -webkit-font-smoothing: antialiased; - will-change: opacity; - backface-visibility: hidden; - -webkit-backface-visibility: hidden; -} - .photo-render { image-rendering: auto; filter: none; @@ -227,129 +187,36 @@ motion img, } } -/* Animation utility classes */ -.transition-base { - transition: all var(--dur-base) var(--ease); -} - -.transition-fast { - transition: all var(--dur-fast) var(--ease-out); -} - -/* Hover effects for interactive elements */ -.hover-lift { - transition: transform var(--dur-fast) var(--ease-out); -} - -.hover-lift:hover { - transform: translateY(-2px); -} - -/* Shine effect for avatar */ -.shine-on-hover { - position: relative; - overflow: hidden; -} - -.shine-on-hover::after { - content: ''; - position: absolute; - inset: 0; - background: linear-gradient( - 45deg, - transparent 30%, - rgba(255, 255, 255, 0.1) 50%, - transparent 70% - ); - transform: translateX(-100%); - transition: transform var(--dur-slow) var(--ease); -} - -.shine-on-hover:hover::after { - transform: translateX(100%); -} - -/* Reduce animations for accessibility */ -@media (prefers-reduced-motion: reduce) { - .scroll-cue { - animation: none; - } - - .transition-base, - .transition-fast, - .hover-lift, - .shine-on-hover::after { - transition: none; - } - - .hover-lift:hover { - transform: none; - } - - .shine-on-hover:hover::after { - transform: translateX(-100%); - } -} - -/* Fluid container utilities */ -.content-width { - max-width: 72rem; -} - -@media (min-width: 1536px) { - .content-width { - max-width: 80rem; +/* + * Hero entrance: plain CSS so the first screen paints (and animates) before + * JavaScript hydrates. Stagger with an arbitrary `[animation-delay:…]` utility. + */ +@layer components { + .hero-enter { + animation: hero-enter 0.7s var(--ease-out) both; } } -@media (min-width: 1920px) { - .content-width { - max-width: 88rem; +@keyframes hero-enter { + from { + opacity: 0; + transform: translateY(14px); } -} -@media (min-width: 2560px) { - .content-width { - max-width: 96rem; + to { + opacity: 1; + transform: none; } } -/* Fluid spacing utilities */ -.section-y { - padding-block: clamp(2rem, 4vw + 1rem, 6rem); -} - -.section-x { - padding-inline: clamp(1rem, 3vw, 3rem); -} - -/* Header scroll effects */ -.header { - transition: box-shadow 180ms var(--ease), backdrop-filter 180ms var(--ease); -} - -.header.has-shadow { - box-shadow: 0 6px 20px -12px rgba(0, 0, 0, 0.45); - backdrop-filter: saturate(1.1) blur(4px); -} - -/* Hero spotlight effect */ -.hero-spotlight::before { - content: ""; - position: absolute; - inset: -10% -10% -10% -10%; - background: radial-gradient(60% 60% at 50% 35%, rgba(220, 162, 147, 0.12), transparent 70%); - pointer-events: none; - z-index: -1; -} - -/* Reduce motion accessibility */ +/* Reduce animations for accessibility */ @media (prefers-reduced-motion: reduce) { - .header { - transition: none; - } - html { scroll-behavior: auto; } + + .scroll-cue, + .hero-enter { + animation: none; + } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c8c2d3a0..73eb9925 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,5 @@ import type { Metadata, Viewport } from 'next' +import { Inter } from 'next/font/google' import './globals.css' import { SITE } from '@/lib/constants' import { OG_IMAGE } from '@/lib/seo' @@ -58,6 +59,13 @@ export const metadata: Metadata = { }, } +// Self-hosted at build time so every device renders the same typeface (no system-font fallback) +const inter = Inter({ + subsets: ['latin'], + display: 'swap', + variable: '--font-inter', +}) + export const viewport: Viewport = { width: 'device-width', initialScale: 1, @@ -71,14 +79,15 @@ export default function RootLayout({ children: React.ReactNode }>) { return ( - + + {/* The hero portrait (Avatar size="hero") is the largest image on the first screen */} diff --git a/src/app/page.tsx b/src/app/page.tsx index 28589eea..0ca2e0a0 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,14 +1,14 @@ import type { Metadata } from 'next' import { JsonLd, LocalizedShell } from '@/components/common' import { HomePageClient } from '@/features/landing' -import { SIDEBAR_CONTENT_OFFSET_CLASS } from '@/features/morphNav/layout' +import { FLOATING_MENU_CLEARANCE_CLASS, SIDEBAR_CONTENT_OFFSET_CLASS } from '@/features/morphNav/layout' import { buildHomeJsonLd, buildHomeMetadata } from '@/lib/seo' export const metadata: Metadata = buildHomeMetadata('en') export default function Home() { return ( - + diff --git a/src/components/common/Footer.tsx b/src/components/common/Footer.tsx index 0f5b867f..2e9518d0 100644 --- a/src/components/common/Footer.tsx +++ b/src/components/common/Footer.tsx @@ -66,7 +66,7 @@ export function Footer({ className }: FooterProps = {}) {
  • {link.label} @@ -86,7 +86,7 @@ export function Footer({ className }: FooterProps = {}) { href={link.href} target={link.external ? '_blank' : undefined} rel={link.external ? 'noopener noreferrer' : undefined} - className="inline-flex min-h-9 max-w-full items-center gap-1.5 py-1 text-sm text-[var(--fg-muted)] transition-colors duration-200 hover:text-[var(--fg)]" + className="inline-flex min-h-9 max-w-full items-center gap-1.5 py-1 text-sm pointer-coarse:min-h-11 text-[var(--fg-muted)] transition-colors duration-200 hover:text-[var(--fg)]" > {link.label} {link.external && ( diff --git a/src/components/common/LocalizedShell.tsx b/src/components/common/LocalizedShell.tsx index 5419f5b5..fc425608 100644 --- a/src/components/common/LocalizedShell.tsx +++ b/src/components/common/LocalizedShell.tsx @@ -1,6 +1,7 @@ 'use client' import type { ReactNode } from 'react' +import { MotionConfig } from 'framer-motion' import { Footer } from '@/components/common/Footer' import { I18nProvider, useTranslations, type Locale } from '@/i18n' import { BrowserTranslateHint, LocalePreferenceGate } from '@/features/language' @@ -31,13 +32,16 @@ export function LocalizedShell({ }: LocalizedShellProps) { return ( - - -
    - {children} -
    - {showFooter &&