diff --git a/astro.config.mjs b/astro.config.mjs index 53e49de..eb7b830 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,9 +1,7 @@ // @ts-check import { defineConfig } from 'astro/config'; -import react from '@astrojs/react'; - // https://astro.build/config export default defineConfig({ - integrations: [react()] + site: 'https://pgscom.es', }); \ No newline at end of file diff --git a/functions/contact.ts b/functions/contact.ts index c286d28..c041277 100644 --- a/functions/contact.ts +++ b/functions/contact.ts @@ -1,18 +1,18 @@ +interface TurnstileResponse { + success: boolean; + 'error-codes'?: string[]; +} + export const onRequestPost: PagesFunction = async ({ request, env }) => { - console.log('📨 Recibiendo petición de contacto...'); - try { const contentType = request.headers.get('content-type') || ''; - console.log('Content-Type:', contentType); let data: Record = {}; if (contentType.includes('application/json')) { data = await request.json(); - console.log('✅ Datos JSON parseados'); } else if (contentType.includes('application/x-www-form-urlencoded')) { const form = await request.formData(); data = Object.fromEntries([...form.entries()].map(([k, v]) => [k, String(v)])); - console.log('✅ Datos form-data parseados'); } else { console.error('❌ Content-type no soportado:', contentType); return new Response(JSON.stringify({ ok: false, error: 'Unsupported content type' }), { @@ -22,8 +22,7 @@ export const onRequestPost: PagesFunction = async ({ request, env }) => { } const { nombre, email, asunto, mensaje } = data; - console.log('Datos recibidos:', { nombre, email, asunto, mensajeLength: mensaje?.length }); - + if (!nombre || !email || !asunto || !mensaje) { console.error('❌ Campos faltantes'); return new Response(JSON.stringify({ ok: false, error: 'Missing fields' }), { @@ -32,7 +31,6 @@ export const onRequestPost: PagesFunction = async ({ request, env }) => { }); } - // Basic email validation const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { console.error('❌ Email inválido:', email); @@ -45,11 +43,11 @@ export const onRequestPost: PagesFunction = async ({ request, env }) => { // Validar Turnstile const turnstileToken = data['cf-turnstile-response']; if (!turnstileToken) { - console.error('❌ Token de Turnstile faltante'); - return new Response(JSON.stringify({ ok: false, error: 'Turnstile token missing' }), { - status: 400, - headers: { 'content-type': 'application/json' }, - }); + console.error('❌ Token de Turnstile faltante'); + return new Response(JSON.stringify({ ok: false, error: 'Turnstile token missing' }), { + status: 400, + headers: { 'content-type': 'application/json' }, + }); } const ip = request.headers.get('CF-Connecting-IP'); @@ -64,20 +62,16 @@ export const onRequestPost: PagesFunction = async ({ request, env }) => { method: 'POST', }); - const turnstileOutcome: any = await turnstileResult.json(); + const turnstileOutcome: TurnstileResponse = await turnstileResult.json(); if (!turnstileOutcome.success) { - console.error('❌ Validación de Turnstile fallida:', turnstileOutcome); - return new Response(JSON.stringify({ ok: false, error: 'Invalid Turnstile token' }), { + console.error('❌ Validación de Turnstile fallida:', turnstileOutcome); + return new Response(JSON.stringify({ ok: false, error: 'Invalid Turnstile token' }), { status: 403, headers: { 'content-type': 'application/json' }, }); } // Enviar mensaje via Telegram Bot - console.log('🔍 Verificando variables de Telegram...'); - console.log('TELEGRAM_BOT_TOKEN presente:', !!env.TELEGRAM_BOT_TOKEN); - console.log('TELEGRAM_CHAT_ID presente:', !!env.TELEGRAM_CHAT_ID); - if (!env.TELEGRAM_BOT_TOKEN || !env.TELEGRAM_CHAT_ID) { console.error('❌ Variables de entorno de Telegram NO configuradas'); return new Response(JSON.stringify({ @@ -103,7 +97,6 @@ export const onRequestPost: PagesFunction = async ({ request, env }) => { `🕐 ${new Date().toLocaleString('es-ES', { timeZone: 'Europe/Madrid' })}`, ].join('\n'); - console.log('📤 Enviando mensaje a Telegram...'); const tgResp = await fetch(tgApi, { method: 'POST', headers: { 'content-type': 'application/json' }, @@ -115,7 +108,6 @@ export const onRequestPost: PagesFunction = async ({ request, env }) => { }); const tgData = await tgResp.json(); - console.log('Respuesta de Telegram:', { status: tgResp.status, ok: tgData.ok }); if (!tgResp.ok || !tgData.ok) { console.error('❌ Error de Telegram:', JSON.stringify(tgData)); @@ -128,7 +120,6 @@ export const onRequestPost: PagesFunction = async ({ request, env }) => { }); } - console.log('✅ Mensaje enviado exitosamente a Telegram'); return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'content-type': 'application/json' }, diff --git a/package-lock.json b/package-lock.json index efb3ae7..de03262 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,9 @@ "name": "pgscom.github.io", "version": "0.0.1", "dependencies": { - "@astrojs/react": "^4.4.0", - "@types/react": "^19.1.16", - "@types/react-dom": "^19.1.9", "astro": "^5.15.1", - "gsap": "^3.12.7", - "liquid-glass-react": "^1.1.1", - "react": "^19.1.1", - "react-dom": "^19.1.1" + "gsap": "^3.15.0", + "lenis": "^1.3.23" }, "optionalDependencies": { "@esbuild/linux-x64": "0.25.10" @@ -74,26 +69,6 @@ "node": "18.20.8 || ^20.3.0 || >=22.0.0" } }, - "node_modules/@astrojs/react": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@astrojs/react/-/react-4.4.0.tgz", - "integrity": "sha512-RzblkVImAFdV1C0AWsSWzS70Z0FMtW2p0XXkNYu3QePfyVJta3JIy8m8jY8271etaCZtpFjsE2UaiHGZIBm6nw==", - "license": "MIT", - "dependencies": { - "@vitejs/plugin-react": "^4.7.0", - "ultrahtml": "^1.6.0", - "vite": "^6.3.6" - }, - "engines": { - "node": "18.20.8 || ^20.3.0 || >=22.0.0" - }, - "peerDependencies": { - "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", - "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", - "react": "^17.0.2 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/@astrojs/telemetry": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.0.tgz", @@ -112,167 +87,6 @@ "node": "18.20.8 || ^20.3.0 || >=22.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -291,28 +105,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", @@ -328,68 +120,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/types": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", @@ -1269,63 +999,18 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@oslojs/encoding": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "license": "MIT" }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "license": "MIT" - }, "node_modules/@rollup/pluginutils": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", @@ -1707,47 +1392,6 @@ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "license": "MIT" }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1796,26 +1440,6 @@ "@types/unist": "*" } }, - "node_modules/@types/react": { - "version": "19.1.16", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.16.tgz", - "integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==", - "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.1.9", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", - "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "@types/react": "^19.0.0" - } - }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -1828,26 +1452,6 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2095,15 +1699,6 @@ "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", "license": "MIT" }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz", - "integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -2132,40 +1727,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/browserslist": { - "version": "4.26.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", - "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "baseline-browser-mapping": "^2.8.3", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, "node_modules/camelcase": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", @@ -2178,26 +1739,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001746", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001746.tgz", - "integrity": "sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -2326,12 +1867,6 @@ "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==", "license": "ISC" }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT" - }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -2446,12 +1981,6 @@ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "license": "CC0-1.0" }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2635,12 +2164,6 @@ "node": ">=4" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.227", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz", - "integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==", - "license": "ISC" - }, "node_modules/emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", @@ -2706,15 +2229,6 @@ "@esbuild/win32-x64": "0.25.10" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/escape-string-regexp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", @@ -2809,15 +2323,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-east-asian-width": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", @@ -2837,10 +2342,10 @@ "license": "ISC" }, "node_modules/gsap": { - "version": "3.12.7", - "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.7.tgz", - "integrity": "sha512-V4GsyVamhmKefvcAKaoy0h6si0xX7ogwBoBSs2CTJwt7luW0oZzC0LhdkyuKV8PJAXr7Yaj8pMjCKD4GJ+eEMg==", - "license": "Standard 'no charge' license: https://gsap.com/standard-license. Club GSAP members get more: https://gsap.com/licensing/. Why GreenSock doesn't employ an MIT license: https://gsap.com/why-license/" + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", + "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." }, "node_modules/h3": { "version": "1.15.5", @@ -3146,12 +2651,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -3164,41 +2663,35 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/liquid-glass-react": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/liquid-glass-react/-/liquid-glass-react-1.1.1.tgz", - "integrity": "sha512-pKzaktaMAEztd93wpWcz2Z5Z9qdLJUNJdMX+n00Ca4XsnrLTQ5xJzm/+GQXZUeuFXe/PQ8ziVMZO6531PyaFJw==", + "node_modules/lenis": { + "version": "1.3.23", + "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.23.tgz", + "integrity": "sha512-YxYq3TJqj9sJNv0V9SkyQHejt14xwyIwgDaaMK89Uf9SxQfIszu+gTQSSphh6BWlLTNVKvvXAGkg+Zf+oFIevg==", "license": "MIT", "workspaces": [ - "liquid-glass" + "packages/*", + "playground", + "playground/*" ], + "funding": { + "type": "github", + "url": "https://github.com/sponsors/darkroomengineering" + }, "peerDependencies": { - "react": ">=19", - "react-dom": ">=19" + "@nuxt/kit": ">=3.0.0", + "react": ">=17.0.0", + "vue": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "react": { + "optional": true + }, + "vue": { + "optional": true + } } }, "node_modules/longest-streak": { @@ -4111,12 +3604,6 @@ "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", "license": "MIT" }, - "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", - "license": "MIT" - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4350,38 +3837,6 @@ "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", "license": "MIT" }, - "node_modules/react": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", - "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", - "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.1" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -4627,7 +4082,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -4673,12 +4127,6 @@ "node": ">=11.0.0" } }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" - }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -4980,13 +4428,6 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, - "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "license": "MIT", - "optional": true - }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -5236,36 +4677,6 @@ } } }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -5313,7 +4724,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -5459,12 +4869,6 @@ "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", "license": "MIT" }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC" - }, "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", @@ -5518,7 +4922,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 2916fe2..7bc2943 100644 --- a/package.json +++ b/package.json @@ -9,14 +9,9 @@ "astro": "astro" }, "dependencies": { - "@astrojs/react": "^4.4.0", - "@types/react": "^19.1.16", - "@types/react-dom": "^19.1.9", "astro": "^5.15.1", - "gsap": "^3.12.7", - "liquid-glass-react": "^1.1.1", - "react": "^19.1.1", - "react-dom": "^19.1.1" + "gsap": "^3.15.0", + "lenis": "^1.3.23" }, "optionalDependencies": { "@esbuild/linux-x64": "0.25.10" diff --git a/public/PGSTextWebTitle.woff2 b/public/PGSTextWebTitle.woff2 new file mode 100644 index 0000000..98e9a61 Binary files /dev/null and b/public/PGSTextWebTitle.woff2 differ diff --git a/public/assets/animlogo/carga.svg b/public/assets/animlogo/carga.svg new file mode 100644 index 0000000..483b49a --- /dev/null +++ b/public/assets/animlogo/carga.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/animlogo/logo.svg b/public/assets/animlogo/logo.svg index 5f7516e..e111f88 100644 --- a/public/assets/animlogo/logo.svg +++ b/public/assets/animlogo/logo.svg @@ -5,44 +5,11 @@ viewBox="0 0 192.35219 192.35229" version="1.1" id="svg8" - inkscape:version="1.4.2 (f4327f4, 2025-05-13)" - sodipodi:docname="logoflat.svg" - inkscape:export-filename="C:\Users\pablo\Google Drive\Logo svg\Logocircular.png" - inkscape:export-xdpi="333.27728" - inkscape:export-ydpi="333.27728" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> - diff --git a/public/mascara.png b/public/mascara.png deleted file mode 100644 index 43cf3ae..0000000 Binary files a/public/mascara.png and /dev/null differ diff --git a/public/mascara.svg b/public/mascara.svg deleted file mode 100644 index d14c875..0000000 --- a/public/mascara.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/mascaramin.svg b/public/mascaramin.svg deleted file mode 100644 index 1961922..0000000 --- a/public/mascaramin.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/mascaraminmin.svg b/public/mascaraminmin.svg deleted file mode 100644 index 53ad4b9..0000000 --- a/public/mascaraminmin.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..1eb3610 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://pgscom.es/sitemap.xml diff --git a/src/components/liquidglasscomponent.jsx b/src/components/liquidglasscomponent.jsx deleted file mode 100644 index 0dfd2ee..0000000 --- a/src/components/liquidglasscomponent.jsx +++ /dev/null @@ -1,86 +0,0 @@ -import React, { useRef, useState, useEffect } from 'react'; -// liquid-glass-react exports a CommonJS module. Import the default and -// extract LiquidGlass safely to support both ESM and CJS interop. -import _pkg from 'liquid-glass-react'; -const LiquidGlass = (_pkg && (_pkg.LiquidGlass || _pkg.default || _pkg)) || null; - -// Acepta `children` como una prop -export default function LiquidGlassComponent({ children }) { - const containerRef = useRef(null); - const [isMobile, setIsMobile] = useState(true); // Por defecto móvil para SSR - const [isClient, setIsClient] = useState(false); - - // Detectar si estamos en el cliente y si es móvil - useEffect(() => { - setIsClient(true); - - // Detectar dispositivos móviles de manera más completa - const checkMobile = () => { - const userAgent = navigator.userAgent || navigator.vendor || window.opera; - const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent.toLowerCase()); - const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; - const isSmallScreen = window.innerWidth <= 1024; // Aumentado para tablets también - - return isMobileUA || (isTouchDevice && isSmallScreen); - }; - - setIsMobile(checkMobile()); - - // NO actualizar en resize para evitar re-renders costosos - }, []); - - // Fallback ULTRA-LIGERO para móviles - SIN backdrop-filter, SIN efectos pesados - const MobileFallback = () => ( -
- {children} -
- ); - - // Si no se encontró el componente o estamos en el servidor, renderizamos fallback - if (!LiquidGlass || !isClient) { - return ; - } - - // En móviles, usar fallback CSS ultra-ligero (sin WebGL, sin backdrop-filter) - if (isMobile) { - return ; - } - - // En desktop, usar el efecto completo pero muy optimizado - return ( - - {children} - - - ); -} \ No newline at end of file diff --git a/src/content.config.ts b/src/content.config.ts new file mode 100644 index 0000000..ec65ad2 --- /dev/null +++ b/src/content.config.ts @@ -0,0 +1,42 @@ +import { defineCollection, z } from 'astro:content'; +import { glob } from 'astro/loaders'; + +// Cada proyecto es un .md en src/content/proyectos/: los metadatos van en el +// frontmatter y el contenido de la ficha se escribe en markdown (admite +// imágenes, listas, citas…). El nombre del archivo es el id/URL del proyecto. +const proyectos = defineCollection({ + loader: glob({ pattern: '**/*.md', base: './src/content/proyectos' }), + schema: z.object({ + titulo: z.string(), + // "YYYY", "YYYY-MM" o "YYYY-MM-DD"; coerce por si YAML lo lee como número + fecha: z.coerce.string().optional(), + categorias: z.array(z.string()).default([]), + peso: z.number().optional(), + // Opcional: sin imagen la tarjeta muestra el icono de su categoría + imagen: z.string().optional(), + // Imagen grande de la cabecera de la ficha; si falta se usa `imagen` + portada: z.string().optional(), + // Vídeo "trailer" (mp4/webm) para la cabecera; sustituye a la portada + trailer: z.string().optional(), + // Orden en el hero (1 = primero); los proyectos sin destacado no flotan + destacado: z.number().optional(), + descripcion: z.string().optional(), + tecnologias: z.array(z.string()).optional(), + link: z.string().optional(), + premio: z.string().optional(), + sub: z + .array( + z.object({ + titulo: z.string(), + descripcion: z.string().optional(), + categoria: z.string().optional(), + fecha: z.coerce.string().optional(), + imagen: z.string().optional(), + link: z.string().optional(), + }), + ) + .optional(), + }), +}); + +export const collections = { proyectos }; diff --git a/src/content/proyectos/blog-personal.md b/src/content/proyectos/blog-personal.md new file mode 100644 index 0000000..22979fb --- /dev/null +++ b/src/content/proyectos/blog-personal.md @@ -0,0 +1,21 @@ +--- +titulo: Blog Personal +fecha: "2023-09" +categorias: [desarrollo-web] +peso: 45 +destacado: 6 +imagen: /assets/proyectos/Blog.png +descripcion: Blog técnico sobre audiovisual, tecnología y programación. +tecnologias: [HTML, CSS, JavaScript] +link: https://blog.pgscom.es +--- + +### Blog Técnico Personal + +Espacio dedicado a compartir conocimientos sobre **producción audiovisual** y **programación**. + +#### Temáticas: + +- Tutoriales de edición +- Guías de programación +- Consejos de diseño diff --git a/src/content/proyectos/carteleria.md b/src/content/proyectos/carteleria.md new file mode 100644 index 0000000..fd358a5 --- /dev/null +++ b/src/content/proyectos/carteleria.md @@ -0,0 +1,18 @@ +--- +titulo: Cartelería de Eventos +fecha: "2023-02" +categorias: [diseño-grafico] +peso: 45 +descripcion: Diseño de carteles para eventos culturales. +tecnologias: [Photoshop, Illustrator, InDesign] +--- + +### Cartelería + +**Material gráfico** promocional. + +#### Tipos: + +- Estrenos cinematográficos +- Eventos musicales +- Portadas diff --git a/src/content/proyectos/cortometrajes.md b/src/content/proyectos/cortometrajes.md new file mode 100644 index 0000000..bc0809d --- /dev/null +++ b/src/content/proyectos/cortometrajes.md @@ -0,0 +1,19 @@ +--- +titulo: Cortometrajes Varios +fecha: "2022" +categorias: [video-produccion] +peso: 60 +imagen: /assets/proyectos/PortfolioCortos.png +descripcion: Colección de cortometrajes de ficción y documental. +tecnologias: [DaVinci Resolve, After Effects, Audition] +--- + +### Colección de Cortometrajes + +Recopilación de proyectos audiovisuales en **ficción** y **documental**. + +#### Roles: + +- Dirección de fotografía +- Montaje +- Corrección de color diff --git a/src/content/proyectos/cpp-learning.md b/src/content/proyectos/cpp-learning.md new file mode 100644 index 0000000..b36c317 --- /dev/null +++ b/src/content/proyectos/cpp-learning.md @@ -0,0 +1,18 @@ +--- +titulo: Aprendiendo C++ +fecha: "2024-10" +categorias: [programacion] +peso: 40 +descripcion: Proyectos académicos y personales en C++. +tecnologias: [C++, CMake, Visual Studio] +--- + +### Aprendiendo C++ + +Formación en **C++ moderno**. + +#### Áreas: + +- C++17/20 +- Algoritmos +- Desarrollo de juegos diff --git "a/src/content/proyectos/dise\303\261o-web.md" "b/src/content/proyectos/dise\303\261o-web.md" new file mode 100644 index 0000000..e67478d --- /dev/null +++ "b/src/content/proyectos/dise\303\261o-web.md" @@ -0,0 +1,21 @@ +--- +titulo: UI/UX Design +fecha: "2024-06" +categorias: [diseño-grafico, desarrollo-web] +peso: 60 +destacado: 3 +imagen: /assets/proyectos/UI.png +descripcion: Diseño de interfaces para aplicaciones web y móviles. +tecnologias: [Figma, Adobe XD, Sketch] +--- + +### UI/UX Design + +**Interfaces** centradas en experiencia. + +#### Proceso: + +1. Investigación +2. Wireframing +3. Diseño visual +4. Testing diff --git a/src/content/proyectos/fotografia-eventos.md b/src/content/proyectos/fotografia-eventos.md new file mode 100644 index 0000000..4d5ac6e --- /dev/null +++ b/src/content/proyectos/fotografia-eventos.md @@ -0,0 +1,18 @@ +--- +titulo: Fotografía de Eventos +fecha: "2023-07" +categorias: [fotografia] +peso: 55 +descripcion: Cobertura fotográfica de eventos culturales. +tecnologias: [Lightroom, Photoshop, Capture One] +--- + +### Fotografía de Eventos + +Cobertura **profesional**. + +#### Servicios: + +- Eventos culturales +- Corporativos +- Premieres diff --git a/src/content/proyectos/fotografia-producto.md b/src/content/proyectos/fotografia-producto.md new file mode 100644 index 0000000..c6d818b --- /dev/null +++ b/src/content/proyectos/fotografia-producto.md @@ -0,0 +1,18 @@ +--- +titulo: Fotografía de Producto +fecha: "2024-01" +categorias: [fotografia] +peso: 50 +descripcion: Fotografía comercial de productos. +tecnologias: [Photoshop, Lightroom, Capture One] +--- + +### Fotografía de Producto + +**Fotografía comercial** para e-commerce. + +#### Servicios: + +- E-commerce +- Catálogos +- Gastronómica diff --git a/src/content/proyectos/github-contributions.md b/src/content/proyectos/github-contributions.md new file mode 100644 index 0000000..af760d7 --- /dev/null +++ b/src/content/proyectos/github-contributions.md @@ -0,0 +1,20 @@ +--- +titulo: Contribuciones Open Source +fecha: "2021" +categorias: [programacion] +peso: 65 +imagen: /assets/proyectos/Contribuciones.png +descripcion: Más de 4 años de experiencia con Git/GitHub. +tecnologias: [Git, GitHub, Python, JavaScript] +link: https://github.com/PGSCOM +--- + +### Open Source + +Participación activa en **código abierto**. + +#### Estadísticas: + +- 20+ repositorios +- 50+ estrellas +- 30+ pull requests diff --git a/src/content/proyectos/identidad-pgscom.md b/src/content/proyectos/identidad-pgscom.md new file mode 100644 index 0000000..deee1ef --- /dev/null +++ b/src/content/proyectos/identidad-pgscom.md @@ -0,0 +1,19 @@ +--- +titulo: Identidad PGSCOM +fecha: "2022-03" +categorias: [diseño-grafico] +peso: 55 +descripcion: Diseño de identidad visual completa. +tecnologias: [Illustrator, Photoshop, Figma] +--- + +### Identidad PGSCOM + +**Identidad de marca** completa. + +#### Elementos: + +- Logo vectorial +- Paleta de colores +- Tipografía +- Assets sociales diff --git a/src/content/proyectos/logo-animado-pgscom.md b/src/content/proyectos/logo-animado-pgscom.md new file mode 100644 index 0000000..33612ad --- /dev/null +++ b/src/content/proyectos/logo-animado-pgscom.md @@ -0,0 +1,18 @@ +--- +titulo: Logo Animado PGSCOM +fecha: "2023-11" +categorias: [animacion-3d] +peso: 50 +descripcion: Animación 3D del logo personal con partículas y efectos visuales. +tecnologias: [Blender, Cycles, After Effects] +--- + +### Logo Animado PGSCOM + +Animación 3D con **sistema de partículas**. + +#### Especificaciones: + +- **Render:** Cycles GPU +- **Resolución:** 4K +- **Frames:** 120+ diff --git a/src/content/proyectos/motion-graphics.md b/src/content/proyectos/motion-graphics.md new file mode 100644 index 0000000..5814655 --- /dev/null +++ b/src/content/proyectos/motion-graphics.md @@ -0,0 +1,18 @@ +--- +titulo: Motion Graphics +fecha: "2023-04" +categorias: [animacion-3d, diseño-grafico] +peso: 55 +descripcion: Piezas de motion graphics para redes sociales y proyectos comerciales. +tecnologias: [Blender, After Effects, Illustrator] +--- + +### Motion Graphics + +**Piezas animadas** para redes y publicidad. + +#### Servicios: + +- Contenido social +- Intros/outros +- Infografías animadas diff --git a/src/content/proyectos/portafolio-cine.md b/src/content/proyectos/portafolio-cine.md new file mode 100644 index 0000000..36534fc --- /dev/null +++ b/src/content/proyectos/portafolio-cine.md @@ -0,0 +1,21 @@ +--- +titulo: Portafolio Cinematográfico +fecha: "2024-02" +categorias: [desarrollo-web, video-produccion] +peso: 60 +destacado: 4 +imagen: /assets/proyectos/PortfolioCortos.png +descripcion: Galería de proyectos audiovisuales con reproductor de video integrado. +tecnologias: [HTML5, CSS3, JavaScript, Video.js] +link: https://cine.pgscom.es +--- + +### Portafolio Cinematográfico + +Galería especializada para mostrar mis **proyectos audiovisuales**. + +#### Funcionalidades: + +- Reproductor personalizado +- Sistema de categorías +- Galería de stills diff --git a/src/content/proyectos/portfolio-pgscom.md b/src/content/proyectos/portfolio-pgscom.md new file mode 100644 index 0000000..259fcd2 --- /dev/null +++ b/src/content/proyectos/portfolio-pgscom.md @@ -0,0 +1,22 @@ +--- +titulo: Portfolio PGSCOM +fecha: "2025-03" +categorias: [desarrollo-web] +peso: 90 +destacado: 1 +imagen: /assets/proyectos/portfolio.png +descripcion: Sitio web personal desarrollado con Astro, React y animaciones con AnimeJS. +tecnologias: [Astro, React, TypeScript, AnimeJS, CSS3] +link: https://pgscom.es +--- + +### Portfolio Personal PGSCOM + +Este proyecto representa mi **carta de presentación digital**, desarrollado completamente desde cero con las tecnologías más modernas. + +#### Características principales: + +- **Astro 4.0** - Framework ultra-rápido +- **React** - Componentes interactivos +- **AnimeJS** - Animaciones fluidas +- **Diseño Responsivo** diff --git a/src/content/proyectos/premio-macula-2023.md b/src/content/proyectos/premio-macula-2023.md new file mode 100644 index 0000000..20170af --- /dev/null +++ b/src/content/proyectos/premio-macula-2023.md @@ -0,0 +1,24 @@ +--- +titulo: Premio Mácula 2023 +fecha: "2023-05" +categorias: [video-produccion, programacion] +peso: 70 +imagen: /assets/proyectos/Macula.png +descripcion: Segundo Premio Mácula 2023 - Proyecto audiovisual experimental. +tecnologias: [DaVinci Resolve, Premiere Pro] +premio: 2º Premio Mácula 2023 +sub: + - titulo: Sistema de validación de entradas + descripcion: Primera versión del validador de entradas del festival. + categoria: programacion +--- + +### Segundo Premio Mácula 2023 + +Proyecto audiovisual experimental con **nuevas formas narrativas**. + +#### Características: + +- Montaje experimental +- Paleta expresionista +- Banda sonora original diff --git a/src/content/proyectos/premio-macula-2024.md b/src/content/proyectos/premio-macula-2024.md new file mode 100644 index 0000000..8cf1f0d --- /dev/null +++ b/src/content/proyectos/premio-macula-2024.md @@ -0,0 +1,34 @@ +--- +titulo: Premio Mácula 2024 +fecha: "2024-05" +categorias: [video-produccion, programacion] +peso: 80 +destacado: 2 +imagen: /assets/proyectos/Macula.png +descripcion: Segundo Premio Mácula 2024 - Cortometraje documental galardonado. +tecnologias: [DaVinci Resolve, Premiere Pro, After Effects] +premio: 2º Premio Mácula 2024 +sub: + - titulo: Sistema de validación de entradas + descripcion: Aplicación web para validar las entradas del festival en la puerta. + categoria: programacion + fecha: "2024-04" + imagen: /assets/proyectos/Herramientas.png + link: /proyectos/premio-macula-2024#sistema-validacion + - titulo: Cartelería del festival + descripcion: Diseño del cartel y material promocional de la edición. + categoria: diseño-grafico + link: /proyectos/premio-macula-2024#carteleria +--- + +### Segundo Premio Mácula 2024 + +Cortometraje documental galardonado con el **Segundo Premio**. + +#### Ficha técnica: + +- **Formato:** Documental +- **Duración:** 12 minutos +- **Postproducción:** DaVinci Resolve + +> "Trabajo excepcional" - Jurado Festival Mácula diff --git a/src/content/proyectos/python-tools.md b/src/content/proyectos/python-tools.md new file mode 100644 index 0000000..d6d820e --- /dev/null +++ b/src/content/proyectos/python-tools.md @@ -0,0 +1,20 @@ +--- +titulo: Herramientas Python +fecha: "2022-06" +categorias: [programacion] +peso: 75 +destacado: 5 +imagen: /assets/proyectos/Herramientas.png +descripcion: Scripts de automatización para proyectos audiovisuales. +6 años. +tecnologias: [Python, NumPy, OpenCV, FFmpeg] +--- + +### Herramientas Python + +**Scripts de automatización** audiovisual. + +#### Proyectos: + +- Automatización FFmpeg +- Procesamiento de metadatos +- Batch de imágenes diff --git a/src/content/proyectos/retratos.md b/src/content/proyectos/retratos.md new file mode 100644 index 0000000..b7f904a --- /dev/null +++ b/src/content/proyectos/retratos.md @@ -0,0 +1,18 @@ +--- +titulo: Retratos +fecha: "2022-09" +categorias: [fotografia] +peso: 40 +descripcion: Sesiones de retrato profesional. +tecnologias: [Lightroom, Photoshop] +--- + +### Retratos + +**Retrato profesional**. + +#### Tipos: + +- Corporativos +- Headshots +- Familiares diff --git a/src/content/proyectos/visuales-unreal.md b/src/content/proyectos/visuales-unreal.md new file mode 100644 index 0000000..48eab37 --- /dev/null +++ b/src/content/proyectos/visuales-unreal.md @@ -0,0 +1,18 @@ +--- +titulo: Entornos Virtuales +fecha: "2024-08" +categorias: [animacion-3d] +peso: 70 +descripcion: Creación de entornos virtuales fotorrealistas en Unreal Engine. +tecnologias: [Unreal Engine, Blender, Substance Painter] +--- + +### Entornos Virtuales + +Escenarios **fotorrealistas** en Unreal Engine 5. + +#### Tecnologías: + +- Lumen y Nanite +- Texturizado PBR +- Modelado en Blender diff --git a/src/data/categorias.json b/src/data/categorias.json new file mode 100644 index 0000000..49a7ddf --- /dev/null +++ b/src/data/categorias.json @@ -0,0 +1,46 @@ +{ + "categorias": [ + { + "id": "desarrollo-web", + "titulo": "Desarrollo Web", + "logo": "/icons/web.svg", + "color": "#4da3ff", + "descripcion": "Desarrollo de sitios y aplicaciones web con tecnologías modernas" + }, + { + "id": "video-produccion", + "titulo": "Producción Audiovisual", + "logo": "/icons/video.svg", + "color": "#ff7a4d", + "descripcion": "Cortometrajes, documentales y proyectos cinematográficos" + }, + { + "id": "animacion-3d", + "titulo": "Animación 3D", + "logo": "/icons/animation.svg", + "color": "#9a8cff", + "descripcion": "Modelado, animación y renders con Blender y Unreal Engine" + }, + { + "id": "programacion", + "titulo": "Programación", + "logo": "/icons/code.svg", + "color": "#34d39b", + "descripcion": "Aplicaciones y scripts en Python, C++ y más" + }, + { + "id": "diseño-grafico", + "titulo": "Diseño Gráfico", + "logo": "/icons/design.svg", + "color": "#ff6fb3", + "descripcion": "Identidad visual, carteles y diseño digital" + }, + { + "id": "fotografia", + "titulo": "Fotografía", + "logo": "/icons/photo.svg", + "color": "#ffc94d", + "descripcion": "Fotografía digital y retoque profesional" + } + ] +} diff --git a/src/data/proyectos.json b/src/data/proyectos.json deleted file mode 100644 index db5b53a..0000000 --- a/src/data/proyectos.json +++ /dev/null @@ -1,200 +0,0 @@ -[ - { - "id": "desarrollo-web", - "titulo": "Desarrollo Web", - "logo": "/icons/web.svg", - "descripcion": "Desarrollo de sitios y aplicaciones web con tecnologías modernas", - "proyectos": [ - { - "id": "portfolio-pgscom", - "titulo": "Portfolio PGSCOM", - "imagen": "/assets/proyectos/portfolio.png", - "descripcion": "Sitio web personal desarrollado con Astro, React y animaciones con AnimeJS.", - "tecnologias": ["Astro", "React", "TypeScript", "AnimeJS", "CSS3"], - "link": "https://pgscom.es", - "contenido": "

