Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
--breakpoint-xs: 26.25rem;
}

/*
* Viewport-height variant: laptop screens once browser chrome is subtracted
* (1366x768 -> ~657px, 1536x864 -> ~750px, 1440x900 -> ~790px) and most phones.
* The hero uses it to fit its first screen. Combine with breakpoints: `lg:short:mt-4`.
*/
@custom-variant short (@media (max-height: 56.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.
Expand Down
6 changes: 4 additions & 2 deletions src/components/ui/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,16 @@ export interface MetricTileProps {
value: string
detail?: string
className?: string
/** Extra classes for the detail line (e.g. hiding it where vertical space is tight) */
detailClassName?: string
}

export function MetricTile({ label, value, detail, className }: MetricTileProps) {
export function MetricTile({ label, value, detail, className, detailClassName }: MetricTileProps) {
return (
<Surface variant="flat" className={cn('p-4', className)}>
<p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-[var(--fg-muted)]/58">{label}</p>
<p className="mt-2 text-sm font-semibold leading-snug text-[var(--fg)]">{value}</p>
{detail && <p className="mt-2 text-xs leading-relaxed text-[var(--fg-muted)]/78">{detail}</p>}
{detail && <p className={cn('mt-2 text-xs leading-relaxed text-[var(--fg-muted)]/78', detailClassName)}>{detail}</p>}
</Surface>
)
}
Expand Down
72 changes: 56 additions & 16 deletions src/features/landing/Hero.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import { useId, useState } from 'react'
import {
ArrowRightIcon,
Avatar,
Expand All @@ -15,6 +16,7 @@ import {
Surface,
} from '@/components/ui'
import { useTranslations, useLocale } from '@/i18n'
import { cn } from '@/lib/utils'
import { SOCIAL_LINKS, getCvUrl } from '@/lib/constants'
import { useEmailCopyFeedback } from '@/lib/hooks/useEmailCopyFeedback'
import { FEATURED_PROJECTS } from '@/content/featuredProjects'
Expand All @@ -26,6 +28,9 @@ import { FeaturedProjectCard } from './FeaturedProjectCard'

type SocialKey = (typeof SOCIAL_LINKS)[number]['key']

const METRIC_TILE_CLASS = 'lg:short:p-3'
const METRIC_DETAIL_CLASS = 'lg:short:max-xl:hidden'

function BriefcaseIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
Expand Down Expand Up @@ -58,6 +63,10 @@ export function Hero() {
} = useFeaturedRotation(FEATURED_PROJECTS)
const cvUrl = getCvUrl(locale)
const { copiedEmail, copyEmail } = useEmailCopyFeedback()
// On short screens (`short`: ≤900px tall) the long bio is clamped so the whole hero fits the first screen;
// the full text stays in the DOM for screen readers and search engines.
const [isBioExpanded, setIsBioExpanded] = useState(false)
const bioId = useId()
// Copy about the current role switches on its start date (see useEntryPhase)
const isRoleUpcoming = useEntryPhase(CURRENT_ROLE_ID) === 'upcoming'
const roleMetric = isRoleUpcoming ? 'metrics.upcoming' : 'metrics.current'
Expand All @@ -68,12 +77,17 @@ export function Hero() {
}

return (
<SectionShell id="home" className={`flex min-h-[calc(100svh-4rem)] items-center pt-24 lg:pt-28 ${SIDEBAR_BLEED_CLASS}`} tone="xr">
<SectionShell
id="home"
// From lg the hero owns the first screen: content is centred between the 64px header and the fold
className={`flex min-h-[calc(100svh-4rem)] items-center pt-24 lg:min-h-svh lg:pt-28 lg:pb-16 lg:short:pt-20 lg:short:pb-8 ${SIDEBAR_BLEED_CLASS}`}
tone="xr"
>
<div className="grid grid-cols-1 items-center gap-8 sm:gap-10 lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)] lg:gap-10 xl:gap-14">
{/* CSS entrance (.hero-enter): the first screen is visible before hydration */}
<div className="hero-enter order-2 flex flex-col items-center [animation-delay:160ms] lg:order-1">
<Surface variant="xr" className="technical-frame w-full max-w-md p-5 sm:p-6">
<div className="mb-5 flex items-center justify-between gap-4 text-[10px] font-semibold uppercase tracking-[0.18em] text-[var(--fg-muted)]/55">
<Surface variant="xr" className="technical-frame w-full max-w-md p-5 sm:p-6 lg:short:p-5">
<div className="mb-5 flex items-center justify-between gap-4 text-[10px] lg:short:mb-3 font-semibold uppercase tracking-[0.18em] text-[var(--fg-muted)]/55">
<span>XR FIELD</span>
<span>MD-ES / 2026</span>
</div>
Expand All @@ -85,11 +99,17 @@ export function Hero() {
>
<div className="absolute -inset-6 rounded-full border border-[var(--accent)]/20" />
<div className="relative overflow-hidden rounded-full border border-[var(--accent)]/28 bg-black/30 p-2 shadow-2xl shadow-black/35">
<Avatar size="hero" loading="eager" fetchPriority="high" />
{/* From lg the portrait scales with the viewport height (138px at 657px tall, capped at 224px) */}
<Avatar
size="hero"
loading="eager"
fetchPriority="high"
className="lg:!h-[clamp(8rem,21svh,14rem)] lg:!w-[clamp(8rem,21svh,14rem)]"
/>
</div>
</div>

<DividerLine className="my-6" />
<DividerLine className="my-6 lg:short:my-4" />

<FeaturedProjectCard
projects={FEATURED_PROJECTS}
Expand All @@ -111,19 +131,19 @@ export function Hero() {
</div>

<h1
className="hero-enter mt-4 [animation-delay:40ms] max-w-4xl text-balance text-4xl font-semibold leading-[1.05] tracking-normal text-[var(--fg)] sm:text-5xl md:text-6xl xl:text-7xl"
className="hero-enter mt-4 [animation-delay:40ms] max-w-4xl text-balance text-4xl font-semibold leading-[1.05] tracking-normal text-[var(--fg)] sm:text-5xl md:text-6xl lg:mt-3 lg:text-[clamp(2.75rem,7svh,4.5rem)]"
translate="no"
>
{t('name')}
</h1>

<h2
className="hero-enter mt-4 [animation-delay:80ms] max-w-2xl text-balance text-base font-medium leading-relaxed text-[var(--fg-muted)] sm:text-lg"
className="hero-enter mt-4 [animation-delay:80ms] lg:short:mt-3 max-w-2xl text-balance text-base font-medium leading-relaxed text-[var(--fg-muted)] sm:text-lg"
>
{t('headline')}
</h2>

<div className="hero-enter mt-6 flex [animation-delay:120ms] flex-wrap items-center justify-center gap-3 lg:justify-start">
<div className="hero-enter mt-6 flex [animation-delay:120ms] lg:short:mt-4 flex-wrap items-center justify-center gap-3 lg:justify-start">
<StatusPill tone="success">
<BriefcaseIcon className="h-3.5 w-3.5" />
{t(isRoleUpcoming ? 'availabilityLabelUpcoming' : 'availabilityLabel')}
Expand All @@ -132,7 +152,7 @@ export function Hero() {
<StatusPill tone="xr">{t('location')}</StatusPill>
</div>

<div className="hero-enter relative mt-7 flex [animation-delay:160ms] flex-wrap items-center justify-center gap-3 lg:justify-start">
<div className="hero-enter relative mt-7 flex [animation-delay:160ms] lg:short:mt-5 flex-wrap items-center justify-center gap-3 lg:justify-start">
<LinkButton href={cvUrl} download rel="noopener">
{t('ctaResume')}
<ArrowRightIcon className="h-4 w-4" />
Expand Down Expand Up @@ -168,14 +188,34 @@ export function Hero() {
</span>
</div>

<p className="hero-enter mt-8 [animation-delay:200ms] max-w-2xl text-pretty text-left text-sm leading-relaxed text-[var(--fg-muted)] sm:text-base">
{t(isRoleUpcoming ? 'aboutMeUpcoming' : 'aboutMe')}
</p>
<div className="hero-enter mt-8 w-full max-w-2xl text-left [animation-delay:200ms] lg:short:mt-5">
<p
id={bioId}
className={cn(
'text-pretty text-sm leading-relaxed text-[var(--fg-muted)] sm:text-base',
!isBioExpanded && 'short:line-clamp-4 lg:short:line-clamp-3'
)}
>
{t(isRoleUpcoming ? 'aboutMeUpcoming' : 'aboutMe')}
</p>
{/* Only short screens clamp the bio, so only they need the toggle */}
<button
type="button"
onClick={() => setIsBioExpanded((expanded) => !expanded)}
aria-expanded={isBioExpanded}
aria-controls={bioId}
className="mt-1 hidden min-h-9 items-center gap-1 rounded-lg text-sm font-semibold text-[var(--accent)] transition-colors hover:text-[var(--fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] short:inline-flex pointer-coarse:min-h-11"
>
{t(isBioExpanded ? 'readLess' : 'readMore')}
<ArrowRightIcon className={cn('h-3.5 w-3.5 transition-transform duration-200', isBioExpanded ? '-rotate-90' : 'rotate-90')} />
</button>
</div>

<div className="hero-enter mt-8 grid [animation-delay:240ms] w-full max-w-2xl grid-cols-1 gap-3 sm:grid-cols-3">
<MetricTile label={t(`${roleMetric}.label`)} value={t(`${roleMetric}.value`)} detail={t(`${roleMetric}.detail`)} />
<MetricTile label={t('metrics.research.label')} value={t('metrics.research.value')} detail={t('metrics.research.detail')} />
<MetricTile label={t('metrics.next.label')} value={t('metrics.next.value')} detail={t('metrics.next.detail')} />
<div className="hero-enter mt-8 grid [animation-delay:240ms] w-full max-w-2xl grid-cols-1 gap-3 sm:grid-cols-3 lg:short:mt-5">
{/* Short lg/laptop screens: tighter tiles; below xl the narrow columns would wrap the detail onto two lines */}
<MetricTile label={t(`${roleMetric}.label`)} value={t(`${roleMetric}.value`)} detail={t(`${roleMetric}.detail`)} className={METRIC_TILE_CLASS} detailClassName={METRIC_DETAIL_CLASS} />
<MetricTile label={t('metrics.research.label')} value={t('metrics.research.value')} detail={t('metrics.research.detail')} className={METRIC_TILE_CLASS} detailClassName={METRIC_DETAIL_CLASS} />
<MetricTile label={t('metrics.next.label')} value={t('metrics.next.value')} detail={t('metrics.next.detail')} className={METRIC_TILE_CLASS} detailClassName={METRIC_DETAIL_CLASS} />
</div>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"headlineSub": "Systems, Cloud, Full-Stack and XR developer tools",
"aboutMe": "Telematics Engineer from URJC, graduated in January 2026, with experience in software development, systems, and data visualization. Author of Code-XR, an open-source VS Code extension published at IEEE VISSOFT 2025, focused on visual analysis of code metrics in XR environments. Currently enrolled in the Master's Degree in Telecommunications Engineering at UPM, specializing in Machine Learning and Big Data, while working as a Specialist Technician on a URJC research project on dependencies in complex collections of software modules, after an extracurricular Cloud & Systems N2 internship at SATEC. Interested in software analytics, code repositories, and applied research.",
"aboutMeUpcoming": "Telematics Engineer from URJC, graduated in January 2026, with experience in software development, systems, and data visualization. Author of Code-XR, an open-source VS Code extension published at IEEE VISSOFT 2025, focused on visual analysis of code metrics in XR environments. Currently enrolled in the Master's Degree in Telecommunications Engineering at UPM, specializing in Machine Learning and Big Data. After an extracurricular Cloud & Systems N2 internship at SATEC, joining a URJC research project on dependencies in complex collections of software modules as a Specialist Technician on October 8, 2026. Interested in software analytics, code repositories, and applied research.",
"readMore": "Read more",
"readLess": "Show less",
"ctaProjects": "View Projects",
"ctaResume": "Download CV",
"availabilityLabel": "Currently active",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"headlineSub": "Sistemas, Cloud, Full-Stack y herramientas XR",
"aboutMe": "Ingeniero Telemático por la URJC, titulado en enero de 2026, con experiencia en desarrollo software, sistemas y visualización de datos. Autor de Code-XR, extensión open source para VS Code publicada en IEEE VISSOFT 2025, orientada al análisis visual de métricas de código en entornos XR. Actualmente cursando el Máster Universitario en Ingeniería de Telecomunicación en la UPM, con intensificación en Aprendizaje Automático y Big Data, compaginándolo con un puesto de Técnico Especialista en un proyecto de investigación de la URJC sobre dependencias en colecciones complejas de módulos software, tras unas prácticas extracurriculares en Cloud & Sistemas N2 de SATEC. Interesado en analítica de software, repositorios de código e investigación aplicada.",
"aboutMeUpcoming": "Ingeniero Telemático por la URJC, titulado en enero de 2026, con experiencia en desarrollo software, sistemas y visualización de datos. Autor de Code-XR, extensión open source para VS Code publicada en IEEE VISSOFT 2025, orientada al análisis visual de métricas de código en entornos XR. Actualmente cursando el Máster Universitario en Ingeniería de Telecomunicación en la UPM, con intensificación en Aprendizaje Automático y Big Data. Tras unas prácticas extracurriculares en Cloud & Sistemas N2 de SATEC, se incorpora el 8 de octubre de 2026 como Técnico Especialista a un proyecto de investigación de la URJC sobre dependencias en colecciones complejas de módulos software. Interesado en analítica de software, repositorios de código e investigación aplicada.",
"readMore": "Leer más",
"readLess": "Mostrar menos",
"ctaProjects": "Ver Proyectos",
"ctaResume": "Descargar CV",
"availabilityLabel": "Actualmente activo",
Expand Down
Loading