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
6 changes: 6 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import type { NextConfig } from "next";
const isProd = process.env.NODE_ENV === 'production';
const isUserPages = true; // Este es un repo <usuario>.github.io

// Fecha del build (YYYY-MM-DD, hora de Madrid): "hoy" con el que se pre-renderiza el HTML estático
const buildDate = new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Madrid' }).format(new Date());

const nextConfig: NextConfig = {
output: "export", // Imprescindible para GitHub Pages
env: {
NEXT_PUBLIC_BUILD_DATE: buildDate,
},
trailingSlash: true, // Ayuda con rutas relativas en GH Pages
distDir: isProd ? 'out' : '.next-dev', // Evita conflictos: .next-dev en dev, out en prod
images: {
Expand Down
4 changes: 2 additions & 2 deletions public/favicons/site.webmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"purpose": "maskable"
}
],
"theme_color": "#403e3e",
"background_color": "#403e3e",
"theme_color": "#040304",
"background_color": "#040304",
"display": "standalone"
}
17 changes: 16 additions & 1 deletion scripts/check-journey-timeline.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ function isPastHalfOfYear(today) {
return dateToYearPercent(today) > 50;
}

function isUpcomingEntry(entry, today) {
return toLocalDate(getEntryStartDate(entry)).getTime() > toLocalDate(today).getTime();
}

function getEntryEndDate(entry, today) {
if (entry.endYear === null) {
return today;
return isUpcomingEntry(entry, today) ? getEntryStartDate(entry) : today;
}

return {
Expand Down Expand Up @@ -159,4 +163,15 @@ assert(getVisibleTimelineYears(planned2028Entries, beforeHalf, 2020, 2026).at(-1

assert(hasScheduledContentInYear(baseEntries, 2027, nextYear), 'Ongoing entries must occupy the current new year.');

const upcomingRole = { id: 'urjcResearch', startYear: 2026, startMonth: 10, startDay: 8, endYear: null };
const beforeStart = { year: 2026, month: 10, day: 5 };
const onStart = { year: 2026, month: 10, day: 8 };
const afterStart = { year: 2026, month: 11, day: 2 };
const sameDate = (a, b) => a.year === b.year && a.month === b.month && a.day === b.day;

assert(isUpcomingEntry(upcomingRole, beforeStart), 'An open entry starting after today must be upcoming.');
assert(sameDate(getEntryEndDate(upcomingRole, beforeStart), getEntryStartDate(upcomingRole)), 'An upcoming open entry must end on its start date, never before it.');
assert(!isUpcomingEntry(upcomingRole, onStart), 'An entry must stop being upcoming on its start date.');
assert(sameDate(getEntryEndDate(upcomingRole, afterStart), afterStart), 'A started open entry must run until today.');

console.log('Journey timeline checks passed.');
4 changes: 2 additions & 2 deletions src/app/es/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<LocalizedShell locale="es" footerClassName={SIDEBAR_CONTENT_OFFSET_CLASS}>
<LocalizedShell locale="es" footerClassName={`${SIDEBAR_CONTENT_OFFSET_CLASS} ${FLOATING_MENU_CLEARANCE_CLASS}`}>
<JsonLd data={buildHomeJsonLd('es')} />
<HomePageClient />
</LocalizedShell>
Expand Down
275 changes: 71 additions & 204 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 */
Expand All @@ -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;
Expand All @@ -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;
}
}
Loading
Loading