Portfolio Personal PGSCOM

Este proyecto representa mi carta de presentación digital, desarrollado completamente desde cero con las tecnologías más modernas.

Características principales:

  • Astro 4.0 - Framework ultra-rápido
  • React - Componentes interactivos
  • AnimeJS - Animaciones fluidas
  • Diseño Responsivo
" - }, - { - "id": "blog-personal", - "titulo": "Blog Personal", - "imagen": "/assets/proyectos/Blog.png", - "descripcion": "Blog técnico sobre audiovisual, tecnología y programación.", - "tecnologias": ["HTML", "CSS", "JavaScript"], - "link": "https://blog.pgscom.es", - "contenido": "

Blog Técnico Personal

Espacio dedicado a compartir conocimientos sobre producción audiovisual y programación.

Temáticas:

  • Tutoriales de edición
  • Guías de programación
  • Consejos de diseño
" - }, - { - "id": "portafolio-cine", - "titulo": "Portafolio Cinematográfico", - "imagen": "/assets/proyectos/PortfolioCortos.png", - "descripcion": "Galería de proyectos audiovisuales con reproductor de video integrado.", - "tecnologias": ["HTML5", "CSS3", "JavaScript", "Video.js"], - "link": "https://cine.pgscom.es", - "contenido": "

Portafolio Cinematográfico

Galería especializada para mostrar mis proyectos audiovisuales.

Funcionalidades:

  • Reproductor personalizado
  • Sistema de categorías
  • Galería de stills
" - } - ] - }, - { - "id": "video-produccion", - "titulo": "Producción Audiovisual", - "logo": "/icons/video.svg", - "descripcion": "Cortometrajes, documentales y proyectos cinematográficos", - "proyectos": [ - { - "id": "premio-macula-2024", - "titulo": "Premio Mácula 2024 ", - "imagen": "/assets/proyectos/Macula.png", - "descripcion": "Segundo Premio Mácula 2024 - Cortometraje documental galardonado.", - "tecnologias": ["DaVinci Resolve", "Premiere Pro", "After Effects"], - "premio": "2º Premio Mácula 2024", - "contenido": "

Segundo Premio Mácula 2024

Cortometraje documental galardonado con el Segundo Premio.

Ficha técnica:

  • Formato: Documental
  • Duración: 12 minutos
  • Postproducción: DaVinci Resolve
\"Trabajo excepcional\" - Jurado Festival Mácula
" - }, - { - "id": "premio-macula-2023", - "titulo": "Premio Mácula 2023 ", - "imagen": "/assets/proyectos/Macula.png", - "descripcion": "Segundo Premio Mácula 2023 - Proyecto audiovisual experimental.", - "tecnologias": ["DaVinci Resolve", "Premiere Pro"], - "premio": "2º Premio Mácula 2023", - "contenido": "

Segundo Premio Mácula 2023

Proyecto audiovisual experimental con nuevas formas narrativas.

Características:

  • Montaje experimental
  • Paleta expresionista
  • Banda sonora original
" - }, - { - "id": "cortometrajes", - "titulo": "Cortometrajes Varios", - "imagen": "/assets/proyectos/PortfolioCortos.png", - "descripcion": "Colección de cortometrajes de ficción y documental.", - "tecnologias": ["DaVinci Resolve", "After Effects", "Audition"], - "contenido": "

Colección de Cortometrajes

Recopilación de proyectos audiovisuales en ficción y documental.

Roles:

  • Dirección de fotografía
  • Montaje
  • Corrección de color
" - } - ] - }, - { - "id": "animacion-3d", - "titulo": "Animación 3D", - "logo": "/icons/animation.svg", - "descripcion": "Modelado, animación y renders con Blender y Unreal Engine", - "proyectos": [ - { - "id": "logo-animado-pgscom", - "titulo": "Logo Animado PGSCOM", - "imagen": "/assets/proyectos/logo-anim.jpg", - "descripcion": "Animación 3D del logo personal con partículas y efectos visuales.", - "tecnologias": ["Blender", "Cycles", "After Effects"], - "contenido": "

Logo Animado PGSCOM

Animación 3D con sistema de partículas.

Especificaciones:

  • Render: Cycles GPU
  • Resolución: 4K
  • Frames: 120+
" - }, - { - "id": "visuales-unreal", - "titulo": "Entornos Virtuales", - "imagen": "/assets/proyectos/unreal.jpg", - "descripcion": "Creación de entornos virtuales fotorrealistas en Unreal Engine.", - "tecnologias": ["Unreal Engine", "Blender", "Substance Painter"], - "contenido": "

Entornos Virtuales

Escenarios fotorrealistas en Unreal Engine 5.

Tecnologías:

  • Lumen y Nanite
  • Texturizado PBR
  • Modelado en Blender
" - }, - { - "id": "motion-graphics", - "titulo": "Motion Graphics", - "imagen": "/assets/proyectos/motion.jpg", - "descripcion": "Piezas de motion graphics para redes sociales y proyectos comerciales.", - "tecnologias": ["Blender", "After Effects", "Illustrator"], - "contenido": "

Motion Graphics

Piezas animadas para redes y publicidad.

Servicios:

  • Contenido social
  • Intros/outros
  • Infografías animadas
" - } - ] - }, - { - "id": "programacion", - "titulo": "Proyectos de Programación", - "logo": "/icons/code.svg", - "descripcion": "Aplicaciones y scripts en Python, C++ y más", - "proyectos": [ - { - "id": "github-contributions", - "titulo": "Contribuciones Open Source", - "imagen": "/assets/proyectos/github.jpg", - "descripcion": "Más de 4 años de experiencia con Git/GitHub.", - "tecnologias": ["Git", "GitHub", "Python", "JavaScript"], - "link": "https://github.com/PGSCOM", - "contenido": "

Open Source

Participación activa en código abierto.

Estadísticas:

  • 20+ repositorios
  • 50+ estrellas
  • 30+ pull requests
" - }, - { - "id": "python-tools", - "titulo": "Herramientas Python", - "imagen": "/assets/proyectos/Herramientas.png", - "descripcion": "Scripts de automatización para proyectos audiovisuales. +6 años.", - "tecnologias": ["Python", "NumPy", "OpenCV", "FFmpeg"], - "contenido": "

Herramientas Python

Scripts de automatización audiovisual.

Proyectos:

  • Automatización FFmpeg
  • Procesamiento de metadatos
  • Batch de imágenes
" - }, - { - "id": "cpp-learning", - "titulo": "Aprendiendo C++", - "imagen": "/assets/proyectos/cpp.jpg", - "descripcion": "Proyectos académicos y personales en C++.", - "tecnologias": ["C++", "CMake", "Visual Studio"], - "contenido": "

Aprendiendo C++

Formación en C++ moderno.

Áreas:

  • C++17/20
  • Algoritmos
  • Desarrollo de juegos
" - } - ] - }, - { - "id": "diseño-grafico", - "titulo": "Diseño Gráfico", - "logo": "/icons/design.svg", - "descripcion": "Identidad visual, carteles y diseño digital", - "proyectos": [ - { - "id": "identidad-pgscom", - "titulo": "Identidad PGSCOM", - "imagen": "/assets/proyectos/branding.jpg", - "descripcion": "Diseño de identidad visual completa.", - "tecnologias": ["Illustrator", "Photoshop", "Figma"], - "contenido": "

Identidad PGSCOM

Identidad de marca completa.

Elementos:

  • Logo vectorial
  • Paleta de colores
  • Tipografía
  • Assets sociales
" - }, - { - "id": "carteleria", - "titulo": "Cartelería de Eventos", - "imagen": "/assets/proyectos/posters.jpg", - "descripcion": "Diseño de carteles para eventos culturales.", - "tecnologias": ["Photoshop", "Illustrator", "InDesign"], - "contenido": "

Cartelería

Material gráfico promocional.

Tipos:

  • Estrenos cinematográficos
  • Eventos musicales
  • Portadas
" - }, - { - "id": "diseño-web", - "titulo": "UI/UX Design", - "imagen": "/assets/proyectos/UI.png", - "descripcion": "Diseño de interfaces para aplicaciones web y móviles.", - "tecnologias": ["Figma", "Adobe XD", "Sketch"], - "contenido": "

UI/UX Design

Interfaces centradas en experiencia.

Proceso:

  1. Investigación
  2. Wireframing
  3. Diseño visual
  4. Testing
" - } - ] - }, - { - "id": "fotografia", - "titulo": "Fotografía", - "logo": "/icons/photo.svg", - "descripcion": "Fotografía digital y retoque profesional", - "proyectos": [ - { - "id": "fotografia-eventos", - "titulo": "Fotografía de Eventos", - "imagen": "/assets/proyectos/eventos.jpg", - "descripcion": "Cobertura fotográfica de eventos culturales.", - "tecnologias": ["Lightroom", "Photoshop", "Capture One"], - "contenido": "

Fotografía de Eventos

Cobertura profesional.

Servicios:

  • Eventos culturales
  • Corporativos
  • Premieres
" - }, - { - "id": "retratos", - "titulo": "Retratos", - "imagen": "/assets/proyectos/retratos.jpg", - "descripcion": "Sesiones de retrato profesional.", - "tecnologias": ["Lightroom", "Photoshop"], - "contenido": "

Retratos

Retrato profesional.

Tipos:

  • Corporativos
  • Headshots
  • Familiares
" - }, - { - "id": "fotografia-producto", - "titulo": "Fotografía de Producto", - "imagen": "/assets/proyectos/producto.jpg", - "descripcion": "Fotografía comercial de productos.", - "tecnologias": ["Photoshop", "Lightroom", "Capture One"], - "contenido": "

Fotografía de Producto

Fotografía comercial para e-commerce.

Servicios:

  • E-commerce
  • Catálogos
  • Gastronómica
" - } - ] - } -] diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 8d2af9f..e02e1df 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -1,3 +1,16 @@ +--- +const { + title = 'PGSCOM — Pablo García Santamaría', + description = 'Portfolio de Pablo García Santamaría: proyectos de desarrollo web, producción audiovisual, animación 3D, programación, diseño gráfico y fotografía.', + image = '/assets/proyectos/portfolio.png', + type = 'website', + noindex = false, +} = Astro.props; + +const canonical = new URL(Astro.url.pathname, Astro.site ?? Astro.url.origin).href; +const ogImage = new URL(image, Astro.site ?? Astro.url.origin).href; +--- + @@ -5,8 +18,67 @@ - Astro Basics - + + + {title} + + + {noindex && } + + + + + + + + + + + + + + + + + + + @@ -25,10 +97,9 @@ + - - - + @@ -38,7 +109,7 @@ diff --git a/src/pages/_styles.css b/src/pages/_styles.css index a9a0dfa..80996a9 100644 --- a/src/pages/_styles.css +++ b/src/pages/_styles.css @@ -1,229 +1,365 @@ /* Reset y estilos base */ + * { margin: 0; padding: 0; box-sizing: border-box; } -.parte2-wrapper { - position: relative; - display: block; - width: 100%; - z-index: 50; - contain: paint style; +/* ============================================ + PRELOADER — la P de carga.svg como máscara + ============================================ */ + +#preloader { + position: fixed; + inset: 0; + z-index: 9999; + background: #f5f5f7; + display: grid; + place-items: center; + overflow: hidden; } -.parte2 { - /* Sticky para mantener el bloque en la parte superior mientras se hace scroll */ - position: -webkit-sticky; - position: sticky; - top: 0; - z-index: 50; +.pl-svg { + width: min(56vmin, 420px); + height: min(56vmin, 420px); + /* Origen del zoom: centro real del glifo de la P. + BBox de la P en el viewBox 600: x[185,427] y[93,456] → centro (306,275) + → (306/600, 275/600) = (51%, 45.8%). Así crece desde su propio centro. */ + transform-origin: 51% 45.8%; + will-change: transform; +} - /* Containing block para .galaxiamask */ - overflow: visible; /* permitir que el video y máscara se muestren correctamente */ +/* Guía tenue de la forma completa, siempre visible bajo el trazo */ +.pl-outline { + fill: none; + stroke: rgba(0, 0, 0, 0.12); + stroke-width: 2; +} - /* Estilos de layout por defecto; si hay inline styles se pueden sobreescribir */ +/* Trazo que construye la P (DrawSVG, ligado al progreso de carga). + Oculto hasta que el JS fija drawSVG:0% para evitar ver el trazo completo. */ +.pl-draw { + fill: none; + stroke-width: 7; + stroke-linecap: round; + stroke-linejoin: round; + visibility: hidden; +} + +/* Relleno con degradado que aparece al completar la carga */ +.pl-fill { + opacity: 0; +} + +/* ============================================ + HERO — landing oscura con proyectos flotando + ============================================ */ + +.main-container { + position: relative; + width: 100vw; height: 100vh; - width: 100%; /* Asegurar ancho completo */ + overflow: hidden; display: flex; align-items: center; justify-content: center; + background: + radial-gradient(60vw 45vh at 72% 18%, rgba(37, 99, 235, 0.20), transparent 60%), + radial-gradient(50vw 40vh at 18% 78%, rgba(255, 90, 205, 0.15), transparent 60%), + radial-gradient(45vw 35vh at 50% 50%, rgba(54, 126, 120, 0.18), transparent 65%); } -.galaxiamask { - /* Contenedor del video: ocupa todo el area de .parte2 */ +/* Retícula de puntos sutil, desvanecida hacia los bordes */ +.main-container::before { + content: ''; position: absolute; - inset: 0; /* top:0; right:0; bottom:0; left:0; */ - width: 100%; - height: 100%; - overflow: hidden; - pointer-events: none; /* No bloquear interacciones */ - - /* Máscara cuadrada */ - -webkit-mask-image: linear-gradient(#000, #000); - mask-image: linear-gradient(#000, #000); - -webkit-mask-size: 38vh 38vh; /* Convertido a cuadrado y tamaño inicial subido */ - mask-size: 38vh 38vh; - -webkit-mask-position: center; /* Centrado horizontal y vertical */ - mask-position: center; - -webkit-mask-repeat: no-repeat; /* Evitar que la máscara se repita */ - mask-repeat: no-repeat; - - will-change: mask-size, -webkit-mask-size; - - /* Imagen inicial de fondo para evitar parpadeo o vacío antes de cargar la secuencia */ - background-image: url(/vid/empezar/000.png); - background-size: cover; - background-position: center; - background-repeat: no-repeat; + inset: 0; + background-image: radial-gradient(rgba(0, 0, 0, 0.05) 1px, transparent 1.5px); + background-size: 34px 34px; + -webkit-mask-image: radial-gradient(ellipse at center, black 25%, transparent 78%); + mask-image: radial-gradient(ellipse at center, black 25%, transparent 78%); + pointer-events: none; } -.video-galaxia { - /* Llenar el contenedor .galaxiamask sin afectar al layout */ +/* Fundido inferior: de transparente al color de parte2-wrapper */ +.main-container::after { + content: ''; position: absolute; - top: 0; + bottom: 0; left: 0; width: 100%; - height: 100%; - object-fit: cover; - z-index: 0; /* Queda dentro de .parte2; el logo tendrá z-index mayor */ + height: 30vh; + background: linear-gradient(to bottom, transparent, #f5f7fa); pointer-events: none; } -.parte2 > img { - /* Asegurar que el logo quede por encima del video y no cambie su posición */ +/* Contenido central */ +.content { position: relative; - z-index: 1; + z-index: 2; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: clamp(10px, 1.6vh, 20px); + text-align: center; + padding: 0 20px; + pointer-events: none; } -/*Logo flotante*/ -.sky-image { - position: fixed; - top: 25px; - left: 25px; - z-index: 9999; +.content a { + pointer-events: auto; } -/* Contenedor principal */ -.main-container { - width: 100vw; - height: 100vh; - /*background: white;*/ +.hero-kicker { + font-family: 'Chakra Petch', sans-serif; + font-size: clamp(0.7rem, 1.1vw, 0.85rem); + letter-spacing: 0.32em; + text-transform: uppercase; + color: rgba(0, 0, 0, 0.5); +} + +.hero-sub { + font-family: 'Chakra Petch', sans-serif; + font-size: clamp(0.9rem, 1.4vw, 1.1rem); + color: rgba(0, 0, 0, 0.6); + max-width: 560px; + line-height: 1.6; +} + +.hero-actions { display: flex; - align-items: center; - justify-content: flex-start; - overflow: visible; /* Cambiado de hidden a visible para permitir scroll */ - z-index: -2; + gap: 14px; + margin-top: clamp(6px, 1.2vh, 14px); + flex-wrap: wrap; + justify-content: center; } -/* Eliminamos el overlay global */ +.hero-cta { + font-family: 'Chakra Petch', sans-serif; + font-size: 0.9rem; + font-weight: 600; + letter-spacing: 0.06em; + color: #fff; + background: #1a1a2e; + border: 1px solid #1a1a2e; + border-radius: 999px; + padding: 13px 30px; + text-decoration: none; + transition: transform 0.25s ease, box-shadow 0.25s ease, background 0.25s ease, color 0.25s ease; +} -/* Contenido principal - lado izquierdo */ -.content { +.hero-cta:hover { + transform: translateY(-2px); + box-shadow: 0 10px 35px rgba(0, 0, 0, 0.15); +} + +.hero-cta--ghost { + background: transparent; + color: rgba(0, 0, 0, 0.8); + border-color: rgba(0, 0, 0, 0.25); +} + +.hero-cta--ghost:hover { + border-color: rgba(0, 0, 0, 0.5); + box-shadow: 0 10px 35px rgba(0, 0, 0, 0.06); +} + +/* Indicador de scroll */ +.hero-scroll-hint { + position: absolute; + bottom: clamp(18px, 4vh, 40px); + left: 50%; + transform: translateX(-50%); z-index: 2; - text-align: center; - color: #367e78; - padding: 0; - background: transparent; /* Sin fondo para seguir el boceto */ - animation: fadeInLeft 1s ease-out; - max-width: 60%; display: flex; flex-direction: column; - justify-content: center; - height: 100%; - width: 48%; align-items: center; + gap: 8px; + font-family: 'Chakra Petch', sans-serif; + font-size: 0.65rem; + letter-spacing: 0.3em; + text-transform: uppercase; + color: rgba(0, 0, 0, 0.4); + pointer-events: none; } -/* Contenedor de la imagen */ -.image-container { - position: fixed; - top: 0; - right: 5%; - z-index: -1; - flex: 1; - display: flex; - justify-content: flex-end; - align-items: center; - width: 40%; - height: 100vh; +.hero-scroll-hint::before { + content: ''; + width: 1px; + height: 34px; + background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.4)); + animation: scroll-hint-drop 2s ease-in-out infinite; +} + +@keyframes scroll-hint-drop { + 0% { transform: scaleY(0); transform-origin: top; } + 45% { transform: scaleY(1); transform-origin: top; } + 55% { transform: scaleY(1); transform-origin: bottom; } + 100% { transform: scaleY(0); transform-origin: bottom; } +} + +/* ── Cards de proyectos flotando ── */ + +.hero-cards { + position: absolute; + inset: 0; + z-index: 1; +} + +.hero-card { + position: absolute; + display: block; + width: clamp(150px, 16vw, 250px); + text-decoration: none; + will-change: transform; + cursor: pointer; +} + +.hero-card--lg { + width: clamp(180px, 19vw, 300px); + z-index: 3; +} + +.hero-card--sm { + width: clamp(110px, 11vw, 180px); +} + +.hero-card.is-far img { + filter: brightness(0.9) saturate(0.85); +} + +.hero-card-tilt { + display: block; + will-change: transform; +} + +.hero-card-media { + position: relative; + display: block; + border-radius: 14px; overflow: hidden; - will-change: transform, opacity; + background: #e8ecf0; + box-shadow: + 0 24px 60px rgba(0, 0, 0, 0.12), + 0 0 0 1px rgba(0, 0, 0, 0.06); + transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.35s ease; } -/* Estilo para la imagen de personaje */ -.fotopgs { +.hero-card:hover .hero-card-media { + transform: scale(1.05); + box-shadow: + 0 30px 70px rgba(0, 0, 0, 0.15), + 0 0 0 1px rgba(0, 0, 0, 0.1), + 0 0 45px rgba(37, 99, 235, 0.1); +} + +.hero-card-media img { display: block; - max-height: 80vh; - max-width: 100%; - object-fit: contain; - /*filter: drop-shadow(2px 2px 10px rgba(0, 0, 0, 0.1));*/ - /* máscara aplicada por .image-wrapper::after (ya no se usa) */ + width: 100%; + aspect-ratio: 16 / 10; + object-fit: cover; } -/* Partículas flotantes */ -.content::after { - content: ''; +.hero-card-label { position: absolute; - width: 100px; - height: 100px; - background: radial-gradient(circle, rgba(65, 105, 225, 0.1) 0%, transparent 70%); - border-radius: 50%; - transform: translate(-50%, -50%); - animation: float 4s ease-in-out infinite; - z-index: -1; - pointer-events: none; + inset: auto 0 0 0; + display: flex; + flex-direction: column; + gap: 2px; + padding: 24px 12px 10px; + background: linear-gradient(to top, rgba(255, 255, 255, 0.95), transparent); + opacity: 0; + translate: 0 8px; + transition: opacity 0.3s ease, translate 0.3s ease; } -@keyframes float { - 0%, 100% { - transform: translate(-50%, -50%) scale(1); - opacity: 0.3; - } - 50% { - transform: translate(-50%, -60%) scale(1.2); - opacity: 0.6; - } +.hero-card:hover .hero-card-label { + opacity: 1; + translate: 0 0; +} + +.hero-card-label strong { + font-family: 'Chakra Petch', sans-serif; + font-size: 0.8rem; + color: #111; + line-height: 1.2; +} + +.hero-card-label em { + font-family: 'Chakra Petch', sans-serif; + font-style: normal; + font-size: 0.65rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: rgba(0, 0, 0, 0.5); +} + +/* ============================================ + LOGO / TIPOGRAFÍA ANIMADA + ============================================ */ + +#logo-wrapper { + display: inline-flex; + justify-content: center; + align-items: center; + will-change: transform, opacity; + min-height: 13vw; } -/* Texto principal del logo */ .logo-text { - font-size: 7cqw; /*Tamaño base*/ + font-size: 7cqw; font-weight: bold; - /* color: #367e78; Reemplazado por gradiente */ text-align: center; letter-spacing: 0.3rem; line-height: 1.2; position: relative; - display: inline-block; + display: flex !important; + justify-content: center; + align-items: center; font-family: 'Montserrat', sans-serif; - /* Propiedades para mantener tamaño consistente */ width: 100%; - min-height: 1.5em; /* Altura mínima consistente */ - height: auto; - padding: 0 10px; /* Añadimos margen horizontal */ - box-sizing: border-box; /* Para que el padding no afecte al ancho total */ + padding: 0 10px; + box-sizing: border-box; + animation: none !important; + color: inherit; + background: none; + -webkit-background-clip: initial; +} - /* Animación de entrada y gradiente */ +/* El degradado multicolor viaja sobre las letras para evitar que desaparezcan */ +#letter-p, #letters-gscom { background-image: linear-gradient( 60deg, - /* Fase 1: Arcoíris vibrante */ #2563eb, #ff5acd, #fbda61, #ff5acd, #2563eb, - /* Fase 2: Azul verdoso (Ciclo 1) */ #2a6b66, #367e78, #4a9d94, #367e78, #2a6b66, - /* Fase 3: Azul verdoso (Ciclo 2 para loop infinito) */ #2a6b66, #367e78, #4a9d94, #367e78, #2a6b66 ); background-size: 300%; -webkit-background-clip: text; background-clip: text; color: transparent; - - animation: - pgscom-entrance 1.5s cubic-bezier(0.34, 1.56, 0.64, 1) 0s 1 normal, - pgscom-intro-scroll 2s linear forwards, - pgscom-loop-scroll 4s linear 2s infinite; -} - -@keyframes pgscom-entrance { - 0% { - opacity: 0; - transform: perspective(800px) translate3d(0, 0, 400px) rotateY(-40deg) scale(0.5); - filter: blur(10px); - } - 50% { - opacity: 1; - transform: perspective(800px) translate3d(0, 0, 0) rotateY(0deg) scale(1.1); - filter: blur(0); - } - 70% { - transform: perspective(800px) translate3d(0, 0, 0) rotateY(0deg) scale(0.95); - } - 100% { - opacity: 1; - transform: perspective(800px) translate3d(0, 0, 0) rotateY(0deg) scale(1); - } +} + +#letter-p { + display: inline-block; + will-change: transform, opacity; + transform-origin: center; +} + +#letters-gscom-wrap { + display: inline-flex; + overflow: hidden; + white-space: pre; + will-change: width, opacity; +} + +#letters-gscom { + display: inline-block; + padding-right: 0.05em; } @keyframes pgscom-intro-scroll { @@ -236,348 +372,158 @@ 100% { background-position: 100% 50%; } } -/* Eliminamos la animación anterior pgscom-gradient ya que se reemplaza por intro/loop */ - -/*.logo-text::before { - content: 'PGSCOM'; - position: absolute; - top: 0; - left: 0; - color: rgba(255, 255, 255, 0.1); - font-size: inherit; - font-family: inherit; - font-weight: inherit; - letter-spacing: inherit; - text-shadow: none; - z-index: -1; - transform: translate(3px, 3px); - animation: shimmer 2s ease-in-out infinite; -}*/ - -/* Subtítulo */ -.subtitle { - font-size: 1.4rem; - color: rgba(0, 0, 0, 0.7); - margin-top: 0.2rem; - font-weight: 300; - letter-spacing: 0.1rem; - margin-left: 0.5rem; -} - -/* Tagline adicional */ -.tagline { - font-size: 0.9rem; - color: rgba(0, 0, 0, 0.7); - margin-top: 0.5rem; - font-weight: 400; - letter-spacing: 0.15rem; - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1); - opacity: 0.9; +/* Estados de animación del logo */ +.logo-text.loading { + opacity: 0.7; } -/* Sistema de partículas minimalistas SVG */ -.particles { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - z-index: 1; - overflow: hidden; +.logo-text.loaded { + opacity: 1; } -.particle { - position: absolute; +/* Transición suave entre fuentes con fade y blur */ +.logo-text.fade-out { opacity: 0; - animation: floatParticle 15s ease-in-out infinite; - filter: none; - box-shadow: none; - will-change: transform, opacity; + filter: blur(6px); + transform: scale(0.92); + transition: opacity 0.6s ease-in-out, + filter 0.6s ease-in-out, + transform 0.6s ease-in-out; } -.particle-1 { - top: 20%; - left: 15%; - width: 50px; - height: 50px; - animation-delay: 0s; - animation-duration: 12s; -} - -.particle-2 { - top: 60%; - left: 25%; - width: 80px; - height: 80px; - animation-delay: 3s; - animation-duration: 15s; -} - -.particle-3 { - top: 40%; - left: 10%; - width: 35px; - height: 35px; - animation-delay: 6s; - animation-duration: 10s; -} - -.particle-4 { - top: 70%; - left: 35%; - width: 45px; - height: 45px; - animation-delay: 2s; - animation-duration: 14s; -} - -.particle-5 { - top: 30%; - left: 28%; - width: 75px; - height: 75px; - animation-delay: 5s; - animation-duration: 32.2s; -} - -.particle-6 { - top: 50%; - left: 20%; - width: 30px; - height: 30px; - animation-delay: 1s; - animation-duration: 8s; -} - -@keyframes floatParticle { - 0% { - transform: translateY(0) scale(0.8) rotate(0deg); - opacity: 0; - } - 10% { - opacity: 0.5; - } - 50% { - opacity: 0.7; - transform: translateY(-120px) scale(1) rotate(180deg); - } - 90% { - opacity: 0.5; - } - 100% { - transform: translateY(-240px) scale(0.8) rotate(360deg); - opacity: 0; - } +.logo-text.fade-in { + opacity: 1; + filter: blur(0px); + transform: scale(1); + transition: opacity 0.8s ease-out, + filter 0.8s ease-out, + transform 0.8s ease-out; } -/* Animaciones para el logo-text */ -@keyframes fadeInLeft { - from { - opacity: 0; - transform: translateX(-30px); - } - to { - opacity: 1; - transform: translateX(0); - } -} +/* ============================================ + SECCIÓN GALAXIA (scroll + vídeo enmascarado) + ============================================ */ -@keyframes gradientShift { - 0%, 100% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } +.parte2-wrapper { + position: relative; + display: block; + width: 100%; + z-index: 50; + background: #f5f7fa; } -@keyframes shimmer { - 0%, 100% { - opacity: 0.1; - } - 50% { - opacity: 0.3; - } +.parte2 { + /* Sticky para mantener el bloque en la parte superior mientras se hace scroll */ + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 50; + overflow: visible; + height: 100vh; + width: 100%; + display: flex; + align-items: center; + justify-content: center; } -/* Estilos para el contenedor del logo */ -.logo-container { - display: inline-block; +.galaxiamask { position: absolute; - min-height: 120px; /* Altura mínima fija para evitar saltos */ - min-width: 300px; /* Ancho mínimo para evitar redimensionamientos bruscos */ - overflow: hidden; /* Evita que el contenido se salga durante las transiciones */ - width: 100vh; + inset: 0; + width: 100%; height: 100%; - right: 50%; - left: 50%; + overflow: hidden; + pointer-events: none; + + /* Máscara cuadrada */ + -webkit-mask-image: linear-gradient(#000, #000); + mask-image: linear-gradient(#000, #000); + -webkit-mask-size: 38vh 38vh; + mask-size: 38vh 38vh; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; } -/* Estados de animación del logo */ -.logo-text.loading { - opacity: 0.7; +.video-galaxia { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + z-index: 0; + pointer-events: none; } -.logo-text.loaded { - opacity: 1; +.parte2 > img { + position: relative; + z-index: 1; } -/* Transición suave entre fuentes con fade y blur */ -.logo-text.fade-out { - opacity: 0; - filter: blur(6px); - transform: scale(0.92); - transition: opacity 0.6s ease-in-out, - filter 0.6s ease-in-out, - transform 0.6s ease-in-out; +.scroll-spacer { + height: 120vh; + display: flex; + align-items: center; + justify-content: center; + position: relative; + z-index: 60; } -.logo-text.fade-in { - opacity: 1; - filter: blur(0px); - transform: scale(1); - transition: opacity 0.8s ease-out, - filter 0.8s ease-out, - transform 0.8s ease-out; +/* ============================================ + ÓRBITA DE CATEGORÍAS (sección proyectos) + ============================================ */ + +.proyectos-grid { + position: absolute; + inset: 0; } -/* Efectos hover */ -/*.content:hover { - transform: translateY(-5px); - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4); - transition: all 0.3s ease; -}*/ +.proyectos-grid .proyecto-card { + will-change: transform; +} -/*.logo-text:hover { - transform: scale(1.05); - transition: transform 0.3s ease; -}*/ +/* ============================================ + RESPONSIVE + ============================================ */ -/* Responsive design */ @media (max-width: 768px) { - .main-container { - flex-direction: column; - padding: 1rem; - justify-content: flex-start; - } - - .content { - max-width: 100%; - width: 100%; - margin-right: 0; - margin-bottom: 0; - text-align: center; - position: absolute; - height: auto; - top: 15%; - padding-top: 2rem; - padding-left: 20px; - padding-right: 20px; - align-items: stretch; /* permitir que hijos ocupen ancho completo */ - } - - .image-container { - max-width: 100%; - width: 100%; - height: auto; - right: auto; - overflow: visible; - justify-content: center; - /*bottom: 17%;*/ - top: 30%; - } - - .fotopgs { - max-height: 60vh; - margin: 0 auto; - } - - .logo-container { - min-height: 80px; - min-width: 200px; - width: 100%; - display: flex; - justify-content: center; - } - .logo-text { font-size: 10cqw; letter-spacing: 0.2rem; - text-align: center; - width: 100%; - padding: 0 15px; - box-sizing: border-box; - display: block; /* asegurar que el elemento ocupe ancho completo */ - } - - .subtitle { - font-size: 1rem; - } - - .tagline { - font-size: 0.8rem; - letter-spacing: 0.1rem; - } - - /* Ajustes partículas para móviles */ - .particles { - width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; - } - - /* Mostrar todas las partículas */ - .particle-1, .particle-2, .particle-3, .particle-4, .particle-5, .particle-6 { - display: block; } - - /* Reposicionar las partículas para que ocupen toda la pantalla */ - .particle-1 { - top: 10%; - left: 10%; - width: 40px; - height: 40px; + + #logo-wrapper { + min-height: 26vw; } - - .particle-2 { - top: 30%; - right: 15%; - left: auto; - width: 60px; - height: 60px; + + .hero-card { + width: clamp(105px, 26vw, 150px); } - - .particle-3 { - top: 50%; - left: 20%; - width: 30px; - height: 30px; + + .hero-card--lg { + width: clamp(120px, 30vw, 170px); } - - .particle-4 { - top: 20%; - right: 20%; - left: auto; - width: 35px; - height: 35px; + + /* Las cards lejanas sobran en pantallas pequeñas */ + .hero-card.is-far { + display: none; } - - .particle-5 { - top: 70%; - left: 15%; - width: 45px; - height: 45px; + + /* Posiciones pensadas para no tapar el texto central */ + .hero-card:nth-child(1) { left: 20% !important; top: 16% !important; } + .hero-card:nth-child(2) { left: 80% !important; top: 13% !important; } + .hero-card:nth-child(3) { left: 79% !important; top: 84% !important; } + .hero-card:nth-child(4) { left: 21% !important; top: 86% !important; } + + .hero-sub { + font-size: 0.85rem; + max-width: 320px; } - - .particle-6 { - top: 85%; - right: 25%; - left: auto; - width: 30px; - height: 30px; + + .hero-cta { + padding: 11px 22px; + font-size: 0.8rem; } } @@ -585,88 +531,9 @@ .logo-text { font-size: 12cqw; letter-spacing: 0.1rem; - width: 100%; - padding: 0; - box-sizing: border-box; - } - - .subtitle { - font-size: 0.9rem; - } - - .tagline { - font-size: 0.7rem; - letter-spacing: 0.05rem; } - - .content { - padding: 1rem 20px; - margin: 0; - top: 14%; - width: 100%; - } - - .fotopgs { - max-height: 50vh; - } - - /* Mantener todas las partículas pero hacerlas más pequeñas */ - .particle-1, .particle-2, .particle-3, .particle-4, .particle-5, .particle-6 { - display: block; - } - - .particle-1 { - width: 35px; - height: 35px; - } - - .particle-2 { - width: 45px; - height: 45px; - } - - .particle-3, .particle-4, .particle-5, .particle-6 { - width: 30px; - height: 30px; - } -} -@media (max-width: 320px) { - .logo-text { - font-size: 14cqw; - padding: 0 15px; - } - - .subtitle { - font-size: 0.8rem; - } - - .tagline { - font-size: 0.6rem; - } - - .fotopgs { - max-height: 45vh; - } - - /* Mantener al menos 4 partículas en pantallas muy pequeñas */ - .particle-1, .particle-2, .particle-3, .particle-4 { - display: block; - } - - .particle-5, .particle-6 { - display: none; - } - - .particle-1, .particle-2, .particle-3, .particle-4 { - width: 25px; - height: 25px; + .hero-kicker { + letter-spacing: 0.2em; } } - -/* El sticky de .parte2 se maneja con CSS puro (position: sticky en .parte2) */ - -.liquidglassstyle{ - position: fixed; - right: 50%; -} \ No newline at end of file diff --git a/src/pages/index.astro b/src/pages/index.astro index d5c72e0..3582d0f 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -4,52 +4,161 @@ import './_styles.css'; import '../styles/proyectos.css'; import '../styles/contacto.css'; import { Image } from 'astro:assets'; -//import pgscom from '../assets/pgscom.png'; -import minimal1 from '../assets/minimal1.svg'; -import customShape from '../assets/custom-shape.svg'; -import minimal3 from '../assets/minimal3.svg'; import logo from '../assets/logo.png'; -// import logosvganim from '../assets/animlogo/logo.svg'; -import proyectosData from '../data/proyectos.json'; +import { getCollection } from 'astro:content'; +import categoriasData from '../data/categorias.json'; import contactoData from '../data/contacto.json'; +import { formatFecha } from '../utils/fechas.js'; + +// Forma de la P extraída de public/assets/animlogo/carga.svg (viewBox 600x600) +const pPath = 'M 185.06066,92.957646 V 214.07196 456.30058 H 306.17677 V 335.19165 c 66.88974,0 121.11609,-54.22597 121.11609,-121.11609 0,-66.88986 -54.22635,-121.114324 -121.11609,-121.114324 v -0.004 z'; + +const categorias = categoriasData.categorias; +// Cada proyecto vive en src/content/proyectos/.md; aquí solo se usan +// los metadatos del frontmatter (el cuerpo markdown se renderiza en su ficha) +const proyectos = (await getCollection('proyectos')).map((e) => ({ id: e.id, ...e.data })); +const catById = Object.fromEntries(categorias.map((c) => [c.id, c])); +// Color cuando un proyecto no tiene categoría (o esta no existe en el JSON) +const COLOR_NEUTRO = '#9bb4d0'; +const colorDe = (catId) => catById[catId]?.color ?? COLOR_NEUTRO; + +// Posición, profundidad (parallax) y rotación de cada card del hero. +// Los proyectos con `destacado: N` en su frontmatter flotan aquí (ordenados +// por N); solo caben tantos como huecos hay en este layout. +const heroLayout = [ + { left: '14%', top: '26%', depth: 0.9, rot: -7, size: 'md', far: false }, + { left: '80%', top: '20%', depth: 0.7, rot: 6, size: 'md', far: false }, + { left: '87%', top: '64%', depth: 1.15, rot: 9, size: 'lg', far: false }, + { left: '11%', top: '69%', depth: 1.25, rot: -10, size: 'lg', far: false }, + { left: '30%', top: '87%', depth: 0.55, rot: 5, size: 'sm', far: true }, + { left: '67%', top: '89%', depth: 0.8, rot: -5, size: 'sm', far: true }, +]; + +const featured = proyectos + .filter((p) => p.destacado != null && p.imagen) + .sort((a, b) => a.destacado - b.destacado) + .slice(0, heroLayout.length) + .map((p) => ({ ...p, aptitudTitulo: catById[p.categorias?.[0]]?.titulo ?? '' })); + +// ── Ruta cronológica (formato currículum: lo más reciente arriba) ── +// Las categorías son etiquetas del proyecto (puede tener varias) y las +// subtarjetas (`sub`) muestran trabajos concretos dentro del proyecto. +const cronologia = [...proyectos].sort((a, b) => + String(b.fecha ?? '').localeCompare(String(a.fecha ?? '')) +); + +let añoAnterior = null; +const rutaEntries = cronologia.map((p, i) => { + const año = String(p.fecha ?? '').slice(0, 4) || 'Otros'; + const nuevoAño = año !== añoAnterior; + añoAnterior = año; + const peso = p.peso ?? 50; + const catsProyecto = p.categorias ?? []; + const subs = (p.sub ?? []).map((s) => ({ + ...s, + cat: s.categoria ? catById[s.categoria] : null, + color: s.categoria ? colorDe(s.categoria) : null, + fechaTexto: formatFecha(s.fecha), + })); + return { + ...p, + año, + nuevoAño, + fechaTexto: formatFecha(p.fecha), + lado: i % 2 === 0 ? 'izq' : 'der', + inset: ['0%', '5%', '2%'][i % 3], + w: peso >= 75 ? 580 : peso >= 55 ? 500 : 430, + color: colorDe(catsProyecto[0]), + // Para el filtro cuentan solo las categorías del proyecto + catIds: catsProyecto, + cats: catsProyecto.map((id) => catById[id]).filter(Boolean), + subs, + }; +}); --- - + + + + + + + +
-
- - - - - - +
+ {featured.map((proyecto, i) => { + const l = heroLayout[i]; + return ( + + + + {proyecto.titulo} + + {proyecto.titulo} + {proyecto.aptitudTitulo} + + + + + ); + })}
-
-

PGSCOM

+ +
+

Desarrollo web · Audiovisual · Animación 3D

+
+

+ PGSCOM +

+
+

Portfolio de Pablo García Santamaría — proyectos de código, cine y diseño.

+
-
-
- -
+
Scroll
+
@@ -60,19 +169,26 @@ import contactoData from '../data/contacto.json';
-
+

Proyectos

-

Explora mis trabajos por categoría

+

+ Mi trayectoria en orden cronológico, de lo más reciente a lo más antiguo. Filtra por disciplina para ver solo lo que te interesa. +

- - -
-
- - - - - - - + + ))}
-
-
+
+ +
@@ -327,121 +432,10 @@ import contactoData from '../data/contacto.json';
- - - @@ -573,6 +574,15 @@ import contactoData from '../data/contacto.json'; import '../scripts/galaxia-scroll.js'; + + + - -
\ No newline at end of file diff --git a/src/pages/proyectos.astro b/src/pages/proyectos.astro deleted file mode 100644 index 0f4783c..0000000 --- a/src/pages/proyectos.astro +++ /dev/null @@ -1,12 +0,0 @@ ---- - import Layout from '../layouts/Layout.astro'; - import LiquidGlassComponent from '../components/liquidglasscomponent.jsx'; - import './_styles.css'; - --- - - - -

¡Funciona! 🚀

-

Este contenido se está renderizando dentro del efecto de cristal líquido con sticky.

-
-
\ No newline at end of file diff --git a/src/pages/proyectos/[id].astro b/src/pages/proyectos/[id].astro new file mode 100644 index 0000000..84b923f --- /dev/null +++ b/src/pages/proyectos/[id].astro @@ -0,0 +1,224 @@ +--- +import Layout from '../../layouts/Layout.astro'; +import '../../styles/proyecto-detalle.css'; +import { getCollection, render } from 'astro:content'; +import categoriasData from '../../data/categorias.json'; +import { formatFecha } from '../../utils/fechas.js'; + +// Genera una ruta estática por cada .md de src/content/proyectos/. +// Cada página es HTML real y rastreable por los bots de Google. +export async function getStaticPaths() { + const cats = categoriasData.categorias; + const entradas = await getCollection('proyectos'); + const proyectos = entradas.map((e) => ({ id: e.id, ...e.data })); + return entradas.map((entrada) => { + const proyecto = { id: entrada.id, ...entrada.data }; + const catIds = proyecto.categorias ?? []; + // La primera categoría del proyecto actúa como principal + const aptitud = cats.find((c) => c.id === catIds[0]) ?? cats[0]; + const relacionados = proyectos + .filter((p) => p.id !== proyecto.id && (p.categorias ?? []).some((c) => catIds.includes(c))) + .slice(0, 3); + return { + params: { id: proyecto.id }, + props: { entrada, proyecto, aptitud, relacionados }, + }; + }); +} + +const { entrada, proyecto, aptitud, relacionados } = Astro.props; +// El cuerpo markdown del .md se convierte en el contenido de la ficha +const { Content } = await render(entrada); + +const fechaTexto = formatFecha(proyecto.fecha); +// `imagen` viste las tarjetas/relacionados y el og:image; la cabecera usa +// `trailer` (vídeo) o `portada`, con `imagen` como respaldo +const imagen = proyecto.imagen || aptitud.logo; +const portada = proyecto.portada || proyecto.imagen; +// Sin portada ni imagen, la cabecera muestra el icono de la categoría contenido +const esIcono = !portada; +const portadaSrc = portada || aptitud.logo; + +const tituloPagina = `${proyecto.titulo.trim()} · ${aptitud.titulo} — PGSCOM`; +const descripcionPagina = proyecto.descripcion || `${proyecto.titulo} — proyecto de ${aptitud.titulo} de Pablo García Santamaría.`; + +// Datos estructurados (Schema.org) para enriquecer el resultado en Google +const jsonLd = { + '@context': 'https://schema.org', + '@type': 'CreativeWork', + name: proyecto.titulo.trim(), + description: descripcionPagina, + url: new URL(`/proyectos/${proyecto.id}`, Astro.site).href, + image: new URL(imagen, Astro.site).href, + genre: aptitud.titulo, + keywords: (proyecto.tecnologias ?? []).join(', '), + creator: { + '@type': 'Person', + name: 'Pablo García Santamaría', + url: Astro.site?.href, + }, + ...(proyecto.premio && { + award: proyecto.premio, + }), + ...(proyecto.link && { + sameAs: proyecto.link, + }), +}; + +const breadcrumbLd = { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { '@type': 'ListItem', position: 1, name: 'Inicio', item: Astro.site?.href }, + { '@type': 'ListItem', position: 2, name: 'Proyectos', item: new URL('/#proyectos-mapa', Astro.site).href }, + { '@type': 'ListItem', position: 3, name: proyecto.titulo.trim(), item: new URL(`/proyectos/${proyecto.id}`, Astro.site).href }, + ], +}; +--- + + + + diff --git a/src/pages/sitemap.xml.ts b/src/pages/sitemap.xml.ts new file mode 100644 index 0000000..78242a9 --- /dev/null +++ b/src/pages/sitemap.xml.ts @@ -0,0 +1,30 @@ +import type { APIRoute } from 'astro'; +import { getCollection } from 'astro:content'; + +// Sitemap generado proceduralmente: portada + una URL por cada proyecto. +export const GET: APIRoute = async ({ site }) => { + const base = (site?.href ?? 'https://pgscom.es/').replace(/\/$/, ''); + + const urls: string[] = [`${base}/`]; + for (const proyecto of await getCollection('proyectos')) { + urls.push(`${base}/proyectos/${proyecto.id}`); + } + + const body = ` + +${urls + .map( + (url) => ` + ${url} + monthly + ${url.endsWith('/') ? '1.0' : '0.8'} + `, + ) + .join('\n')} + +`; + + return new Response(body, { + headers: { 'Content-Type': 'application/xml; charset=utf-8' }, + }); +}; diff --git a/src/pages/test.astro b/src/pages/test.astro deleted file mode 100644 index 9a8a29e..0000000 --- a/src/pages/test.astro +++ /dev/null @@ -1,36 +0,0 @@ ---- - // #region 📦 IMPORTS Y CONFIGURACIÓN INICIAL - import Layout from '../layouts/Layout.astro'; - import LiquidGlassComponent from '../components/liquidglasscomponent.jsx'; - import './_styles.css'; - // #endregion ---- - - -
-
- Background -
-

Contenido de scroll

-

Este contenido permite hacer scroll...

-

Sigue scrolleando para ver el efecto sticky del componente de cristal líquido.

-
- Content -
-

Más contenido para hacer scroll...

-
-
- - -

¡Funciona! 🚀

-

Este contenido se está renderizando dentro del efecto de cristal líquido con sticky.

-
- \ No newline at end of file diff --git a/src/scripts/galaxia-scroll.js b/src/scripts/galaxia-scroll.js index 5f675fb..741112f 100644 --- a/src/scripts/galaxia-scroll.js +++ b/src/scripts/galaxia-scroll.js @@ -3,58 +3,62 @@ import { ScrollTrigger } from 'gsap/ScrollTrigger'; gsap.registerPlugin(ScrollTrigger); -function init() { - const targetEl = document.querySelector('.image-container'); - const logoEl = document.querySelector('#logoanimar'); - const maskEl = document.querySelector('.galaxiamask'); - const videoA = document.querySelector('#video-galaxia-a'); - const videoB = document.querySelector('#video-galaxia-b'); +// ── Zoom de galaxia con scroll ──────────────────────────────────────────────── +// Durante los dos primeros viewports de scroll, el logo enmascara un vídeo que +// se escala hasta llenar la pantalla mientras el vídeo avanza al ritmo del +// scroll (scrub). Al completar el zoom se cambia a un vídeo de intro completo +// y después a una playlist en bucle, con doble búfer para que el corte entre +// vídeos no se note. - if (!targetEl || !logoEl || !maskEl || !videoA || !videoB) return; +function init() { + const logoEl = document.querySelector('#logoanimar'); + const maskEl = document.querySelector('.galaxiamask'); + const videoA = document.querySelector('#video-galaxia-a'); + const videoB = document.querySelector('#video-galaxia-b'); - const isMobile = window.matchMedia('(max-width: 768px)').matches; + if (!logoEl || !maskEl || !videoA || !videoB) return; - const tl = gsap.timeline({ paused: true }); gsap.config({ force3D: true }); - tl.fromTo(targetEl, - { opacity: 1, scale: 1 }, - { opacity: 0, scale: 0.8, duration: isMobile ? 0.311 : 0.8, ease: 'power2.out', force3D: true }, 0) - .fromTo(logoEl, + // Timeline del zoom: el logo crece y la máscara se abre con él + const tl = gsap.timeline({ paused: true }); + tl.fromTo(logoEl, { scale: 1, y: 0 }, { - scale: 25, y: 45, duration: 1, ease: 'power2.in', force3D: true, + scale: 25, y: 45, duration: 1, ease: 'power2.in', onUpdate() { - const rect = logoEl.getBoundingClientRect(); - const s = (rect.height / window.innerHeight) * 100 - 8; + const p = this.progress(); + const s = 33 * (1 + 24 * p * p * p) - 8; maskEl.style.webkitMaskSize = `${s}vh ${s}vh`; maskEl.style.maskSize = `${s}vh ${s}vh`; } }, 1) .to(logoEl, { opacity: 0, duration: 0.001, ease: 'none' }, 1.8); - const VIDEO_SCROLL = '/vid/empezar_scroll.mp4'; - const VIDEO_INTRO_FULL = 'https://pgscom.github.io/webvid/empezar.mp4'; - const PLAYLIST = ['https://pgscom.github.io/webvid/loopinversed.mp4', 'https://pgscom.github.io/webvid/loop.mp4']; + const VIDEO_SCRUB = '/vid/empezar_scroll.mp4'; + const VIDEO_INTRO = 'https://pgscom.github.io/webvid/empezar.mp4'; + const PLAYLIST = ['https://pgscom.github.io/webvid/loopinversed.mp4', 'https://pgscom.github.io/webvid/loop.mp4']; - const ZOOM_START_S = 1; - const ZOOM_END_S = 2; + // El zoom ocupa el tramo [1s, 2s] de la timeline y recorre los 2s del vídeo + const ZOOM_START_S = 1; + const ZOOM_END_S = 2; const SCRUB_DURATION_S = 2; - let playlistIndex = 0; - let isScrollScrubActive = true; - let afterZoomStarted = false; - let activeMetadataReady = false; + let playlistIndex = 0; + let afterZoomStarted = false; let activeVideo = videoA; let bufferVideo = videoB; + let activeReady = false; // metadatos del vídeo de scrub disponibles + let nextReady = null; // promesa de la precarga del búfer + // ── Scrub: como mucho un seek por frame, y nunca antes de tener metadatos ── let pendingScrubT = null; let scrubRafId = 0; function applyScrub() { scrubRafId = 0; - if (pendingScrubT == null || !activeMetadataReady) return; + if (pendingScrubT == null || !activeReady) return; const t = pendingScrubT; pendingScrubT = null; try { @@ -69,10 +73,12 @@ function init() { if (!scrubRafId) scrubRafId = requestAnimationFrame(applyScrub); } + // Resuelve con metadatos o con error: una carga fallida no cuelga la cadena function waitForMetadata(el) { return new Promise(resolve => { - if (el.readyState >= 1 && !isNaN(el.duration)) { resolve(); return; } + if ((el.readyState >= 1 && !isNaN(el.duration)) || el.error) { resolve(); return; } el.addEventListener('loadedmetadata', resolve, { once: true }); + el.addEventListener('error', resolve, { once: true }); }); } @@ -82,7 +88,6 @@ function init() { if (el.src !== location.origin + src && el.src !== src) el.src = src; el.load(); await waitForMetadata(el); - if (el === activeVideo) activeMetadataReady = true; const t = Math.max(0, Math.min(startTime, el.duration || Infinity)); if (!isNaN(t)) try { el.currentTime = t; } catch {} @@ -92,8 +97,9 @@ function init() { } catch {} } - async function prepareNext(src, startTime = 0) { - await setVideoSource(bufferVideo, src, startTime, false); + async function prepareNext(src) { + await setVideoSource(bufferVideo, src, 0, false); + // Forzar el buffering del primer frame para que el cambio sea instantáneo try { await bufferVideo.play(); bufferVideo.pause(); } catch {} } @@ -101,7 +107,7 @@ function init() { activeVideo.onended = async () => { await swapToPreloaded(); playlistIndex = (playlistIndex + 1) % PLAYLIST.length; - prepareNext(PLAYLIST[playlistIndex], 0); + nextReady = prepareNext(PLAYLIST[playlistIndex]); }; bufferVideo.onended = null; } @@ -120,47 +126,54 @@ function init() { attachEndedHandler(); } - setVideoSource(activeVideo, VIDEO_SCROLL, 0, false); - prepareNext(VIDEO_INTRO_FULL, 0); + // Tras el zoom: espera a que el vídeo de intro esté precargado y cambia. + // Si su carga falló (sin red), se queda el último frame del scrub en vez + // de cortar a negro. + async function startAfterZoom() { + if (afterZoomStarted) return; + afterZoomStarted = true; - function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); } + pendingScrubT = null; + if (scrubRafId) { cancelAnimationFrame(scrubRafId); scrubRafId = 0; } + + await nextReady; + if (bufferVideo.error) return; - function onScroll() { - const scrollY = window.scrollY || document.documentElement.scrollTop; - const progress = clamp(scrollY / (2 * window.innerHeight), 0, 1); - const seekTime = progress * tl.duration(); + await swapToPreloaded(); + nextReady = prepareNext(PLAYLIST[playlistIndex]); + } + // El vídeo de scrub se carga en el elemento activo; si llega tarde, aplica + // el seek que hubiera quedado pendiente de scrolls anteriores + setVideoSource(activeVideo, VIDEO_SCRUB, 0, false).then(() => { + activeReady = true; + if (pendingScrubT != null && !afterZoomStarted) scheduleScrub(pendingScrubT); + }); + nextReady = prepareNext(VIDEO_INTRO); + + function onProgress(progress) { tl.progress(progress); - if (!isScrollScrubActive) return; + if (afterZoomStarted) return; - if (seekTime <= ZOOM_START_S) { - try { if (!activeVideo.paused) activeVideo.pause(); } catch {} - scheduleScrub(0); - } else if (seekTime < ZOOM_END_S) { - const localP = (seekTime - ZOOM_START_S) / (ZOOM_END_S - ZOOM_START_S); + const seekTime = progress * tl.duration(); + if (seekTime < ZOOM_END_S) { + const localP = Math.max(0, (seekTime - ZOOM_START_S) / (ZOOM_END_S - ZOOM_START_S)); try { if (!activeVideo.paused) activeVideo.pause(); } catch {} scheduleScrub(localP * SCRUB_DURATION_S); - } else if (!afterZoomStarted && activeMetadataReady) { - afterZoomStarted = true; - isScrollScrubActive = false; - - pendingScrubT = null; - if (scrubRafId) { cancelAnimationFrame(scrubRafId); scrubRafId = 0; } - - swapToPreloaded().then(() => prepareNext(PLAYLIST[playlistIndex], 0)); + } else { + startAfterZoom(); } } - onScroll(); - - let rafPending = false; - window.addEventListener('scroll', () => { - if (!rafPending) { - rafPending = true; - requestAnimationFrame(() => { onScroll(); rafPending = false; }); - } - }, { passive: true }); + // ScrollTrigger (sincronizado con Lenis) en vez de un listener propio. + // onRefresh cubre la posición inicial al cargar o redimensionar. + ScrollTrigger.create({ + start: 0, + end: () => window.innerHeight * 2, + onUpdate: (self) => onProgress(self.progress), + onRefresh: (self) => onProgress(self.progress), + }); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); diff --git a/src/scripts/hero.js b/src/scripts/hero.js new file mode 100644 index 0000000..6de8057 --- /dev/null +++ b/src/scripts/hero.js @@ -0,0 +1,150 @@ +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import lenis from './smooth-scroll.js'; +gsap.registerPlugin(ScrollTrigger); + +// ── Hero: cards de proyectos flotando alrededor del logo ───────────────────── +// Tres capas por card para que las animaciones no se pisen el transform: +// .hero-card → posición, entrada, parallax de ratón y scroll (GSAP) +// .hero-card-tilt → rotación base + flotación orgánica (GSAP) +// .hero-card-media → hover scale (CSS) + +function initHero() { + const cards = gsap.utils.toArray('.hero-card'); + const pEl = document.getElementById('letter-p'); + const gscomWrap = document.getElementById('letters-gscom-wrap'); + const gscomText = document.getElementById('letters-gscom'); + if (!pEl || !gscomWrap) return; + + const fadeEls = ['.hero-kicker', '.hero-sub', '.hero-actions', '.hero-scroll-hint'] + .map(s => document.querySelector(s)) + .filter(Boolean); + + // ── Estados iniciales (el preloader cubre la pantalla mientras tanto) ──── + gsap.set(cards, { xPercent: -50, yPercent: -50, autoAlpha: 0, scale: 0.55 }); + cards.forEach(card => { + const tilt = card.querySelector('.hero-card-tilt'); + gsap.set(tilt, { rotation: parseFloat(card.dataset.rot) || 0 }); + }); + gsap.set(gscomWrap, { width: 0, opacity: 0 }); + gsap.set(pEl, { scale: 0.4, autoAlpha: 0 }); + gsap.set(fadeEls, { autoAlpha: 0, y: 26 }); + + let entered = false; + + function startIdleFloat() { + cards.forEach(card => { + const tilt = card.querySelector('.hero-card-tilt'); + const d = parseFloat(card.dataset.depth) || 1; + gsap.to(tilt, { + y: gsap.utils.random(10, 20) * (Math.random() < 0.5 ? -1 : 1) * d, + rotation: `+=${gsap.utils.random(-3, 3)}`, + duration: gsap.utils.random(3, 5), + yoyo: true, repeat: -1, ease: 'sine.inOut', + delay: gsap.utils.random(0, 1.5), + }); + }); + } + + function entrance() { + if (entered) return; + entered = true; + + // Medir GSCOM con la fuente ya cargada (el preloader espera fonts.ready) + gsap.set(gscomWrap, { width: 'auto' }); + const gscomWidth = gscomWrap.offsetWidth + 2; + gsap.set(gscomWrap, { width: 0 }); + + const tl = gsap.timeline({ defaults: { ease: 'power3.out' } }); + tl.to(pEl, { scale: 1, autoAlpha: 1, duration: 0.7, ease: 'back.out(1.8)' }) + .to(gscomWrap, { + width: gscomWidth, opacity: 1, duration: 0.9, ease: 'power3.inOut', + onComplete() { + gsap.set(gscomWrap, { width: 'auto', overflow: 'visible' }); + const grad = 'pgscom-intro-scroll 2s linear forwards, pgscom-loop-scroll 4s linear 2s infinite'; + pEl.style.animation = grad; + if (gscomText) gscomText.style.animation = grad; + }, + }, '-=0.35') + .to(cards, { + autoAlpha: 1, scale: 1, duration: 1, + ease: 'back.out(1.4)', + stagger: { each: 0.08, from: 'random' }, + }, 0.2); + + fadeEls.forEach((el, i) => { + tl.to(el, { autoAlpha: 1, y: 0, duration: 0.55 }, 0.55 + i * 0.12); + }); + + tl.call(startIdleFloat); + } + + // ── Parallax de ratón (solo puntero fino, fuera del hero se pausa) ─────── + if (window.matchMedia('(pointer: fine)').matches && cards.length) { + let heroVisible = true; + const setters = cards.map(card => ({ + x: gsap.quickTo(card, 'x', { duration: 0.9, ease: 'power3' }), + y: gsap.quickTo(card, 'y', { duration: 0.9, ease: 'power3' }), + d: parseFloat(card.dataset.depth) || 1, + })); + + window.addEventListener('pointermove', (e) => { + if (!heroVisible || !entered) return; + const nx = e.clientX / window.innerWidth - 0.5; + const ny = e.clientY / window.innerHeight - 0.5; + for (const s of setters) { s.x(nx * 46 * s.d); s.y(ny * 28 * s.d); } + }, { passive: true }); + + ScrollTrigger.create({ + trigger: '.main-container', + start: 'top top', end: 'bottom top', + onToggle: self => { heroVisible = self.isActive; }, + }); + } + + // ── Salida con scroll: cada profundidad sube a distinta velocidad ──────── + const exit = gsap.timeline({ + scrollTrigger: { + trigger: '.main-container', + start: 'top top', end: 'bottom top', + scrub: 0.8, + }, + }); + cards.forEach(card => { + const d = parseFloat(card.dataset.depth) || 1; + exit.to(card, { yPercent: -(50 + 55 * d), ease: 'none' }, 0); + }); + exit.to('.content', { yPercent: -22, autoAlpha: 0.15, ease: 'none' }, 0); + exit.to('.hero-scroll-hint', { autoAlpha: 0, ease: 'none', duration: 0.25 }, 0); + + // ── Botón "Ver proyectos": scroll suave para no saltarse las animaciones ── + // En vez de saltar directo a la cronología (lo que se salta el zoom de + // galaxia y la salida de las cards), recorremos la página despacio con + // Lenis; si el usuario interactúa, Lenis le devuelve el control. + const cta = document.querySelector('.hero-cta'); + if (cta) { + cta.addEventListener('click', (e) => { + const target = document.querySelector('#proyectos-mapa') || document.querySelector('#proyectos'); + if (!target) return; + e.preventDefault(); + lenis.scrollTo(target, { + duration: 7, + easing: (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2), + }); + }); + } + + // El preloader dispara la entrada; fallback por si ya no existe + if (!document.getElementById('preloader')) { + entrance(); + } else { + window.addEventListener('loading-done', entrance, { once: true }); + setTimeout(entrance, 9000); // red de seguridad si el preloader falla + } +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initHero); +} else { + initHero(); +} diff --git a/src/scripts/preloader.js b/src/scripts/preloader.js new file mode 100644 index 0000000..5d4392e --- /dev/null +++ b/src/scripts/preloader.js @@ -0,0 +1,77 @@ +import gsap from 'gsap'; +import { DrawSVGPlugin } from 'gsap/DrawSVGPlugin.js'; +gsap.registerPlugin(DrawSVGPlugin); + +// ── Preloader: la P de carga.svg se construye trazándose con DrawSVG ───────── +// El overlay viene renderizado desde el servidor (index.astro) para que cubra +// la pantalla desde el primer paint. El trazo avanza con la carga real y, al +// completar, el relleno con el degradado aparece y la P hace zoom de salida. + +function dispatchDone() { + document.documentElement.style.overflow = ''; + // Flag para listeners que se registran tarde (p. ej. tras un await) + window.__loadingDone = true; + window.dispatchEvent(new CustomEvent('loading-done')); +} + +function initPreloader() { + const pre = document.getElementById('preloader'); + if (!pre) { dispatchDone(); return; } + + const svg = pre.querySelector('.pl-svg'); + const draw = pre.querySelector('.pl-draw'); + const fill = pre.querySelector('.pl-fill'); + const outline = pre.querySelector('.pl-outline'); + + if ('scrollRestoration' in history) history.scrollRestoration = 'manual'; + window.scrollTo(0, 0); + document.documentElement.style.overflow = 'hidden'; + + gsap.set(draw, { drawSVG: '0%', visibility: 'visible' }); + gsap.set(fill, { opacity: 0 }); + + const state = { p: 0 }; + const render = () => { + gsap.set(draw, { drawSVG: `0% ${state.p}%` }); + }; + + // El trazo avanza hasta 90 mientras carga de verdad; el 100 llega con el load real + const crawl = gsap.to(state, { p: 90, duration: 2.6, ease: 'power1.inOut', onUpdate: render }); + + const ready = Promise.all([ + document.readyState === 'complete' + ? Promise.resolve() + : new Promise(r => window.addEventListener('load', r, { once: true })), + document.fonts?.ready ?? Promise.resolve(), + new Promise(r => setTimeout(r, 1700)), // duración mínima para que la intro respire + ]); + + ready.then(() => { + crawl.kill(); + gsap.timeline() + // El trazo se cierra del todo… + .to(state, { p: 100, duration: 0.5, ease: 'power2.out', onUpdate: render }) + // …y la P se rellena con el degradado mientras los trazos se apagan + .to(fill, { opacity: 1, duration: 0.45, ease: 'power2.inOut' }, '-=0.1') + .to([draw, outline], { opacity: 0, duration: 0.35, ease: 'power1.out' }, '<0.15') + // El hero arranca su entrada debajo mientras la P hace zoom + .add(dispatchDone, '+=0.1') + // Zoom a través de la P: transform compositado sobre el propio + .to(svg, { scale: 30, duration: 1.1, ease: 'power3.in' }) + .to(pre, { autoAlpha: 0, duration: 0.4, ease: 'power1.out' }, '-=0.4') + .call(() => pre.remove()); + }); +} + +// Bfcache: si el navegador restaura la página, el preloader ya no pinta nada +window.addEventListener('pageshow', (e) => { + if (!e.persisted) return; + document.getElementById('preloader')?.remove(); + document.documentElement.style.overflow = ''; +}); + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initPreloader); +} else { + initPreloader(); +} diff --git a/src/scripts/proyectos-orbit.js b/src/scripts/proyectos-orbit.js new file mode 100644 index 0000000..b777dd8 --- /dev/null +++ b/src/scripts/proyectos-orbit.js @@ -0,0 +1,63 @@ +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +gsap.registerPlugin(ScrollTrigger); + +// ── Órbita de categorías alrededor del logo en la sección de proyectos ─────── +// Usa las .proyecto-card reales (enlaces a cada bloque), sin clones. +// El tween solo corre mientras la sección está en viewport. + +function initOrbit() { + const ring = document.querySelector('.proyectos-grid'); + if (!ring) return; + const cards = gsap.utils.toArray('.proyecto-card', ring); + if (!cards.length) return; + + const proxy = { a: 0 }; + let radius = 0; + + const measure = () => { + const rect = ring.getBoundingClientRect(); + const card = cards[0].getBoundingClientRect(); + radius = Math.min(rect.width, rect.height) / 2 - card.width / 2 - 8; + }; + + const place = () => { + cards.forEach((card, i) => { + const ang = -Math.PI / 2 + (i / cards.length) * Math.PI * 2 + proxy.a; + gsap.set(card, { x: Math.cos(ang) * radius, y: Math.sin(ang) * radius }); + }); + }; + + gsap.set(cards, { xPercent: -50, yPercent: -50 }); + measure(); + place(); + + gsap.from(cards, { + scale: 0, autoAlpha: 0, duration: 0.7, ease: 'back.out(1.6)', + stagger: 0.06, + scrollTrigger: { trigger: '.proyectos-hero', start: 'top 75%' }, + }); + + // La órbita solo gira mientras la sección está en pantalla + const spin = gsap.to(proxy, { + a: Math.PI * 2, duration: 48, repeat: -1, ease: 'none', + onUpdate: place, paused: true, + }); + ScrollTrigger.create({ + trigger: '.proyectos-hero', + start: 'top bottom', end: 'bottom top', + onToggle: self => (self.isActive ? spin.play() : spin.pause()), + }); + + let resizeRaf = 0; + window.addEventListener('resize', () => { + if (resizeRaf) return; + resizeRaf = requestAnimationFrame(() => { resizeRaf = 0; measure(); place(); }); + }); +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initOrbit); +} else { + initOrbit(); +} diff --git a/src/scripts/proyectos-ruta.js b/src/scripts/proyectos-ruta.js new file mode 100644 index 0000000..8f77e57 --- /dev/null +++ b/src/scripts/proyectos-ruta.js @@ -0,0 +1,283 @@ +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import lenis from './smooth-scroll.js'; +gsap.registerPlugin(ScrollTrigger); + +// ── Ruta cronológica de proyectos ───────────────────────────────────────────── +// Una línea luminosa baja por la cronología (lo más reciente arriba) pasando +// por cada marcador de año y cada tarjeta, y se traza con el scroll. Las +// subtarjetas aparecen "conectándose" a su tarjeta principal. Los filtros por +// disciplina atenúan y desaturan lo que no encaja, sin ocultarlo. + +function initRuta() { + const mapa = document.getElementById('proyectos-mapa'); + const cuerpo = mapa?.querySelector('.ruta-cuerpo'); + const svg = mapa?.querySelector('.ruta-linea'); + if (!mapa || !cuerpo || !svg) return; + + // Mismo punto de corte que el @media del CSS que endereza la cronología + const narrowQuery = window.matchMedia('(max-width: 760px)'); + const NS = 'http://www.w3.org/2000/svg'; + const glow = document.createElementNS(NS, 'path'); + const trazo = document.createElementNS(NS, 'path'); + glow.setAttribute('class', 'ruta-linea-glow'); + trazo.setAttribute('class', 'ruta-linea-trazo'); + + // ── Máscara: la línea se oculta al pasar por detrás de cada tarjeta ── + // Siempre activa (con o sin filtro): la línea parece "colarse" entre las + // tarjetas. Los bordes se difuminan para que el corte sea suave. El + // desenfoque se aplica una sola vez a todo el grupo de recortes (más barato + // en móvil) y la máscara solo se recalcula cuando cambia el layout. + const defs = document.createElementNS(NS, 'defs'); + const blur = document.createElementNS(NS, 'filter'); + blur.setAttribute('id', 'ruta-mascara-blur'); + blur.setAttribute('x', '-20%'); + blur.setAttribute('y', '-20%'); + blur.setAttribute('width', '140%'); + blur.setAttribute('height', '140%'); + const feBlur = document.createElementNS(NS, 'feGaussianBlur'); + feBlur.setAttribute('stdDeviation', '16'); + blur.append(feBlur); + const mask = document.createElementNS(NS, 'mask'); + mask.setAttribute('id', 'ruta-mascara'); + mask.setAttribute('maskUnits', 'userSpaceOnUse'); + const maskBg = document.createElementNS(NS, 'rect'); // blanco = visible + maskBg.setAttribute('x', '0'); + maskBg.setAttribute('y', '0'); + maskBg.setAttribute('fill', '#fff'); + const recortes = document.createElementNS(NS, 'g'); // negro = oculto + recortes.setAttribute('filter', 'url(#ruta-mascara-blur)'); + mask.append(maskBg, recortes); + defs.append(blur, mask); + svg.append(defs, glow, trazo); + glow.setAttribute('mask', 'url(#ruta-mascara)'); + trazo.setAttribute('mask', 'url(#ruta-mascara)'); + + // Posición de layout relativa al cuerpo: ignora los transforms de las + // animaciones de entrada (scale/translate), que falsearían las medidas + function rectLayout(el) { + let x = 0; + let y = 0; + for (let n = el; n && n !== cuerpo; n = n.offsetParent) { + x += n.offsetLeft; + y += n.offsetTop; + } + return { x, y, w: el.offsetWidth, h: el.offsetHeight }; + } + + function construirMascara() { + recortes.replaceChildren(); + cuerpo.querySelectorAll('.ruta-item').forEach((el) => { + const b = rectLayout(el); + const pad = 22; + const r = document.createElementNS(NS, 'rect'); + r.setAttribute('x', (b.x - pad).toFixed(1)); + r.setAttribute('y', (b.y - pad).toFixed(1)); + r.setAttribute('width', (b.w + pad * 2).toFixed(1)); + r.setAttribute('height', (b.h + pad * 2).toFixed(1)); + r.setAttribute('rx', '36'); + r.setAttribute('fill', '#000'); + recortes.append(r); + }); + } + + let drawTween = null; + + // Puntos de paso: el punto de cada año y el centro de cada tarjeta principal. + // En pantallas estrechas la línea baja recta pasando por los puntos de año. + function puntos() { + const narrow = narrowQuery.matches; + const els = cuerpo.querySelectorAll('.ruta-año-dot, .ruta-item'); + let xRecta = 16; + const dot = cuerpo.querySelector('.ruta-año-dot'); + if (dot) { + const b = rectLayout(dot); + xRecta = b.x + b.w / 2; + } + return [...els].map((el) => { + const b = rectLayout(el); + return { + x: narrow ? xRecta : b.x + b.w / 2, + y: b.y + b.h / 2, + }; + }); + } + + function build() { + const w = cuerpo.clientWidth; + const h = cuerpo.scrollHeight; + svg.setAttribute('width', w); + svg.setAttribute('height', h); + svg.setAttribute('viewBox', `0 0 ${w} ${h}`); + + mask.setAttribute('x', '0'); + mask.setAttribute('y', '0'); + mask.setAttribute('width', w); + mask.setAttribute('height', h); + maskBg.setAttribute('width', w); + maskBg.setAttribute('height', h); + construirMascara(); + + const ps = puntos(); + if (ps.length < 2) return; + + // Curvas con tangente vertical en cada punto → la línea "cae" en S + let d = `M ${ps[0].x.toFixed(1)} ${ps[0].y.toFixed(1)}`; + for (let i = 1; i < ps.length; i++) { + const a = ps[i - 1]; + const b = ps[i]; + const k = (b.y - a.y) * 0.5; + d += ` C ${a.x.toFixed(1)} ${(a.y + k).toFixed(1)}, ${b.x.toFixed(1)} ${(b.y - k).toFixed(1)}, ${b.x.toFixed(1)} ${b.y.toFixed(1)}`; + } + glow.setAttribute('d', d); + trazo.setAttribute('d', d); + + const L = trazo.getTotalLength(); + drawTween?.scrollTrigger?.kill(); + drawTween?.kill(); + + gsap.set([glow, trazo], { strokeDasharray: L, strokeDashoffset: L }); + drawTween = gsap.to([glow, trazo], { + strokeDashoffset: 0, + ease: 'none', + scrollTrigger: { + trigger: cuerpo, + start: 'top 65%', + end: 'bottom 85%', + scrub: 0.5, + }, + }); + } + + // ── Entradas al hacer scroll ── + // Un único ScrollTrigger por marcador de año: entrada animada y "encendido" + // del punto cuando la ruta pasa por él (se apaga al volver hacia arriba) + gsap.utils.toArray('.ruta-año', cuerpo).forEach((el) => { + gsap.from(el, { + autoAlpha: 0, + scale: 0.7, + duration: 0.6, + ease: 'back.out(1.6)', + scrollTrigger: { + trigger: el, + start: 'top 82%', + onEnter: () => el.classList.add('on'), + onLeaveBack: () => el.classList.remove('on'), + }, + }); + }); + + // Un único ScrollTrigger por entrada: la tarjeta principal entra y las + // subtarjetas brotan de ella en cascada, todo en la misma timeline + gsap.utils.toArray('.ruta-entry', cuerpo).forEach((entry) => { + const der = entry.classList.contains('ruta-entry--der'); + const item = entry.querySelector('.ruta-item'); + const subs = entry.querySelectorAll('.ruta-sub'); + + const tlEntry = gsap.timeline({ + scrollTrigger: { trigger: entry, start: 'top 84%' }, + }); + tlEntry.from(item, { + autoAlpha: 0, + y: 56, + rotation: der ? 1.4 : -1.4, + duration: 0.9, + ease: 'power3.out', + }); + if (subs.length) { + tlEntry.from(subs, { + autoAlpha: 0, + x: der ? 36 : -36, + duration: 0.7, + ease: 'power3.out', + stagger: 0.16, + }, '-=0.45'); + } + }); + + // ── Imágenes que no existen: fondo tintado + icono de la categoría ── + cuerpo.querySelectorAll('.ruta-img').forEach((img) => { + const fallar = () => img.closest('.ruta-item')?.classList.add('sin-imagen'); + if (img.complete && img.naturalWidth === 0) fallar(); + else img.addEventListener('error', fallar, { once: true }); + }); + + cuerpo.querySelectorAll('.ruta-sub-img').forEach((img) => { + const fallar = () => img.closest('.ruta-sub')?.classList.add('sin-imagen'); + if (img.complete && img.naturalWidth === 0) fallar(); + else img.addEventListener('error', fallar, { once: true }); + }); + + // ── Filtro por disciplina: atenúa y desatura, no oculta ── + const filtros = mapa.querySelectorAll('.ruta-filtro'); + const entries = cuerpo.querySelectorAll('.ruta-entry'); + let filtroActual = 'all'; + + function setFiltro(cat) { + filtroActual = cat; + entries.forEach((entry) => { + const cats = (entry.dataset.cats || '').split(','); + const item = entry.querySelector('.ruta-item'); + if (item) { + item.classList.toggle('fuera', cat !== 'all' && !cats.includes(cat)); + } + }); + cuerpo.querySelectorAll('.ruta-sub').forEach((sub) => { + const cats = (sub.dataset.cats || '').split(','); + sub.classList.toggle('fuera', cat !== 'all' && !cats.includes(cat)); + }); + filtros.forEach((f) => { + const act = f.dataset.cat === cat; + f.classList.toggle('act', act); + f.setAttribute('aria-pressed', act ? 'true' : 'false'); + }); + document.querySelectorAll('.proyecto-card').forEach((card) => { + card.classList.toggle('is-active', card.dataset.aptitudId === cat); + }); + } + + filtros.forEach((f) => { + f.addEventListener('click', () => { + const cat = f.dataset.cat; + setFiltro(cat !== 'all' && filtroActual === cat ? 'all' : cat); + }); + }); + + // La órbita de categorías también filtra y baja hasta la cronología + document.querySelectorAll('.proyecto-card').forEach((card) => { + card.addEventListener('click', (e) => { + const id = card.dataset.aptitudId; + if (!id) return; + e.preventDefault(); + setFiltro(filtroActual === id ? 'all' : id); + lenis.scrollTo(mapa); + }); + }); + + build(); + // Las imágenes tienen aspect-ratio fijo, pero las fuentes pueden mover el + // layout: se recalcula la línea al cargar todo y al redimensionar. + window.addEventListener('load', () => { + ScrollTrigger.refresh(); + build(); + }); + + let resizeRaf = 0; + let lastW = cuerpo.clientWidth; + window.addEventListener('resize', () => { + if (resizeRaf) return; + resizeRaf = requestAnimationFrame(() => { + resizeRaf = 0; + if (cuerpo.clientWidth !== lastW) { + lastW = cuerpo.clientWidth; + build(); + } + }); + }); +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initRuta); +} else { + initRuta(); +} diff --git a/src/scripts/smooth-scroll.js b/src/scripts/smooth-scroll.js new file mode 100644 index 0000000..9bb4481 --- /dev/null +++ b/src/scripts/smooth-scroll.js @@ -0,0 +1,30 @@ +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import Lenis from 'lenis'; +import 'lenis/dist/lenis.css'; +gsap.registerPlugin(ScrollTrigger); + +// ── Scroll suave con inercia (Lenis) ────────────────────────────────────────── +// Lenis intercepta el scroll nativo y persigue la posición objetivo con lerp, +// dando el efecto de "glide". Se sincroniza con el ticker de GSAP para que +// ScrollTrigger se actualice en cada frame. + +const lenis = new Lenis({ autoRaf: false }); + +lenis.on('scroll', ScrollTrigger.update); +gsap.ticker.add((time) => lenis.raf(time * 1000)); +gsap.ticker.lagSmoothing(0); + +// Enlaces de ancla: scroll animado con la misma inercia. +// Si otro handler ya gestionó el click (CTA del hero, órbita), no se duplica. +document.addEventListener('click', (e) => { + if (e.defaultPrevented) return; + const a = e.target.closest('a[href^="#"]'); + if (!a || a.getAttribute('href') === '#') return; + const dest = document.querySelector(a.getAttribute('href')); + if (!dest) return; + e.preventDefault(); + lenis.scrollTo(dest); +}); + +export default lenis; diff --git a/src/styles/contacto.css b/src/styles/contacto.css index 35b0c0c..bb447a9 100644 --- a/src/styles/contacto.css +++ b/src/styles/contacto.css @@ -33,6 +33,7 @@ border-radius: 50%; box-shadow: 0 0 10px rgba(255, 255, 255, 0.8); animation: float-particle 20s infinite ease-in-out; + will-change: transform; } .particle-space:nth-child(1) { @@ -575,105 +576,4 @@ } } -@media (max-width: 480px) { - .contacto-section { - height: 100vh; - padding: 1.25rem 0.875rem; - overflow-y: auto; - } - - .contacto-header { - margin-bottom: 1rem; - } - - .contacto-titulo { - font-size: 1.5rem; - letter-spacing: 1px; - } - - .contacto-subtitulo { - font-size: 0.8rem; - } - - .contacto-grid { - gap: 1.25rem; - } - - .contacto-info { - gap: 0.65rem; - } - - .info-card { - padding: 0.75rem; - gap: 0.65rem; - } - - .info-icon { - width: 32px; - height: 32px; - } - - .info-icon svg { - width: 16px; - height: 16px; - } - - .info-content h3 { - font-size: 0.7rem; - } - - .info-content a, - .info-content p { - font-size: 0.85rem; - } - - .social-links { - padding: 0.875rem; - } - - .social-links h3 { - font-size: 0.7rem; - margin-bottom: 0.65rem; - } - - .social-icon { - width: 36px; - height: 36px; - } - - .contacto-form-container { - padding: 1rem 0.875rem; - } - - .contacto-form { - gap: 0.75rem; - } - - .form-group label { - font-size: 0.65rem; - margin-bottom: 0.25rem; - } - - .form-group input, - .form-group textarea { - padding: 0.65rem 0.75rem; - font-size: 0.875rem; - } - - .form-group textarea { - min-height: 65px; - max-height: 130px; - } - - .submit-btn { - padding: 0.7rem 1.5rem; - font-size: 0.85rem; - } - - .form-status { - font-size: 0.85rem; - padding: 0.75rem; - } -} - /* #endregion */ diff --git a/src/styles/proyecto-detalle.css b/src/styles/proyecto-detalle.css new file mode 100644 index 0000000..c0f1b05 --- /dev/null +++ b/src/styles/proyecto-detalle.css @@ -0,0 +1,524 @@ +/* ============================================ + FICHA DE PROYECTO — Página individual + Estilo cinematográfico, hermano del modal de proyectos + ============================================ */ + +:root { + --pd-azul: #93c5fd; + --pd-bg: #05070d; +} + +body:has(.pd-page) { + background: var(--pd-bg); +} + +.pd-page { + position: relative; + min-height: 100vh; + color: rgba(255, 255, 255, 0.85); + font-family: 'Chakra Petch', 'Montserrat', sans-serif; + overflow: clip; + isolation: isolate; +} + +/* ── Cabecera cinematográfica (trailer/portada) ──────── */ +.pd-hero { + position: relative; + width: 100%; + height: 94vh; + min-height: 560px; + overflow: hidden; +} + +.pd-hero-media { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; +} + +/* Respaldo sin portada: el icono de la categoría, contenido y tenue */ +.pd-hero-media--icono { + box-sizing: border-box; + object-fit: contain; + padding: 16vh 25vw 38vh; + opacity: 0.55; + filter: drop-shadow(0 12px 50px rgba(0, 0, 0, 0.6)); +} + +.pd-hero-overlay { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + padding: 0 24px 56px; + background: linear-gradient( + 180deg, + rgba(5, 7, 13, 0.55) 0%, + rgba(5, 7, 13, 0) 22%, + rgba(5, 7, 13, 0) 48%, + rgba(5, 7, 13, 0.78) 82%, + var(--pd-bg) 100% + ); +} + +/* ── Botón volver flotante ───────────────────────────── */ +.pd-back { + position: fixed; + top: 24px; + left: 24px; + z-index: 50; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 18px 10px 14px; + border-radius: 999px; + background: rgba(8, 10, 18, 0.6); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.15); + color: rgba(255, 255, 255, 0.9); + text-decoration: none; + font-size: 0.9rem; + font-weight: 600; + transition: transform 0.3s ease, background 0.3s ease, border-color 0.3s ease; +} + +.pd-back:hover { + background: rgba(255, 255, 255, 0.12); + border-color: rgba(147, 197, 253, 0.5); + transform: translateX(-3px); +} + +/* ── Contenedor ──────────────────────────────────────── */ +.pd-wrapper { + position: relative; + z-index: 1; + max-width: 1100px; + margin: 0 auto; + padding: 40px 32px 12vh; +} + +/* ── Migas de pan (superpuestas sobre la cabecera) ───── */ +.pd-breadcrumb { + position: absolute; + top: 28px; + left: 0; + right: 0; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 8px; + font-size: 0.82rem; + color: rgba(255, 255, 255, 0.55); + text-shadow: 0 1px 8px rgba(0, 0, 0, 0.6); +} + +.pd-breadcrumb a { + color: rgba(255, 255, 255, 0.7); + text-decoration: none; + transition: color 0.25s ease; +} + +.pd-breadcrumb a:hover { + color: var(--pd-azul); +} + +.pd-breadcrumb-current { + color: rgba(255, 255, 255, 0.45); +} + +/* ── Info del hero (título y datos centrados abajo) ──── */ +.pd-hero-info { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + max-width: 820px; +} + +.pd-categoria { + display: inline-flex; + align-items: center; + gap: 10px; + margin: 0 0 14px; + padding: 6px 14px 6px 10px; + background: rgba(147, 197, 253, 0.12); + border: 1px solid rgba(147, 197, 253, 0.25); + border-radius: 999px; + font-size: 0.85rem; + font-weight: 600; + color: rgba(200, 220, 255, 0.95); +} + +.pd-categoria img { + width: 20px; + height: 20px; + object-fit: contain; + filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.4)); +} + +.pd-titulo { + font-size: clamp(2.1rem, 5.5vw, 3.6rem); + font-weight: 700; + color: #fff; + margin: 0 0 18px; + line-height: 1.08; + letter-spacing: -0.02em; + text-shadow: 0 4px 24px rgba(0, 0, 0, 0.7); +} + +.pd-meta { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 14px; + margin-bottom: 18px; +} + +.pd-meta-item { + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.7); +} + +.pd-meta-badge { + padding: 5px 12px; + background: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.22); + border-radius: 6px; + font-size: 0.82rem; + font-weight: 600; + color: #fff; +} + +.pd-tagline { + font-size: 1.1rem; + font-style: italic; + color: rgba(255, 255, 255, 0.85); + margin: 0 0 24px; + line-height: 1.55; + max-width: 640px; +} + +/* ── Acciones ────────────────────────────────────────── */ +.pd-actions { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + flex-wrap: wrap; +} + +.pd-btn-primary { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 14px 30px; + background: #fff; + border: none; + border-radius: 8px; + color: #0a0a14; + font-family: inherit; + font-weight: 700; + font-size: 1rem; + text-decoration: none; + cursor: pointer; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease; +} + +.pd-btn-primary:hover { + transform: scale(1.05); + box-shadow: 0 8px 30px rgba(255, 255, 255, 0.25); +} + +.pd-btn-secondary { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 50px; + height: 50px; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 50%; + color: #fff; + cursor: pointer; + transition: transform 0.3s ease, background 0.3s ease, border-color 0.3s ease; +} + +.pd-btn-secondary:hover { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.4); + transform: scale(1.1); +} + +.pd-btn-secondary.pd-copied::after { + content: '¡Copiado!'; + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + white-space: nowrap; + font-size: 0.75rem; + font-weight: 600; + padding: 4px 10px; + border-radius: 6px; + background: rgba(147, 197, 253, 0.9); + color: #0a0a14; +} + +/* ── Cuerpo ──────────────────────────────────────────── */ +.pd-body { + position: relative; +} + +.pd-tecnologias { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 8px; +} + +.pd-tech-tag { + font-size: 0.82rem; + font-weight: 500; + padding: 8px 16px; + background: rgba(147, 197, 253, 0.15); + border: 1px solid rgba(147, 197, 253, 0.3); + border-radius: 20px; + color: rgba(200, 220, 255, 0.95); + letter-spacing: 0.02em; + transition: transform 0.3s ease, background 0.3s ease, border-color 0.3s ease; +} + +.pd-tech-tag:hover { + background: rgba(147, 197, 253, 0.25); + border-color: rgba(147, 197, 253, 0.5); + transform: translateY(-2px); +} + +.pd-tech-tag[data-tech="React"], +.pd-tech-tag[data-tech="Next.js"] { background: rgba(97, 218, 251, 0.18); color: #61dafb; border-color: rgba(97, 218, 251, 0.35); } +.pd-tech-tag[data-tech="Blender"] { background: rgba(234, 106, 32, 0.18); color: #f08a4b; border-color: rgba(234, 106, 32, 0.35); } +.pd-tech-tag[data-tech="Python"] { background: rgba(55, 118, 171, 0.22); color: #7cb3e8; border-color: rgba(55, 118, 171, 0.4); } +.pd-tech-tag[data-tech="TypeScript"] { background: rgba(49, 120, 198, 0.22); color: #6fa8e8; border-color: rgba(49, 120, 198, 0.4); } +.pd-tech-tag[data-tech="Astro"] { background: rgba(255, 90, 40, 0.18); color: #ff8a66; border-color: rgba(255, 90, 40, 0.35); } + +/* ── Contenido HTML ──────────────────────────────────── */ +.pd-contenido { + color: rgba(255, 255, 255, 0.85); + line-height: 1.7; + margin-top: 32px; + padding-top: 32px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + max-width: 800px; +} + +.pd-contenido h3 { + font-size: 1.4rem; + font-weight: 700; + color: #fff; + margin: 0 0 16px; +} + +.pd-contenido h4 { + font-size: 1.1rem; + font-weight: 600; + color: rgba(200, 220, 255, 1); + margin: 26px 0 12px; +} + +.pd-contenido p { + margin: 0 0 16px; + color: rgba(255, 255, 255, 0.8); +} + +.pd-contenido ul, +.pd-contenido ol { + margin: 0 0 16px; + padding-left: 24px; +} + +.pd-contenido li { + margin-bottom: 8px; + color: rgba(255, 255, 255, 0.8); +} + +.pd-contenido strong { + color: var(--pd-azul); + font-weight: 600; +} + +.pd-contenido blockquote { + margin: 22px 0; + padding: 16px 20px; + background: linear-gradient(135deg, rgba(147, 197, 253, 0.1), rgba(196, 181, 253, 0.08)); + border-left: 4px solid rgba(147, 197, 253, 0.6); + border-radius: 0 12px 12px 0; + font-style: italic; + color: rgba(255, 255, 255, 0.9); +} + +.pd-contenido code { + font-family: 'JetBrains Mono', monospace; + font-size: 0.85em; + background: rgba(0, 0, 0, 0.4); + padding: 2px 8px; + border-radius: 4px; + color: rgba(196, 181, 253, 1); +} + +.pd-contenido img { + max-width: 100%; + border-radius: 12px; + margin: 16px 0; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4); +} + +/* ── Relacionados ────────────────────────────────────── */ +.pd-relacionados { + margin-top: 64px; + padding-top: 40px; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.pd-relacionados-titulo { + font-size: 1.5rem; + font-weight: 700; + color: #fff; + margin: 0 0 28px; +} + +.pd-relacionados-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 24px; +} + +.pd-rel-card { + display: flex; + flex-direction: column; + background: linear-gradient(135deg, rgba(30, 35, 55, 0.7), rgba(15, 20, 35, 0.8)); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-radius: 18px; + border: 1px solid rgba(255, 255, 255, 0.1); + text-decoration: none; + overflow: hidden; + transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), border-color 0.4s ease, box-shadow 0.4s ease; +} + +.pd-rel-card:hover { + transform: translateY(-8px); + border-color: rgba(147, 197, 253, 0.5); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5), 0 0 30px rgba(147, 197, 253, 0.15); +} + +.pd-rel-image { + width: 100%; + height: 170px; + overflow: hidden; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.pd-rel-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.pd-rel-card:hover .pd-rel-image img { + transform: scale(1.08); +} + +.pd-rel-info { + padding: 16px; +} + +.pd-rel-titulo { + font-size: 1.05rem; + font-weight: 600; + color: #fff; + margin: 0 0 8px; + transition: color 0.3s ease; +} + +.pd-rel-card:hover .pd-rel-titulo { + color: var(--pd-azul); +} + +.pd-rel-desc { + font-size: 0.82rem; + color: rgba(255, 255, 255, 0.75); + margin: 0; + line-height: 1.45; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* ── Responsive ──────────────────────────────────────── */ +@media (max-width: 768px) { + .pd-wrapper { + padding: 32px 20px 10vh; + } + + .pd-hero { + height: 80vh; + min-height: 480px; + } + + .pd-hero-overlay { + padding-bottom: 40px; + } + + .pd-hero-media--icono { + padding: 14vh 18vw 24vh; + } + + .pd-titulo { + font-size: 1.9rem; + } + + .pd-tagline { + font-size: 1rem; + } + + .pd-back span { + display: none; + } + + .pd-back { + padding: 10px; + } +} + +@media (max-width: 480px) { + .pd-titulo { + font-size: 1.6rem; + } + + .pd-relacionados-grid { + grid-template-columns: 1fr; + } +} + +@media (prefers-reduced-motion: reduce) { + .pd-rel-card, + .pd-rel-image img, + .pd-btn-primary, + .pd-back { + transition: none; + } +} diff --git a/src/styles/proyectos.css b/src/styles/proyectos.css index bc292de..87d06ba 100644 --- a/src/styles/proyectos.css +++ b/src/styles/proyectos.css @@ -1,4 +1,4 @@ -/* Estilos para la sección de proyectos - Layout legible y fluido */ +/* Estilos para la sección de proyectos - Órbita de categorías + ruta cronológica */ /* ============================================ ANIMACIONES PERSONALIZADAS @@ -58,7 +58,8 @@ } /* ============================================ - HERO DECORATIVO - Órbita sin scroll-hijack + HERO DECORATIVO - Órbita de categorías + (cada tarjeta en órbita filtra la cronología) ============================================ */ .proyectos-hero { @@ -68,16 +69,14 @@ justify-content: center; position: relative; padding: 20px; - margin-top: 5vh; - margin-bottom: 10vh; - --orbit-radius: 220px; + margin-top: 2vh; + margin-bottom: 4vh; } .proyectos-hero-inner { position: relative; width: 100%; - /*max-width: 400px;*/ - height: 600px; + height: 520px; display: flex; align-items: center; justify-content: center; @@ -145,7 +144,7 @@ overflow: hidden; transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.4s ease, border-color 0.4s ease, background 0.4s ease; will-change: transform; - box-shadow: + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2), 0 0 20px rgba(147, 197, 253, 0.1); } @@ -179,17 +178,29 @@ .proyecto-card:hover .proyecto-card-inner { transform: translateY(-8px) scale(1.08); - box-shadow: + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.35), 0 0 40px rgba(147, 197, 253, 0.3), 0 0 60px rgba(147, 197, 253, 0.15); - border-color: rgba(147, 197, 253, 0.5); + border-color: var(--cat-color, rgba(147, 197, 253, 0.5)); background: linear-gradient(145deg, rgba(255, 255, 255, 0.2), rgba(147, 197, 253, 0.12), rgba(255, 255, 255, 0.08)); } +/* Categoría activa como filtro: aro en su color */ +.proyecto-card.is-active { + filter: brightness(1.15) saturate(1.2); +} + +.proyecto-card.is-active .proyecto-card-inner { + border-color: var(--cat-color, rgba(147, 197, 253, 0.8)); + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.35), + 0 0 26px color-mix(in srgb, var(--cat-color, #93c5fd) 55%, transparent); +} + .proyecto-icon { width: 100%; height: auto; @@ -238,522 +249,599 @@ } /* ============================================ - CONTENIDO DE PROYECTOS - Layout por Aptitud + RUTA CRONOLÓGICA - Currículum con scroll ============================================ */ -.proyectos-content { - max-width: 1200px; - margin: 0 auto; - padding: 0 24px 8vh; - display: flex; - flex-direction: column; - gap: 10vh; -} - -.aptitud-bloque { +.proyectos-mapa { position: relative; + max-width: 1320px; + margin: 0 auto; + padding: 2vh 24px 12vh; + scroll-margin-top: 8vh; } -.aptitud-header { +/* Filtros por disciplina (atenúan, no ocultan) */ +.ruta-filtros { + position: sticky; + top: 14px; + z-index: 30; display: flex; - align-items: center; - gap: 20px; - margin-bottom: 32px; - padding: 24px; - background: linear-gradient(135deg, - rgba(255, 255, 255, 0.08), - rgba(147, 197, 253, 0.05)); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - border-radius: 20px; + flex-wrap: wrap; + gap: 8px; + width: fit-content; + max-width: 100%; + margin: 0 auto 7vh; + padding: 10px 14px; + border-radius: 22px; + background: rgba(5, 8, 18, 0.72); border: 1px solid rgba(255, 255, 255, 0.12); - box-shadow: - 0 8px 32px rgba(0, 0, 0, 0.2), - inset 0 1px 0 rgba(255, 255, 255, 0.1); - transition: border-color 0.4s ease, box-shadow 0.4s ease; -} - -.aptitud-header:hover { - border-color: rgba(147, 197, 253, 0.25); - box-shadow: - 0 12px 40px rgba(0, 0, 0, 0.3), - 0 0 40px rgba(147, 197, 253, 0.1), - inset 0 1px 0 rgba(255, 255, 255, 0.15); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + box-shadow: 0 10px 36px rgba(0, 0, 0, 0.4); } -.aptitud-icono { - width: 70px; - height: 70px; - border-radius: 50%; - background: linear-gradient(145deg, - rgba(255, 255, 255, 0.15), - rgba(147, 197, 253, 0.1)); - border: 2px solid rgba(255, 255, 255, 0.2); - display: flex; +.ruta-filtro { + display: inline-flex; align-items: center; - justify-content: center; - flex-shrink: 0; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); + gap: 7px; + padding: 7px 14px; + border-radius: 999px; + font-family: 'Chakra Petch', sans-serif; + font-size: 0.8rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.78); + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.14); + cursor: pointer; + transition: all 0.3s ease; } -.aptitud-icono img { - width: 36px; - height: 36px; - object-fit: contain; - filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3)); +.ruta-filtro img { + width: 14px; + height: 14px; } -.aptitud-info { - flex: 1; - min-width: 0; +.ruta-filtro:hover { + color: white; + border-color: color-mix(in srgb, var(--cat-color, #93c5fd) 60%, transparent); + transform: translateY(-1px); } -.aptitud-titulo { - font-family: 'Chakra Petch', sans-serif; - font-size: 1.5rem; - font-weight: 700; +.ruta-filtro.act { color: white; - margin: 0 0 8px 0; - text-shadow: 0 2px 8px rgba(0,0,0,0.4); - line-height: 1.2; + background: color-mix(in srgb, var(--cat-color, #ffffff) 24%, transparent); + border-color: var(--cat-color, #ffffff); } -.aptitud-descripcion { - font-family: 'Chakra Petch', sans-serif; - font-size: 0.95rem; - color: rgba(255, 255, 255, 0.7); - margin: 0; - line-height: 1.5; - max-width: 700px; +.ruta-filtro[data-cat="all"].act { + background: white; + color: #0a0e1c; } -/* Grid de proyectos - Más espacio para tarjetas grandes */ -.proyectos-grid-list { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 32px; +/* Cuerpo de la cronología (sobre él se dibuja la línea) */ +.ruta-cuerpo { + position: relative; } -/* Tarjeta de proyecto individual - Formato Vertical */ -.proyecto-detail-card { - display: flex; - flex-direction: column; - background: linear-gradient(135deg, - rgba(30, 35, 55, 0.7), - rgba(15, 20, 35, 0.8)); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.1); - cursor: pointer; - will-change: transform; - transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), border-color 0.4s ease, box-shadow 0.4s ease; - position: relative; - overflow: hidden; - height: 100%; +/* La línea que serpentea (la dibuja el JS) */ +.ruta-linea { + position: absolute; + top: 0; + left: 0; + pointer-events: none; + z-index: 0; + overflow: visible; } -.proyecto-detail-card:hover { - transform: translateY(-8px); - border-color: rgba(147, 197, 253, 0.5); - box-shadow: - 0 20px 40px rgba(0, 0, 0, 0.5), - 0 0 30px rgba(147, 197, 253, 0.15); +.ruta-linea-glow { + fill: none; + stroke: rgba(147, 197, 253, 0.45); + stroke-width: 7; + stroke-linecap: round; + filter: blur(7px); } -/* Imagen del proyecto (Ahora ocupa el 50% superior) */ -.proyecto-image-container { - width: 100%; - height: 200px; - overflow: hidden; +.ruta-linea-trazo { + fill: none; + stroke: rgba(196, 218, 255, 0.85); + stroke-width: 2; + stroke-linecap: round; +} + +/* Marcador de año en la línea */ +.ruta-año { position: relative; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + width: fit-content; + margin: 2vh auto 7vh; } -.proyecto-image { - width: 100%; - height: 100%; - object-fit: cover; - will-change: transform; - transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); +.ruta-año-dot { + width: 16px; + height: 16px; + border-radius: 50%; + background: rgba(196, 218, 255, 0.95); + box-shadow: 0 0 0 5px rgba(147, 197, 253, 0.18); + transition: box-shadow 0.4s ease, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); } -.proyecto-detail-card:hover .proyecto-image { - transform: scale(1.08); +.ruta-año.on .ruta-año-dot { + transform: scale(1.25); + box-shadow: + 0 0 0 6px rgba(147, 197, 253, 0.25), + 0 0 26px rgba(147, 197, 253, 0.85); } -/* Botón flotante para enlace externo */ -.card-btn-link-float { - position: absolute; - top: 16px; - right: 16px; - width: 40px; - height: 40px; - background: rgba(10, 10, 20, 0.7); +.ruta-año-num { + font-family: 'Chakra Petch', sans-serif; + font-weight: 700; + font-size: clamp(1.4rem, 2.6vw, 1.9rem); + letter-spacing: 0.08em; + color: rgba(255, 255, 255, 0.95); + padding: 4px 20px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.16); + background: rgba(6, 9, 20, 0.6); backdrop-filter: blur(8px); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 50%; + -webkit-backdrop-filter: blur(8px); + text-shadow: 0 2px 14px rgba(0, 0, 0, 0.7); + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35); +} + +/* Entrada de la cronología: tarjeta principal + subtarjetas conectadas */ +.ruta-entry { + position: relative; + z-index: 1; display: flex; align-items: center; - justify-content: center; - color: white; - opacity: 0; - transform: translateY(-10px); - transition: opacity 0.3s ease, transform 0.3s ease; - z-index: 2; + gap: 34px; + margin-bottom: 14vh; + transition: opacity 0.5s ease, filter 0.5s ease; } -.proyecto-detail-card:hover .card-btn-link-float { - opacity: 1; - transform: translateY(0); +.ruta-entry--izq { + justify-content: flex-start; + padding-left: var(--inset, 0%); } -.card-btn-link-float:hover { - background: rgba(34, 197, 94, 0.8); - border-color: #22c55e; - transform: scale(1.1) !important; +.ruta-entry--der { + flex-direction: row-reverse; + justify-content: flex-start; + padding-right: var(--inset, 0%); } -/* Contenedor de la info */ -.proyecto-info-wrapper { - padding: 16px; - position: static; - transform: none; +/* Filtrado: fuera de la disciplina elegida → gris y atenuado */ +.ruta-item.fuera, +.ruta-sub.fuera { + opacity: 0.2; + filter: grayscale(1) brightness(0.85); } -.proyecto-info { - margin-bottom: 0; +/* Tarjeta principal */ +.ruta-item { + position: relative; + display: block; + flex: 0 1 var(--w, 500px); + min-width: 280px; + text-decoration: none; + outline: none; } -.proyecto-detail-titulo { +/* Fecha del proyecto: chip legible sobre la esquina de la tarjeta */ +.ruta-fecha { + position: absolute; + top: -15px; + left: 18px; + z-index: 3; font-family: 'Chakra Petch', sans-serif; - font-size: 1.1rem; - font-weight: 600; + font-weight: 700; + font-size: 0.8rem; color: white; - margin: 0 0 8px 0; - line-height: 1.2; - transition: color 0.3s ease; + white-space: nowrap; + padding: 7px 14px; + border-radius: 999px; + background: rgba(6, 9, 20, 0.9); + border: 1px solid color-mix(in srgb, var(--cat-color, #93c5fd) 60%, transparent); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: + 0 4px 18px rgba(0, 0, 0, 0.45), + 0 0 14px color-mix(in srgb, var(--cat-color, #93c5fd) 18%, transparent); } -.proyecto-detail-card:hover .proyecto-detail-titulo { - color: #93c5fd; +.ruta-entry--der .ruta-fecha { + left: auto; + right: 18px; } -.proyecto-detail-desc { - font-family: 'Chakra Petch', sans-serif; - font-size: 0.82rem; - color: rgba(255, 255, 255, 0.75); - margin: 0 0 10px 0; - line-height: 1.45; - display: -webkit-box; - -webkit-line-clamp: 2; - line-clamp: 2; - -webkit-box-orient: vertical; +.ruta-media { + position: relative; + aspect-ratio: 16 / 10; + border-radius: 26px; overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.14); + background: linear-gradient(135deg, rgba(20, 26, 48, 0.9), rgba(8, 12, 26, 0.95)); + box-shadow: 0 16px 44px rgba(0, 0, 0, 0.45); + transition: + transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1), + border-color 0.35s ease, + box-shadow 0.45s ease; + will-change: transform; } -/* Tags de tecnologías */ -.proyecto-tecnologias { - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.tech-tag { - font-family: 'Chakra Petch', sans-serif; - font-size: 0.75rem; - padding: 4px 12px; - background: rgba(255,255,255,0.1); - border: 1px solid rgba(147, 197, 253, 0.2); - border-radius: 8px; - color: #bfdbfe; +.ruta-item:hover .ruta-media, +.ruta-item:focus-visible .ruta-media { + transform: translateY(-8px); + border-color: color-mix(in srgb, var(--cat-color, #93c5fd) 75%, transparent); + box-shadow: + 0 26px 64px rgba(0, 0, 0, 0.55), + 0 0 38px color-mix(in srgb, var(--cat-color, #93c5fd) 30%, transparent); } -.tech-tag[data-tech="React"], -.tech-tag[data-tech="Next.js"] { background: rgba(97,218,251,0.2); color: #61dafb; } -.tech-tag[data-tech="Blender"] { background: rgba(234,106,32,0.2); color: #ea6a20; } -.tech-tag[data-tech="Python"] { background: rgba(55,118,171,0.2); color: #3776ab; } -.tech-tag[data-tech="TypeScript"] { background: rgba(49,120,198,0.2); color: #3178c6; } -.tech-tag[data-tech="Astro"] { background: rgba(255,90,40,0.2); color: #ff5a28; } - -.tech-tag-more { - background: transparent; - border-color: rgba(255, 255, 255, 0.2); - color: rgba(255, 255, 255, 0.6); +.ruta-item:focus-visible .ruta-media { + outline: 2px solid var(--cat-color, #93c5fd); + outline-offset: 3px; } -/* ============================================ - MODAL DE PROYECTO - Estilo Cinematográfico - ============================================ */ - -.proyecto-modal-overlay { - position: fixed; +.ruta-img { + position: absolute; inset: 0; - backdrop-filter: blur(5px) saturate(120%); - -webkit-backdrop-filter: blur(5px) saturate(120%); - z-index: 10000; - display: flex; - align-items: center; - justify-content: center; - padding: 24px; - opacity: 0; - visibility: hidden; - transition: opacity 0.36s cubic-bezier(0.16, 1, 0.3, 1), visibility 0.36s cubic-bezier(0.16, 1, 0.3, 1); - overflow-y: auto; + width: 100%; + height: 100%; + object-fit: cover; + will-change: transform; + transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); } -.proyecto-modal-overlay.active { - opacity: 1; - visibility: visible; +.ruta-item:hover .ruta-img { + transform: scale(1.05); } -.proyecto-modal { - width: 100%; - max-width: 1400px; - max-height: 90vh; - overflow: hidden; - display: flex; - flex-direction: column; - position: relative; - transform: scale(0.95) translateY(20px); - transition: transform 0.44s cubic-bezier(0.16, 1, 0.3, 1); - background: rgba(12, 14, 20, 0.48); - backdrop-filter: blur(6px) saturate(120%); - -webkit-backdrop-filter: blur(6px) saturate(120%); - border-radius: 18px; - border: 1px solid rgba(255, 255, 255, 0.06); - box-shadow: - 0 30px 90px rgba(0, 0, 0, 0.75), - 0 0 0 1px rgba(255, 255, 255, 0.02); +/* Fallback cuando la imagen del proyecto no existe */ +.ruta-fallback { + display: none; } -.proyecto-modal-overlay.active .proyecto-modal { - transform: scale(1) translateY(0); +.ruta-item.sin-imagen .ruta-img { + display: none; } -/* Imagen de fondo hero */ -.modal-backdrop { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 70%; - overflow: hidden; - z-index: 0; - border-radius: 20px 20px 0 0; +.ruta-item.sin-imagen .ruta-media { + background: + radial-gradient(ellipse at 50% 35%, color-mix(in srgb, var(--cat-color, #93c5fd) 22%, transparent), transparent 70%), + linear-gradient(135deg, rgba(20, 26, 48, 0.95), rgba(8, 12, 26, 0.98)); } -.modal-backdrop-img { - width: 100%; - height: 100%; - object-fit: cover; - object-position: center top; - filter: brightness(0.6); - transition: transform 0.6s ease; +.ruta-item.sin-imagen .ruta-fallback { + display: block; + position: absolute; + left: 50%; + top: 38%; + transform: translate(-50%, -50%); + width: 22%; + opacity: 0.9; + filter: drop-shadow(0 4px 16px rgba(0, 0, 0, 0.5)); } -.proyecto-modal:hover .modal-backdrop-img { - transform: scale(1.02); +/* Insignia de premio */ +.ruta-premio { + position: absolute; + top: 14px; + right: 14px; + z-index: 2; + padding: 6px 12px; + border-radius: 999px; + background: rgba(8, 10, 22, 0.72); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid rgba(255, 217, 122, 0.45); + color: #ffd97a; + font-family: 'Chakra Petch', sans-serif; + font-size: 0.72rem; + font-weight: 600; } -/* Gradiente sobre la imagen */ -.modal-backdrop::after { - content: ''; +/* Info sobre la imagen: chips de categoría + título; detalle al pasar el cursor */ +.ruta-info { position: absolute; - inset: 0; - background: linear-gradient(180deg, + left: 0; + right: 0; + bottom: 0; + z-index: 1; + display: flex; + flex-direction: column; + gap: 6px; + padding: 56px 20px 16px; + background: linear-gradient(180deg, transparent 0%, - transparent 20%, - rgba(10, 10, 20, 0.6) 50%, - rgba(10, 10, 20, 0.95) 80%, - rgba(10, 10, 20, 1) 100%); - pointer-events: none; + rgba(2, 8, 23, 0.5) 40%, + rgba(2, 8, 23, 0.92) 100%); } -/* Botón cerrar */ -.modal-close { - position: absolute; - top: 16px; - right: 16px; - width: 48px; - height: 48px; - border-radius: 50%; - background: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - border: 2px solid rgba(255, 255, 255, 0.2); - color: rgba(255, 255, 255, 0.9); - cursor: pointer; +/* Distintivo de categoría: color + icono (puede haber varias) */ +.ruta-cats { display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.ruta-cat-chip { + display: inline-flex; align-items: center; - justify-content: center; - will-change: transform; - transition: background 0.3s ease, border-color 0.3s ease, color 0.3s ease, transform 0.3s ease; - z-index: 100; + gap: 5px; + font-family: 'Chakra Petch', sans-serif; + font-size: 0.62rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--cat-color, #93c5fd); + padding: 3px 9px; + border-radius: 999px; + border: 1px solid color-mix(in srgb, var(--cat-color, #93c5fd) 45%, transparent); + background: rgba(6, 9, 20, 0.55); +} + +.ruta-cat-chip img { + width: 12px; + height: 12px; } -.modal-close:hover { - background: rgba(255, 255, 255, 0.15); - border-color: rgba(255, 255, 255, 0.3); +.ruta-titulo { + font-family: 'Chakra Petch', sans-serif; + font-size: clamp(1.05rem, 1.7vw, 1.4rem); + font-weight: 700; color: white; - transform: scale(1.1); + margin: 0; + line-height: 1.2; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6); } -/* Contenido principal del modal */ -.modal-content-wrapper { - position: relative; - z-index: 10; +.ruta-desc { + font-family: 'Chakra Petch', sans-serif; + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.82); + line-height: 1.45; + margin: 0; + max-height: 0; + opacity: 0; + overflow: hidden; + transition: max-height 0.45s ease, opacity 0.35s ease; +} + +.ruta-techs { display: flex; - flex-direction: column; - height: 100%; - padding: 0; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: rgba(147, 197, 253, 0.3) transparent; + flex-wrap: wrap; + gap: 5px; + max-height: 0; + opacity: 0; + overflow: hidden; + transition: max-height 0.45s ease, opacity 0.35s ease; } -.modal-content-wrapper::-webkit-scrollbar { - width: 6px; +.ruta-item:hover .ruta-desc, +.ruta-item:focus-visible .ruta-desc { + max-height: 5.5em; + opacity: 1; } -.modal-content-wrapper::-webkit-scrollbar-track { - background: transparent; +.ruta-item:hover .ruta-techs, +.ruta-item:focus-visible .ruta-techs { + max-height: 4em; + opacity: 1; } -.modal-content-wrapper::-webkit-scrollbar-thumb { - background: rgba(147, 197, 253, 0.3); - border-radius: 3px; +.ruta-tech { + font-family: 'Chakra Petch', sans-serif; + font-size: 0.68rem; + padding: 3px 9px; + border-radius: 6px; + border: 1px solid rgba(147, 197, 253, 0.25); + background: rgba(8, 12, 26, 0.55); + color: #bfdbfe; } -/* Sección hero con poster y título */ -.modal-hero { +/* ── Subtarjetas: trabajos concretos que cuelgan del proyecto ───────────── + Columna conectada al lateral de la tarjeta padre por un raíl vertical en + el color del proyecto. Cada subtarjeta es una "estación" del raíl: un + punto en su color de categoría y un ramal corto hasta la tarjeta. El + hueco del raíl lo reserva el padding lateral de .ruta-subs. */ +.ruta-subs { + position: relative; + flex: 0 1 340px; + min-width: 250px; display: flex; - align-items: flex-end; - gap: 30px; - padding: 40vh 48px 28px 48px; - min-height: 55vh; + flex-direction: column; + gap: 14px; + padding-left: 26px; } -/* Poster del proyecto - Cuadrado */ -.modal-poster-container { - flex-shrink: 0; - width: 240px; - height: 240px; - position: relative; +.ruta-entry--der .ruta-subs { + padding-left: 0; + padding-right: 26px; } -.modal-poster { - width: 100%; - height: 100%; - border-radius: 12px; - object-fit: cover; - box-shadow: - 0 20px 50px rgba(0, 0, 0, 0.6), - 0 0 0 1px rgba(255, 255, 255, 0.1); - transition: transform 0.4s ease, box-shadow 0.4s ease; +/* Raíl vertical en el color del proyecto padre, desvanecido en los extremos */ +.ruta-subs::before { + content: ''; + position: absolute; + top: 8px; + bottom: 8px; + left: 5px; + width: 2px; + border-radius: 2px; + background: linear-gradient( + 180deg, + transparent 0%, + color-mix(in srgb, var(--cat-color, #93c5fd) 80%, transparent) 16%, + color-mix(in srgb, var(--cat-color, #93c5fd) 80%, transparent) 84%, + transparent 100% + ); + box-shadow: 0 0 10px color-mix(in srgb, var(--cat-color, #93c5fd) 40%, transparent); } -.modal-poster:hover { - transform: scale(1.03); - box-shadow: - 0 25px 60px rgba(0, 0, 0, 0.7), - 0 0 0 1px rgba(255, 255, 255, 0.2); +.ruta-entry--der .ruta-subs::before { + left: auto; + right: 5px; } -/* Info del héroe */ -.modal-hero-info { - flex: 1; - min-width: 0; - padding-bottom: 10px; +/* Nodo de unión con el padre: rombo a su altura (la entrada centra en vertical) */ +.ruta-subs::after { + content: ''; + position: absolute; + top: 50%; + left: 6px; + width: 11px; + height: 11px; + transform: translate(-50%, -50%) rotate(45deg); + border-radius: 2px; + background: var(--cat-color, #93c5fd); + border: 2px solid rgba(5, 8, 18, 0.92); + box-shadow: 0 0 14px color-mix(in srgb, var(--cat-color, #93c5fd) 75%, transparent); } -/* Título grande estilo película */ -.modal-titulo { - font-family: 'Chakra Petch', sans-serif; - font-size: clamp(2rem, 5vw, 3.5rem); - font-weight: 700; - color: white; - margin: 0 0 16px 0; - line-height: 1.1; - text-shadow: - 0 4px 20px rgba(0, 0, 0, 0.8), - 0 2px 4px rgba(0, 0, 0, 0.5); - letter-spacing: -0.02em; +.ruta-entry--der .ruta-subs::after { + left: auto; + right: 6px; + transform: translate(50%, -50%) rotate(45deg); } -/* Metadatos estilo streaming */ -.modal-meta { +.ruta-sub { + position: relative; display: flex; align-items: center; - gap: 16px; - margin-bottom: 20px; - flex-wrap: wrap; + gap: 12px; + padding: 12px; + border-radius: 16px; + background: linear-gradient(160deg, rgba(16, 22, 42, 0.85), rgba(8, 12, 26, 0.92)); + border: 1px solid rgba(255, 255, 255, 0.1); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35); + transition: border-color 0.3s ease, box-shadow 0.3s ease, background 0.3s ease; + cursor: default; } -.modal-meta-item { - font-family: 'Chakra Petch', sans-serif; - font-size: 0.9rem; - color: rgba(255, 255, 255, 0.7); - display: flex; - align-items: center; - gap: 6px; +/* Ramal desde el raíl hasta la subtarjeta, en el color de su categoría */ +.ruta-sub::before { + content: ''; + position: absolute; + top: 50%; + left: -20px; + width: 20px; + height: 2px; + transform: translateY(-50%); + border-radius: 2px; + background: color-mix(in srgb, var(--cat-color, #93c5fd) 80%, transparent); } -.modal-meta-badge { - padding: 4px 10px; - background: rgba(255, 255, 255, 0.15); - border-radius: 4px; - font-size: 0.8rem; - font-weight: 600; - color: white; - border: 1px solid rgba(255, 255, 255, 0.2); +/* Estación: punto sobre el raíl a la altura de la subtarjeta */ +.ruta-sub::after { + content: ''; + position: absolute; + top: 50%; + left: -20px; + width: 8px; + height: 8px; + transform: translate(-50%, -50%); + border-radius: 50%; + background: var(--cat-color, #93c5fd); + border: 2px solid rgba(5, 8, 18, 0.92); + box-shadow: 0 0 10px color-mix(in srgb, var(--cat-color, #93c5fd) 60%, transparent); } -/* Descripción corta / tagline */ -.modal-tagline { - font-family: 'Chakra Petch', sans-serif; - font-size: 1.1rem; - font-style: italic; - color: rgba(255, 255, 255, 0.85); - margin: 0 0 16px 0; - line-height: 1.5; +.ruta-entry--der .ruta-sub::before { + left: auto; + right: -20px; } -/* Acciones principales */ -.modal-actions { - display: flex; - align-items: center; - gap: 12px; - margin-top: 24px; - flex-wrap: wrap; +.ruta-entry--der .ruta-sub::after { + left: auto; + right: -20px; + transform: translate(50%, -50%); } -.modal-btn-primary { - display: inline-flex; - align-items: center; - gap: 10px; - padding: 14px 32px; - background: white; - border: none; - border-radius: 8px; - color: #0a0a14; - font-family: 'Chakra Petch', sans-serif; - font-weight: 700; - font-size: 1rem; - text-decoration: none; +/* Subtarjetas clickeables: sin transform, para que la estación no se + despegue del raíl al hacer hover */ +.ruta-sub[href] { cursor: pointer; + text-decoration: none; will-change: transform; transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } -.modal-btn-primary:hover { - transform: scale(1.05); - box-shadow: 0 8px 30px rgba(255, 255, 255, 0.3); +.ruta-sub[href]:hover, +.ruta-sub[href]:focus-visible { + border-color: color-mix(in srgb, var(--cat-color, #93c5fd) 60%, transparent); + background: linear-gradient(160deg, rgba(22, 30, 56, 0.9), rgba(10, 15, 32, 0.95)); + box-shadow: + 0 12px 34px rgba(0, 0, 0, 0.45), + 0 0 22px color-mix(in srgb, var(--cat-color, #93c5fd) 22%, transparent); +} + +.ruta-sub[href]:focus-visible { + outline: 2px solid var(--cat-color, #93c5fd); + outline-offset: 3px; +} + +/* Miniatura cuadrada; si no hay imagen (o falla) muestra el icono de la + categoría sobre un fondo tintado, como las tarjetas principales */ +.ruta-sub-media { + position: relative; + width: 64px; + height: 64px; + flex-shrink: 0; + border-radius: 12px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.1); + background: + radial-gradient(ellipse at 50% 40%, color-mix(in srgb, var(--cat-color, #93c5fd) 26%, transparent), transparent 75%), + linear-gradient(135deg, rgba(20, 26, 48, 0.95), rgba(8, 12, 26, 0.98)); +} + +.ruta-sub-fallback { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 55%; + opacity: 0.9; + filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.5)); +} + +.ruta-sub-img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.35s ease; +} + +.ruta-sub[href]:hover .ruta-sub-img { + transform: scale(1.07); } -.modal-btn-primary svg { +.ruta-sub.sin-imagen .ruta-sub-img, +.ruta-sub.sin-imagen .ruta-sub-cat { + display: none; +} + +/* Insignia de categoría sobre la esquina de la miniatura */ +.ruta-sub-cat { + position: absolute; + right: 3px; + bottom: 3px; + display: grid; + place-items: center; width: 20px; height: 20px; + border-radius: 7px; + background: rgba(5, 8, 18, 0.85); + border: 1px solid color-mix(in srgb, var(--cat-color, #93c5fd) 55%, transparent); } .modal-btn-secondary { @@ -773,174 +861,37 @@ transition: background 0.3s ease, border-color 0.3s ease, transform 0.3s ease; } -.modal-btn-secondary:hover { - background: rgba(255, 255, 255, 0.2); - border-color: rgba(255, 255, 255, 0.4); - transform: scale(1.1); -} - -.modal-btn-secondary svg { - width: 22px; - height: 22px; -} - -/* Cuerpo del modal con detalles */ -.modal-body { +.ruta-sub-body { flex: 1; - padding: 0 48px 40px 48px; - background: rgba(10, 10, 20, 1); -} - -/* Descripción del proyecto */ -.modal-description { - max-width: 800px; - margin-bottom: 24px; -} - -.modal-description:empty { - display: none; - margin-bottom: 0; -} - -.modal-description-title { - font-family: 'Chakra Petch', sans-serif; - font-size: 0.85rem; - font-weight: 600; - color: rgba(255, 255, 255, 0.5); - text-transform: uppercase; - letter-spacing: 0.1em; - margin: 0 0 12px 0; -} - -.modal-description-text { - font-family: 'Chakra Petch', sans-serif; - font-size: 1rem; - color: rgba(255, 255, 255, 0.85); - line-height: 1.7; - margin: 0; -} - -/* Tecnologías / Tags */ -.modal-tecnologias { + min-width: 0; display: flex; - flex-wrap: wrap; - gap: 10px; - margin-top: 20px; -} - -.modal-tecnologias:empty { - display: none; - margin-top: 0; -} - -.modal-tech-tag { - font-family: 'Chakra Petch', sans-serif; - font-size: 0.8rem; - font-weight: 500; - padding: 8px 16px; - background: rgba(147, 197, 253, 0.15); - border: 1px solid rgba(147, 197, 253, 0.3); - border-radius: 20px; - color: rgba(200, 220, 255, 0.95); - letter-spacing: 0.02em; - transition: background 0.3s ease, border-color 0.3s ease, transform 0.3s ease; -} - -.modal-tech-tag:hover { - background: rgba(147, 197, 253, 0.25); - border-color: rgba(147, 197, 253, 0.5); - transform: translateY(-2px); + flex-direction: column; + gap: 3px; } -/* Contenido HTML del proyecto */ -.modal-contenido { +.ruta-sub-fecha { font-family: 'Chakra Petch', sans-serif; - color: rgba(255, 255, 255, 0.85); - line-height: 1.7; - margin-top: 28px; - padding-top: 28px; - border-top: 1px solid rgba(255, 255, 255, 0.1); -} - -.modal-contenido:empty { - display: none; - margin-top: 0; - padding-top: 0; - border-top: none; -} - -.modal-contenido h3 { - font-size: 1.3rem; + font-size: 0.6rem; font-weight: 700; - color: white; - margin: 0 0 16px 0; -} - -.modal-contenido h4 { - font-size: 1.05rem; - font-weight: 600; - color: rgba(200, 220, 255, 1); - margin: 24px 0 12px 0; -} - -.modal-contenido p { - margin: 0 0 16px 0; - color: rgba(255, 255, 255, 0.8); -} - -.modal-contenido ul, .modal-contenido ol { - margin: 0 0 16px 0; - padding-left: 24px; -} - -.modal-contenido li { - margin-bottom: 8px; - color: rgba(255, 255, 255, 0.8); + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--cat-color, #93c5fd); } -.modal-contenido strong { - color: rgba(147, 197, 253, 1); +.ruta-sub-titulo { + font-family: 'Chakra Petch', sans-serif; + font-size: 0.88rem; font-weight: 600; + color: white; + line-height: 1.3; } -.modal-contenido code { - font-family: 'JetBrains Mono', monospace; - font-size: 0.85em; - background: rgba(0, 0, 0, 0.4); - padding: 2px 8px; - border-radius: 4px; - color: rgba(196, 181, 253, 1); -} - -.modal-contenido pre { - background: rgba(0, 0, 0, 0.5); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 12px; - padding: 16px; - overflow-x: auto; - margin: 16px 0; -} - -.modal-contenido pre code { - background: none; - padding: 0; -} - -.modal-contenido blockquote { - margin: 20px 0; - padding: 16px 20px; - background: linear-gradient(135deg, rgba(147, 197, 253, 0.1), rgba(196, 181, 253, 0.08)); - border-left: 4px solid rgba(147, 197, 253, 0.6); - border-radius: 0 12px 12px 0; - font-style: italic; - color: rgba(255, 255, 255, 0.9); -} - -.modal-contenido img { - max-width: 100%; - border-radius: 12px; - margin: 16px 0; - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3); +.ruta-sub-desc { + font-family: 'Chakra Petch', sans-serif; + font-size: 0.74rem; + color: rgba(255, 255, 255, 0.68); + line-height: 1.45; + margin: 0; } /* ============================================ @@ -976,10 +927,6 @@ .proyecto-titulo { font-size: 0.65rem; } - - .proyectos-grid-list { - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - } } @media (max-width: 1024px) { @@ -1008,243 +955,155 @@ height: 38px; } - .aptitud-header { - padding: 20px; - gap: 16px; - } - - .aptitud-icono { - width: 60px; - height: 60px; - } - - .aptitud-titulo { - font-size: 1.3rem; - } - - .aptitud-descripcion { - font-size: 0.85rem; + .ruta-entry { + gap: 26px; } - .modal-backdrop { - height: 58%; - } - - .modal-hero { - padding: 30vh 32px 24px 32px; - gap: 24px; - min-height: 50vh; - } - - .modal-poster-container { - width: 200px; - height: 200px; - } - - .modal-body { - padding: 0 32px 32px 32px; + .ruta-subs { + flex-basis: 280px; } } -@media (max-width: 768px) { - .proyectos-header { - padding: 6vh 16px 3vh; - } - - .proyectos-hero { - min-height: 35vh; - padding: 10px; - } - - .proyectos-hero-inner { - max-width: 340px; - height: 340px; - } - - .proyectos-logo { - width: 130px; - height: 130px; - } - - .proyecto-card { - width: 75px; - height: 75px; - } - - .proyecto-card-inner { - width: 75px; - height: 75px; - } - - .proyecto-icon img { - width: 34px; - height: 34px; - } - - .proyecto-titulo { - font-size: 0.6rem; - max-width: 70px; - } - - .proyectos-content { - padding: 0 16px 6vh; - gap: 8vh; +/* Las subtarjetas pasan debajo de la tarjeta cuando no caben al lado */ +@media (max-width: 980px) { + .ruta-entry { + flex-wrap: wrap; + row-gap: 22px; } - .proyectos-grid-list { - grid-template-columns: 1fr; - gap: 16px; + .ruta-subs { + flex: 1 1 100%; + max-width: var(--w, 500px); + padding-left: 0; + padding-right: 0; } - .aptitud-header { - flex-direction: column; - text-align: center; - padding: 20px; - gap: 14px; - } - - .aptitud-icono { - width: 55px; - height: 55px; - } - - .aptitud-icono img { - width: 28px; - height: 28px; - } - - .aptitud-titulo { - font-size: 1.2rem; - } - - .proyecto-detail-card { - flex-direction: column; + .ruta-entry--izq .ruta-subs { + margin-right: auto; } - .proyecto-image-container { - height: 160px; + .ruta-entry--der .ruta-subs { + margin-left: auto; } - .proyecto-info-wrapper { - padding: 16px; - } - - .proyecto-detail-titulo { - font-size: 1.1rem; + /* Sin raíl ni estaciones cuando las subtarjetas pasan debajo */ + .ruta-subs::before, + .ruta-subs::after, + .ruta-sub::before, + .ruta-sub::after { + display: none; } +} - .proyecto-detail-desc { - font-size: 0.85rem; +@media (max-width: 760px) { + /* La cronología pasa a línea de tiempo: línea recta a la izquierda */ + .proyectos-mapa { + padding: 2vh 16px 8vh 44px; } - /* Modal responsive */ - .proyecto-modal-overlay { - padding: 12px; - align-items: flex-start; - padding-top: 40px; + /* En táctil ocupa varias filas: mejor estática para no tapar contenido */ + .ruta-filtros { + position: static; + padding: 8px 10px; + gap: 6px; + margin-bottom: 5vh; } - .proyecto-modal { - max-height: 88vh; - border-radius: 16px; - } - - .modal-backdrop { - border-radius: 16px 16px 0 0; - height: 54%; + .ruta-filtro { + padding: 6px 10px; + font-size: 0.72rem; } - .modal-hero { + .ruta-entry, + .ruta-entry--der { flex-direction: column; - align-items: center; - text-align: center; - padding: 8vh 20px 14px 20px; - gap: 16px; - min-height: auto; - } - - .modal-poster-container { - width: 160px; - height: 160px; + align-items: stretch; + gap: 18px; + padding-left: 0; + padding-right: 0; + margin-bottom: 9vh; } - .modal-hero-info { - display: flex; - flex-direction: column; - align-items: center; + .ruta-item { + flex-basis: auto; + min-width: 0; + width: 100%; } - .modal-titulo { - font-size: 1.6rem; - text-align: center; - line-height: 1.2; - margin-bottom: 12px; + .ruta-entry--der .ruta-fecha { + right: auto; + left: 14px; } - .modal-meta { - justify-content: center; - font-size: 0.85rem; + .ruta-subs { + max-width: none; gap: 12px; } - .modal-meta-badge { - font-size: 0.75rem; - padding: 3px 8px; + .ruta-año { + position: relative; + flex-direction: row; + align-items: center; + margin: 1vh 0 5vh; } - .modal-tagline { - text-align: center; - font-size: 0.95rem; - line-height: 1.4; + .ruta-año-dot { + position: absolute; + left: -36px; + width: 16px; + height: 16px; } - .modal-actions { - justify-content: center; - margin-top: 16px; + /* Sin hover en táctil: la descripción y las techs se muestran siempre */ + .ruta-desc, + .ruta-techs { + max-height: none; + opacity: 1; } - .modal-btn-primary { - padding: 12px 24px; - font-size: 0.9rem; + .ruta-info { + padding: 44px 14px 13px; } +} - .modal-btn-secondary { - width: 44px; - height: 44px; +@media (max-width: 768px) { + .proyectos-header { + padding: 6vh 16px 3vh; } - .modal-body { - padding: 0 20px 24px 20px; + .proyectos-hero { + min-height: 35vh; + padding: 10px; } - .modal-description { - text-align: left; + .proyectos-hero-inner { + max-width: 340px; + height: 340px; } - .modal-description-text { - font-size: 0.9rem; - line-height: 1.6; + .proyectos-logo { + width: 110px; /* Reducido para evitar solapamiento */ + height: 110px; } - .modal-tecnologias { - justify-content: center; - gap: 8px; + .proyecto-card { + width: 65px; /* Reducido */ + height: 65px; } - .modal-tech-tag { - font-size: 0.75rem; - padding: 6px 12px; + .proyecto-card-inner { + width: 65px; + height: 65px; } - .modal-contenido { - font-size: 0.9rem; + .proyecto-icon img { + width: 30px; + height: 30px; } - .modal-close { - top: 10px; - right: 10px; - width: 40px; - height: 40px; + .proyecto-titulo { + font-size: 0.6rem; + max-width: 70px; } } @@ -1263,168 +1122,47 @@ } .proyectos-logo { - width: 100px; - height: 100px; + width: 90px; /* Reducido */ + height: 90px; } .proyecto-card { - width: 65px; - height: 65px; + width: 55px; /* Reducido */ + height: 55px; } .proyecto-card-inner { - width: 65px; - height: 65px; + width: 55px; + height: 55px; } .proyecto-icon img { - width: 28px; - height: 28px; - } - - .proyecto-titulo { - display: none; - } - - .proyectos-content { - padding: 0 12px 5vh; - gap: 6vh; - } - - .aptitud-icono { - width: 48px; - height: 48px; - } - - .aptitud-icono img { width: 24px; height: 24px; } - .aptitud-titulo { - font-size: 1.05rem; - } - - .aptitud-descripcion { - font-size: 0.8rem; - } - - .proyecto-detail-card { - flex-direction: column; - border-radius: 14px; - } - - .proyecto-image-container { - height: 140px; - } - - .proyecto-info-wrapper { - padding: 14px; - } - - .proyecto-detail-titulo { - font-size: 0.95rem; - } - - .proyecto-detail-desc { - font-size: 0.8rem; - } - - /* Modal extra small */ - .proyecto-modal-overlay { - padding: 8px; - padding-top: 30px; - } - - .proyecto-modal { - max-height: 90vh; - border-radius: 12px; - } - - .modal-backdrop { - border-radius: 12px 12px 0 0; - height: 40%; + .proyecto-titulo { + display: none; } - .modal-hero { - padding: 15vh 16px 16px 16px; - gap: 12px; + .proyectos-mapa { + padding: 1vh 12px 7vh 38px; } - .modal-poster-container { - width: 140px; - height: 140px; + .ruta-año-dot { + left: -30px; } - .modal-titulo { - font-size: 1.35rem; - margin-bottom: 8px; + .ruta-titulo { + font-size: 1rem; } - .modal-meta { + .ruta-desc { font-size: 0.8rem; - gap: 10px; - } - - .modal-meta-badge { - font-size: 0.7rem; - padding: 2px 6px; - } - - .modal-tagline { - font-size: 0.85rem; - line-height: 1.4; - } - - .modal-actions { - margin-top: 12px; - gap: 10px; - } - - .modal-btn-primary { - padding: 10px 20px; - font-size: 0.85rem; - } - - .modal-btn-secondary { - width: 40px; - height: 40px; - } - - .modal-body { - padding: 0 16px 20px 16px; - } - - .modal-description-text { - font-size: 0.85rem; - } - - .modal-tech-tag { - font-size: 0.7rem; - padding: 5px 10px; - } - - .modal-contenido { - font-size: 0.85rem; - } - - .modal-contenido h3 { - font-size: 1.1rem; - } - - .modal-contenido h4 { - font-size: 0.95rem; - } - - .modal-info-grid { - grid-template-columns: 1fr; - gap: 16px; } - .modal-close { - width: 36px; - height: 36px; - top: 8px; - right: 8px; + .ruta-fecha { + font-size: 0.72rem; + padding: 6px 11px; } } diff --git a/src/utils/fechas.js b/src/utils/fechas.js new file mode 100644 index 0000000..6f0fa92 --- /dev/null +++ b/src/utils/fechas.js @@ -0,0 +1,12 @@ +// Fecha con precisión variable: "YYYY", "YYYY-MM" o "YYYY-MM-DD". +// Se muestra con la precisión que tenga (compartido por la portada y las fichas). +const MESES = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre']; + +export function formatFecha(f) { + if (!f) return ''; + const [y, m, d] = String(f).split('-'); + if (!m) return y; + const mes = MESES[Number(m) - 1] ?? ''; + if (d) return `${Number(d)} de ${mes} de ${y}`; + return `${mes.charAt(0).toUpperCase()}${mes.slice(1)} ${y}`; +}