Segundo vistaso

This commit is contained in:
unknown
2026-05-26 23:08:21 -04:00
parent bd93fb3bf2
commit 3d063113d1
16 changed files with 1158 additions and 2087 deletions

616
package-lock.json generated
View File

@@ -8,9 +8,12 @@
"name": "landing-page", "name": "landing-page",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@tailwindcss/postcss": "^4.3.0",
"next": "16.2.6", "next": "16.2.6",
"postcss": "^8.5.15",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4" "react-dom": "19.2.4",
"tailwindcss": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
@@ -21,6 +24,18 @@
"typescript": "^5" "typescript": "^5"
} }
}, },
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.29.7", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -265,7 +280,6 @@
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -287,7 +301,6 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -974,7 +987,6 @@
"version": "0.3.13", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/sourcemap-codec": "^1.5.0",
@@ -985,7 +997,6 @@
"version": "2.3.5", "version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/gen-mapping": "^0.3.5",
@@ -996,7 +1007,6 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@@ -1006,14 +1016,12 @@
"version": "1.5.5", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31", "version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
@@ -1024,7 +1032,6 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -1247,11 +1254,266 @@
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
}, },
"node_modules/@tailwindcss/node": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz",
"integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"enhanced-resolve": "^5.21.0",
"jiti": "^2.6.1",
"lightningcss": "1.32.0",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.3.0"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz",
"integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==",
"license": "MIT",
"engines": {
"node": ">= 20"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.3.0",
"@tailwindcss/oxide-darwin-arm64": "4.3.0",
"@tailwindcss/oxide-darwin-x64": "4.3.0",
"@tailwindcss/oxide-freebsd-x64": "4.3.0",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0",
"@tailwindcss/oxide-linux-arm64-gnu": "4.3.0",
"@tailwindcss/oxide-linux-arm64-musl": "4.3.0",
"@tailwindcss/oxide-linux-x64-gnu": "4.3.0",
"@tailwindcss/oxide-linux-x64-musl": "4.3.0",
"@tailwindcss/oxide-wasm32-wasi": "4.3.0",
"@tailwindcss/oxide-win32-arm64-msvc": "4.3.0",
"@tailwindcss/oxide-win32-x64-msvc": "4.3.0"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz",
"integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz",
"integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz",
"integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz",
"integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz",
"integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz",
"integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz",
"integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz",
"integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz",
"integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz",
"integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util",
"@emnapi/wasi-threads",
"tslib"
],
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.10.0",
"@emnapi/runtime": "^1.10.0",
"@emnapi/wasi-threads": "^1.2.1",
"@napi-rs/wasm-runtime": "^1.1.4",
"@tybys/wasm-util": "^0.10.1",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
"integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz",
"integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/postcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.0.tgz",
"integrity": "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==",
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@tailwindcss/node": "4.3.0",
"@tailwindcss/oxide": "4.3.0",
"postcss": "^8.5.10",
"tailwindcss": "4.3.0"
}
},
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.2", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -2566,7 +2828,6 @@
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -2613,6 +2874,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/enhanced-resolve": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz",
"integrity": "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.3.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/es-abstract": { "node_modules/es-abstract": {
"version": "1.24.2", "version": "1.24.2",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
@@ -3547,6 +3821,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/has-bigints": { "node_modules/has-bigints": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -4157,6 +4437,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/jiti": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4284,6 +4573,255 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.32.0",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-darwin-x64": "1.32.0",
"lightningcss-freebsd-x64": "1.32.0",
"lightningcss-linux-arm-gnueabihf": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-arm64-musl": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0",
"lightningcss-linux-x64-musl": "1.32.0",
"lightningcss-win32-arm64-msvc": "1.32.0",
"lightningcss-win32-x64-msvc": "1.32.0"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
"cpu": [
"arm"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -4330,6 +4868,15 @@
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -4805,6 +5352,34 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -5501,6 +6076,25 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/tailwindcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz",
"integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==",
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
"integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.16", "version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",

View File

@@ -9,9 +9,12 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/postcss": "^4.3.0",
"next": "16.2.6", "next": "16.2.6",
"postcss": "^8.5.15",
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4" "react-dom": "19.2.4",
"tailwindcss": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",

5
postcss.config.mjs Normal file
View File

@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};

View File

@@ -1,9 +1,6 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap'); @import "tailwindcss";
/* ============================================ @theme {
DESIGN SYSTEM TOKENS
============================================ */
:root {
/* Colors */ /* Colors */
--color-black: #0a0a0a; --color-black: #0a0a0a;
--color-dark: #111111; --color-dark: #111111;
@@ -11,6 +8,7 @@
--color-gray-800: #2d2d2d; --color-gray-800: #2d2d2d;
--color-gray-600: #555555; --color-gray-600: #555555;
--color-gray-400: #888888; --color-gray-400: #888888;
--color-gray-300: #d1d1d1;
--color-gray-200: #e5e5e5; --color-gray-200: #e5e5e5;
--color-gray-100: #f5f5f5; --color-gray-100: #f5f5f5;
--color-gray-50: #fafafa; --color-gray-50: #fafafa;
@@ -25,290 +23,91 @@
--color-success: #00c853; --color-success: #00c853;
--color-error: #ff3b3b; --color-error: #ff3b3b;
/* Typography */ /* Fonts */
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--text-4xl: 2.25rem;
--text-5xl: 3rem;
--text-6xl: 3.75rem;
--text-7xl: 4.5rem;
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
--space-16: 4rem;
--space-20: 5rem;
--space-24: 6rem;
--space-32: 8rem;
/* Border Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-2xl: 24px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.10);
--shadow-xl: 0 24px 64px rgba(0, 0, 0, 0.12);
/* Transitions */ /* Transitions */
--transition-fast: 150ms ease; --transition-fast: 150ms ease;
--transition-base: 250ms ease; --transition-base: 250ms ease;
--transition-slow: 400ms ease; --transition-slow: 400ms ease;
/* Max widths */
--max-width-sm: 640px;
--max-width-md: 768px;
--max-width-lg: 1024px;
--max-width-xl: 1280px;
--max-width-2xl: 1440px;
} }
/* ============================================ @layer base {
RESET & BASE *, *::before, *::after {
============================================ */ box-sizing: border-box;
*, *::before, *::after { margin: 0;
box-sizing: border-box; padding: 0;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: var(--font-sans);
font-size: var(--text-base);
color: var(--color-dark);
background-color: var(--color-white);
line-height: 1.6;
overflow-x: hidden;
}
img, video {
max-width: 100%;
height: auto;
display: block;
}
a {
color: inherit;
text-decoration: none;
}
button {
cursor: pointer;
font-family: var(--font-sans);
border: none;
outline: none;
background: none;
}
input, textarea, select {
font-family: var(--font-sans);
}
/* ============================================
UTILITIES
============================================ */
.container {
width: 100%;
max-width: var(--max-width-xl);
margin: 0 auto;
padding: 0 var(--space-6);
}
.section {
padding: var(--space-24) 0;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-6);
font-size: var(--text-base);
font-weight: 600;
border-radius: var(--radius-md);
transition: all var(--transition-base);
letter-spacing: -0.01em;
white-space: nowrap;
}
.btn-primary {
background: var(--color-black);
color: var(--color-white);
border: 2px solid var(--color-black);
}
.btn-primary:hover {
background: var(--color-gray-900);
border-color: var(--color-gray-900);
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.btn-secondary {
background: transparent;
color: var(--color-dark);
border: 2px solid var(--color-gray-200);
}
.btn-secondary:hover {
border-color: var(--color-dark);
transform: translateY(-1px);
}
.btn-accent {
background: var(--color-accent);
color: var(--color-white);
border: 2px solid var(--color-accent);
}
.btn-accent:hover {
background: var(--color-accent-dark);
border-color: var(--color-accent-dark);
transform: translateY(-1px);
box-shadow: 0 8px 24px rgba(0, 102, 255, 0.3);
}
.btn-lg {
padding: var(--space-4) var(--space-8);
font-size: var(--text-lg);
border-radius: var(--radius-lg);
}
/* Badges */
.badge {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
font-size: var(--text-xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
border-radius: var(--radius-full);
}
.badge-dark {
background: var(--color-black);
color: var(--color-white);
}
.badge-accent {
background: var(--color-accent-light);
color: var(--color-accent);
}
/* ============================================
SCROLLBAR
============================================ */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: var(--color-gray-100);
}
::-webkit-scrollbar-thumb {
background: var(--color-gray-400);
border-radius: var(--radius-full);
}
/* ============================================
ANIMATIONS
============================================ */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(24px);
} }
to {
opacity: 1; html {
transform: translateY(0); scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: var(--font-sans);
font-size: 1rem;
color: var(--color-dark);
background-color: var(--color-white);
line-height: 1.6;
overflow-x: hidden;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: var(--color-gray-100);
}
::-webkit-scrollbar-thumb {
background: var(--color-gray-400);
border-radius: 9999px;
} }
} }
@keyframes fadeIn { @layer components {
from { opacity: 0; } /* Layout */
to { opacity: 1; }
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.animate-fadeInUp {
animation: fadeInUp 0.6s ease forwards;
}
.animate-fadeIn {
animation: fadeIn 0.5s ease forwards;
}
/* ============================================
RESPONSIVE
============================================ */
@media (max-width: 768px) {
.container { .container {
padding: 0 var(--space-4); @apply w-full max-w-7xl mx-auto px-6 md:px-4;
} }
.section { .section {
padding: var(--space-16) 0; @apply py-16 md:py-24;
}
/* Buttons */
.btn {
@apply inline-flex items-center justify-center gap-2 px-6 py-3 text-base font-semibold rounded-lg transition-all duration-250 ease-out whitespace-nowrap cursor-pointer tracking-tight;
}
.btn-primary {
@apply bg-black text-white border-2 border-black hover:bg-gray-900 hover:border-gray-900 hover:-translate-y-[1px] hover:shadow-[0_8px_32px_rgba(0,0,0,0.10)];
}
.btn-secondary {
@apply bg-transparent text-dark border-2 border-gray-200 hover:border-dark hover:-translate-y-[1px];
}
.btn-accent {
@apply bg-accent text-white border-2 border-accent hover:bg-accent-dark hover:border-accent-dark hover:-translate-y-[1px] hover:shadow-[0_8px_24px_rgba(0,102,255,0.3)];
}
.btn-lg {
@apply px-8 py-4 text-lg rounded-xl;
}
/* Badges */
.badge {
@apply inline-flex items-center gap-2 px-3 py-2 text-xs font-semibold uppercase tracking-widest rounded-full;
}
.badge-dark {
@apply bg-black text-white;
}
.badge-accent {
@apply bg-accent-light text-accent;
} }
} }

View File

@@ -1,367 +0,0 @@
.contactSection {
background: var(--color-white);
}
/* Layout */
.layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-16);
align-items: start;
}
/* Reveal */
.reveal {
opacity: 0;
transform: translateY(24px);
transition: opacity 0.65s ease, transform 0.65s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
/* Info panel */
.infoPanel {
display: flex;
flex-direction: column;
gap: var(--space-6);
position: sticky;
top: calc(72px + var(--space-8));
}
.title {
font-size: clamp(var(--text-3xl), 5vw, var(--text-5xl));
font-weight: 900;
letter-spacing: -0.04em;
line-height: 1.05;
color: var(--color-black);
}
.subtitle {
font-size: var(--text-lg);
color: var(--color-gray-600);
line-height: 1.6;
}
/* Contact details */
.contactDetails {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding: var(--space-6);
background: var(--color-gray-50);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-xl);
}
.contactItem {
display: flex;
align-items: center;
gap: var(--space-4);
}
.contactIcon {
width: 40px;
height: 40px;
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--color-black);
}
.contactLabel {
font-size: var(--text-xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-gray-400);
}
.contactValue {
font-size: var(--text-base);
font-weight: 600;
color: var(--color-black);
}
/* Testimonial */
.testimonial {
background: var(--color-black);
color: var(--color-white);
border-radius: var(--radius-xl);
padding: var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.testimonialText {
font-size: var(--text-base);
line-height: 1.65;
color: rgba(255,255,255,0.85);
font-style: italic;
}
.testimonialAuthor {
display: flex;
align-items: center;
gap: var(--space-3);
}
.authorAvatar {
width: 40px;
height: 40px;
background: rgba(255,255,255,0.15);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.02em;
flex-shrink: 0;
}
.authorName {
font-size: var(--text-sm);
font-weight: 700;
}
.authorRole {
font-size: var(--text-xs);
color: rgba(255,255,255,0.5);
}
/* Form panel */
.formPanel {
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-2xl);
padding: var(--space-10);
box-shadow: var(--shadow-lg);
}
/* Form */
.form {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.formHeader {
margin-bottom: var(--space-2);
}
.formTitle {
font-size: var(--text-2xl);
font-weight: 800;
letter-spacing: -0.03em;
color: var(--color-black);
}
.formSubtitle {
font-size: var(--text-sm);
color: var(--color-gray-400);
margin-top: var(--space-1);
}
/* Row */
.row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-4);
}
/* Field */
.fieldGroup {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.label {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-dark);
}
.required {
color: var(--color-error);
}
.optional {
font-weight: 400;
color: var(--color-gray-400);
}
/* Inputs */
.input,
.textarea {
width: 100%;
padding: var(--space-3) var(--space-4);
font-size: var(--text-base);
font-family: var(--font-sans);
color: var(--color-dark);
background: var(--color-white);
border: 1.5px solid var(--color-gray-200);
border-radius: var(--radius-lg);
transition: all var(--transition-fast);
outline: none;
}
.input::placeholder,
.textarea::placeholder {
color: var(--color-gray-400);
}
.input:focus,
.textarea:focus {
border-color: var(--color-black);
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.06);
}
.inputError {
border-color: var(--color-error) !important;
box-shadow: 0 0 0 3px rgba(255, 59, 59, 0.08) !important;
}
.textarea {
resize: vertical;
min-height: 120px;
}
.textareaFooter {
display: flex;
justify-content: space-between;
align-items: center;
}
/* Error */
.errorMsg {
font-size: var(--text-xs);
color: var(--color-error);
font-weight: 500;
display: flex;
align-items: center;
gap: var(--space-1);
}
.charCount {
font-size: var(--text-xs);
color: var(--color-gray-400);
margin-left: auto;
}
/* Submit */
.submitBtn {
width: 100%;
justify-content: center;
margin-top: var(--space-2);
}
.submitBtn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none !important;
}
/* Spinner */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 18px;
height: 18px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.7s linear infinite;
flex-shrink: 0;
}
/* Privacy note */
.privacyNote {
font-size: var(--text-xs);
color: var(--color-gray-400);
text-align: center;
line-height: 1.5;
}
.privacyLink {
color: var(--color-gray-600);
text-decoration: underline;
text-underline-offset: 2px;
transition: color var(--transition-fast);
}
.privacyLink:hover {
color: var(--color-black);
}
/* Success state */
.successState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: var(--space-4);
padding: var(--space-16) var(--space-8);
animation: scaleIn 0.4s ease;
}
.successIcon {
width: 72px;
height: 72px;
background: var(--color-black);
color: var(--color-white);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.successTitle {
font-size: var(--text-2xl);
font-weight: 800;
letter-spacing: -0.03em;
color: var(--color-black);
}
.successDesc {
font-size: var(--text-base);
color: var(--color-gray-600);
max-width: 320px;
line-height: 1.6;
}
/* Responsive */
@media (max-width: 1024px) {
.layout {
grid-template-columns: 1fr;
gap: var(--space-12);
}
.infoPanel {
position: static;
}
.formPanel {
padding: var(--space-8);
}
}
@media (max-width: 640px) {
.row {
grid-template-columns: 1fr;
}
.formPanel {
padding: var(--space-6);
}
}

View File

@@ -1,7 +1,6 @@
'use client'; 'use client';
import { useState, useRef, useEffect, FormEvent } from 'react'; import { useState, useRef, useEffect, FormEvent } from 'react';
import styles from './ContactForm.module.css';
type FormData = { type FormData = {
name: string; name: string;
@@ -53,12 +52,15 @@ export default function ContactForm() {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => { (entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting) entry.target.classList.add(styles.visible); if (entry.isIntersecting) {
entry.target.classList.add('opacity-100', 'translate-y-0');
entry.target.classList.remove('opacity-0', 'translate-y-6');
}
}); });
}, },
{ threshold: 0.1 } { threshold: 0.1 }
); );
const elements = sectionRef.current?.querySelectorAll(`.${styles.reveal}`); const elements = sectionRef.current?.querySelectorAll('.reveal');
elements?.forEach((el) => observer.observe(el)); elements?.forEach((el) => observer.observe(el));
return () => observer.disconnect(); return () => observer.disconnect();
}, []); }, []);
@@ -115,94 +117,121 @@ export default function ContactForm() {
return ( return (
<section <section
className={`section ${styles.contactSection}`} className="section bg-white"
id="contact" id="contact"
ref={sectionRef} ref={sectionRef}
aria-labelledby="contact-heading" aria-labelledby="contact-heading"
> >
<div className="container"> <div className="container">
<div className={styles.layout}> <div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-16 items-start">
{/* Left info panel */} {/* Left info panel */}
<div className={`${styles.reveal} ${styles.infoPanel}`}> <div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out flex flex-col gap-6 lg:sticky lg:top-[104px]">
<span className="badge badge-dark">Contacto</span> <div className="mb-2">
<h2 id="contact-heading" className={styles.title}> <span className="badge badge-dark">Contacto</span>
</div>
<h2
id="contact-heading"
className="text-[clamp(1.875rem,5vw,3rem)] font-black tracking-[-0.04em] leading-[1.05] text-black"
>
Hablemos de Hablemos de
<br /> <br />
tu negocio tu negocio
</h2> </h2>
<p className={styles.subtitle}> <p className="text-lg text-gray-600 leading-relaxed">
Cuéntanos qué necesitas. Nuestro equipo responderá en menos de 24 horas Cuéntanos qué necesitas. Nuestro equipo responderá en menos de 24 horas
con una propuesta personalizada. con una propuesta personalizada.
</p> </p>
{/* Contact details */} {/* Contact details */}
<div className={styles.contactDetails}> <div className="flex flex-col gap-4 p-6 bg-gray-50 border border-gray-200 rounded-xl">
<div className={styles.contactItem}> <div className="flex items-center gap-4">
<div className={styles.contactIcon}> <div className="w-10 h-10 bg-white border border-gray-200 rounded-lg flex items-center justify-center shrink-0 text-black">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" stroke="currentColor" strokeWidth="2" /> <path
d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"
stroke="currentColor"
strokeWidth="2"
/>
<polyline points="22,6 12,13 2,6" stroke="currentColor" strokeWidth="2" /> <polyline points="22,6 12,13 2,6" stroke="currentColor" strokeWidth="2" />
</svg> </svg>
</div> </div>
<div> <div>
<div className={styles.contactLabel}>Email</div> <div className="text-xs font-semibold uppercase tracking-widest text-gray-400">Email</div>
<div className={styles.contactValue}>hola@flowsync.io</div> <div className="text-base font-semibold text-black">hola@flowsync.io</div>
</div> </div>
</div> </div>
<div className={styles.contactItem}> <div className="flex items-center gap-4">
<div className={styles.contactIcon}> <div className="w-10 h-10 bg-white border border-gray-200 rounded-lg flex items-center justify-center shrink-0 text-black">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 9.62a19.79 19.79 0 01-3.07-8.63A2 2 0 012.18 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.91 7.91a16 16 0 006.18 6.18l1.28-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 16.92z" stroke="currentColor" strokeWidth="2" /> <path
d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 9.62a19.79 19.79 0 01-3.07-8.63A2 2 0 012.18 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.91 7.91a16 16 0 006.18 6.18l1.28-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 16.92z"
stroke="currentColor"
strokeWidth="2"
/>
</svg> </svg>
</div> </div>
<div> <div>
<div className={styles.contactLabel}>Teléfono</div> <div className="text-xs font-semibold uppercase tracking-widest text-gray-400">Teléfono</div>
<div className={styles.contactValue}>+1 (800) 123-4567</div> <div className="text-base font-semibold text-black">+1 (800) 123-4567</div>
</div> </div>
</div> </div>
<div className={styles.contactItem}> <div className="flex items-center gap-4">
<div className={styles.contactIcon}> <div className="w-10 h-10 bg-white border border-gray-200 rounded-lg flex items-center justify-center shrink-0 text-black">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" /> <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" />
<polyline points="12 6 12 12 16 14" stroke="currentColor" strokeWidth="2" /> <polyline points="12 6 12 12 16 14" stroke="currentColor" strokeWidth="2" />
</svg> </svg>
</div> </div>
<div> <div>
<div className={styles.contactLabel}>Respuesta</div> <div className="text-xs font-semibold uppercase tracking-widest text-gray-400">Respuesta</div>
<div className={styles.contactValue}>Menos de 24 horas</div> <div className="text-base font-semibold text-black">Menos de 24 horas</div>
</div> </div>
</div> </div>
</div> </div>
{/* Testimonial */} {/* Testimonial */}
<blockquote className={styles.testimonial}> <blockquote className="bg-black text-white rounded-xl p-6 flex flex-col gap-4">
<p className={styles.testimonialText}> <p className="text-base leading-relaxed text-white/85 italic">
&ldquo;FlowSync transformó la forma en que colaboramos. Lo que antes &ldquo;FlowSync transformó la forma en que colaboramos. Lo que antes
tomaba días, ahora lo hacemos en horas.&rdquo; tomaba días, ahora lo hacemos en horas.&rdquo;
</p> </p>
<footer className={styles.testimonialAuthor}> <footer className="flex items-center gap-3">
<div className={styles.authorAvatar}>MR</div> <div className="w-10 h-10 bg-white/15 rounded-full flex items-center justify-center text-xs font-bold tracking-wider shrink-0">
MR
</div>
<div> <div>
<div className={styles.authorName}>María Rodríguez</div> <div className="text-sm font-bold">María Rodríguez</div>
<div className={styles.authorRole}>CTO en TechLatam</div> <div className="text-xs text-white/50">CTO en TechLatam</div>
</div> </div>
</footer> </footer>
</blockquote> </blockquote>
</div> </div>
{/* Form panel */} {/* Form panel */}
<div className={`${styles.reveal} ${styles.formPanel}`} style={{ transitionDelay: '0.15s' }}> <div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-150 bg-white border border-gray-200 rounded-2xl p-10 max-sm:p-6 shadow-lg">
{status === 'success' ? ( {status === 'success' ? (
<div className={styles.successState} role="alert" aria-live="polite"> <div
<div className={styles.successIcon}> className="flex flex-col items-center justify-center text-center gap-4 py-16 px-8 animate-scaleIn"
role="alert"
aria-live="polite"
>
<div className="w-18 h-18 bg-black text-white rounded-full flex items-center justify-center mb-2">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" aria-hidden="true"> <svg width="32" height="32" viewBox="0 0 32 32" fill="none" aria-hidden="true">
<path d="M6 16l7 7L26 9" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" /> <path
d="M6 16l7 7L26 9"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg> </svg>
</div> </div>
<h3 className={styles.successTitle}>¡Mensaje enviado!</h3> <h3 className="text-2xl font-extrabold tracking-tight text-black">
<p className={styles.successDesc}> ¡Mensaje enviado!
</h3>
<p className="text-base text-gray-600 max-w-[320px] leading-relaxed mb-4">
Gracias por contactarnos. Nuestro equipo te responderá en menos de 24 horas. Gracias por contactarnos. Nuestro equipo te responderá en menos de 24 horas.
</p> </p>
<button <button
@@ -215,28 +244,36 @@ export default function ContactForm() {
</div> </div>
) : ( ) : (
<form <form
className={styles.form} className="flex flex-col gap-5"
onSubmit={handleSubmit} onSubmit={handleSubmit}
noValidate noValidate
aria-label="Formulario de contacto" aria-label="Formulario de contacto"
id="contact-form" id="contact-form"
> >
<div className={styles.formHeader}> <div className="mb-2">
<h3 className={styles.formTitle}>Envíanos un mensaje</h3> <h3 className="text-2xl font-extrabold tracking-tight text-black">
<p className={styles.formSubtitle}>Todos los campos marcados con * son requeridos</p> Envíanos un mensaje
</h3>
<p className="text-sm text-gray-400 mt-1">
Todos los campos marcados con * son requeridos
</p>
</div> </div>
{/* Row: Name + Email */} {/* Row: Name + Email */}
<div className={styles.row}> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className={styles.fieldGroup}> <div className="flex flex-col gap-2">
<label htmlFor="contact-name" className={styles.label}> <label htmlFor="contact-name" className="text-sm font-semibold text-dark">
Nombre completo <span className={styles.required}>*</span> Nombre completo <span className="text-error">*</span>
</label> </label>
<input <input
id="contact-name" id="contact-name"
name="name" name="name"
type="text" type="text"
className={`${styles.input} ${errors.name && touched.name ? styles.inputError : ''}`} className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${
errors.name && touched.name
? 'border-error shadow-[0_0_0_3px_rgba(255,59,59,0.08)]'
: 'border-gray-200'
}`}
placeholder="Juan García" placeholder="Juan García"
value={formData.name} value={formData.name}
onChange={handleChange} onChange={handleChange}
@@ -247,21 +284,25 @@ export default function ContactForm() {
aria-invalid={!!(errors.name && touched.name)} aria-invalid={!!(errors.name && touched.name)}
/> />
{errors.name && touched.name && ( {errors.name && touched.name && (
<span id="name-error" className={styles.errorMsg} role="alert"> <span id="name-error" className="text-xs text-error font-medium flex items-center gap-1" role="alert">
{errors.name} {errors.name}
</span> </span>
)} )}
</div> </div>
<div className={styles.fieldGroup}> <div className="flex flex-col gap-2">
<label htmlFor="contact-email" className={styles.label}> <label htmlFor="contact-email" className="text-sm font-semibold text-dark">
Email <span className={styles.required}>*</span> Email <span className="text-error">*</span>
</label> </label>
<input <input
id="contact-email" id="contact-email"
name="email" name="email"
type="email" type="email"
className={`${styles.input} ${errors.email && touched.email ? styles.inputError : ''}`} className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${
errors.email && touched.email
? 'border-error shadow-[0_0_0_3px_rgba(255,59,59,0.08)]'
: 'border-gray-200'
}`}
placeholder="juan@empresa.com" placeholder="juan@empresa.com"
value={formData.email} value={formData.email}
onChange={handleChange} onChange={handleChange}
@@ -272,7 +313,7 @@ export default function ContactForm() {
aria-invalid={!!(errors.email && touched.email)} aria-invalid={!!(errors.email && touched.email)}
/> />
{errors.email && touched.email && ( {errors.email && touched.email && (
<span id="email-error" className={styles.errorMsg} role="alert"> <span id="email-error" className="text-xs text-error font-medium flex items-center gap-1" role="alert">
{errors.email} {errors.email}
</span> </span>
)} )}
@@ -280,16 +321,20 @@ export default function ContactForm() {
</div> </div>
{/* Row: Company + Phone */} {/* Row: Company + Phone */}
<div className={styles.row}> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className={styles.fieldGroup}> <div className="flex flex-col gap-2">
<label htmlFor="contact-company" className={styles.label}> <label htmlFor="contact-company" className="text-sm font-semibold text-dark">
Empresa <span className={styles.required}>*</span> Empresa <span className="text-error">*</span>
</label> </label>
<input <input
id="contact-company" id="contact-company"
name="company" name="company"
type="text" type="text"
className={`${styles.input} ${errors.company && touched.company ? styles.inputError : ''}`} className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${
errors.company && touched.company
? 'border-error shadow-[0_0_0_3px_rgba(255,59,59,0.08)]'
: 'border-gray-200'
}`}
placeholder="Mi Empresa S.A." placeholder="Mi Empresa S.A."
value={formData.company} value={formData.company}
onChange={handleChange} onChange={handleChange}
@@ -300,22 +345,26 @@ export default function ContactForm() {
aria-invalid={!!(errors.company && touched.company)} aria-invalid={!!(errors.company && touched.company)}
/> />
{errors.company && touched.company && ( {errors.company && touched.company && (
<span id="company-error" className={styles.errorMsg} role="alert"> <span id="company-error" className="text-xs text-error font-medium flex items-center gap-1" role="alert">
{errors.company} {errors.company}
</span> </span>
)} )}
</div> </div>
<div className={styles.fieldGroup}> <div className="flex flex-col gap-2">
<label htmlFor="contact-phone" className={styles.label}> <label htmlFor="contact-phone" className="text-sm font-semibold text-dark">
Teléfono Teléfono
<span className={styles.optional}> (opcional)</span> <span className="font-normal text-gray-400"> (opcional)</span>
</label> </label>
<input <input
id="contact-phone" id="contact-phone"
name="phone" name="phone"
type="tel" type="tel"
className={`${styles.input} ${errors.phone && touched.phone ? styles.inputError : ''}`} className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] ${
errors.phone && touched.phone
? 'border-error shadow-[0_0_0_3px_rgba(255,59,59,0.08)]'
: 'border-gray-200'
}`}
placeholder="+52 55 1234 5678" placeholder="+52 55 1234 5678"
value={formData.phone} value={formData.phone}
onChange={handleChange} onChange={handleChange}
@@ -325,7 +374,7 @@ export default function ContactForm() {
aria-invalid={!!(errors.phone && touched.phone)} aria-invalid={!!(errors.phone && touched.phone)}
/> />
{errors.phone && touched.phone && ( {errors.phone && touched.phone && (
<span id="phone-error" className={styles.errorMsg} role="alert"> <span id="phone-error" className="text-xs text-error font-medium flex items-center gap-1" role="alert">
{errors.phone} {errors.phone}
</span> </span>
)} )}
@@ -333,14 +382,18 @@ export default function ContactForm() {
</div> </div>
{/* Message */} {/* Message */}
<div className={styles.fieldGroup}> <div className="flex flex-col gap-2">
<label htmlFor="contact-message" className={styles.label}> <label htmlFor="contact-message" className="text-sm font-semibold text-dark">
Mensaje <span className={styles.required}>*</span> Mensaje <span className="text-error">*</span>
</label> </label>
<textarea <textarea
id="contact-message" id="contact-message"
name="message" name="message"
className={`${styles.textarea} ${errors.message && touched.message ? styles.inputError : ''}`} className={`w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)] resize-y min-h-[120px] ${
errors.message && touched.message
? 'border-error shadow-[0_0_0_3px_rgba(255,59,59,0.08)]'
: 'border-gray-200'
}`}
placeholder="Cuéntanos sobre tu proyecto, equipo y qué quieres lograr con FlowSync..." placeholder="Cuéntanos sobre tu proyecto, equipo y qué quieres lograr con FlowSync..."
rows={5} rows={5}
value={formData.message} value={formData.message}
@@ -350,15 +403,15 @@ export default function ContactForm() {
aria-describedby={errors.message && touched.message ? 'message-error' : undefined} aria-describedby={errors.message && touched.message ? 'message-error' : undefined}
aria-invalid={!!(errors.message && touched.message)} aria-invalid={!!(errors.message && touched.message)}
/> />
<div className={styles.textareaFooter}> <div className="flex justify-between items-center">
{errors.message && touched.message ? ( {errors.message && touched.message ? (
<span id="message-error" className={styles.errorMsg} role="alert"> <span id="message-error" className="text-xs text-error font-medium flex items-center gap-1" role="alert">
{errors.message} {errors.message}
</span> </span>
) : ( ) : (
<span /> <span />
)} )}
<span className={styles.charCount}> <span className="text-xs text-gray-400 ml-auto">
{formData.message.length} caracteres {formData.message.length} caracteres
</span> </span>
</div> </div>
@@ -367,30 +420,41 @@ export default function ContactForm() {
{/* Submit */} {/* Submit */}
<button <button
type="submit" type="submit"
className={`btn btn-primary btn-lg ${styles.submitBtn}`} className="btn btn-primary btn-lg w-full justify-center mt-2 disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none"
disabled={status === 'loading'} disabled={status === 'loading'}
id="contact-submit-btn" id="contact-submit-btn"
aria-busy={status === 'loading'} aria-busy={status === 'loading'}
> >
{status === 'loading' ? ( {status === 'loading' ? (
<> <>
<span className={styles.spinner} aria-hidden="true" /> <span
className="w-[18px] h-[18px] border-2 border-white/30 border-t-white rounded-full animate-[spin_0.7s_linear_infinite] shrink-0"
aria-hidden="true"
/>
Enviando... Enviando...
</> </>
) : ( ) : (
<> <>
Enviar mensaje Enviar mensaje
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M2 8h12M10 4l4 4-4 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> <path
d="M2 8h12M10 4l4 4-4 4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg> </svg>
</> </>
)} )}
</button> </button>
<p className={styles.privacyNote}> <p className="text-xs text-gray-400 text-center leading-relaxed">
Al enviar, aceptas nuestra{' '} Al enviar, aceptas nuestra{' '}
<a href="#" className={styles.privacyLink}>política de privacidad</a>. <a href="#" className="text-gray-600 underline underline-offset-2 transition-colors duration-150 hover:text-black">
Nunca compartiremos tu información. política de privacidad
</a>
. Nunca compartiremos tu información.
</p> </p>
</form> </form>
)} )}

View File

@@ -1,150 +0,0 @@
.features {
background: var(--color-gray-50);
border-top: 1px solid var(--color-gray-200);
border-bottom: 1px solid var(--color-gray-200);
}
/* Reveal */
.reveal {
opacity: 0;
transform: translateY(24px);
transition: opacity 0.65s ease, transform 0.65s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
/* Header */
.header {
text-align: center;
max-width: 600px;
margin: 0 auto var(--space-16);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
}
.title {
font-size: clamp(var(--text-3xl), 5vw, var(--text-5xl));
font-weight: 900;
letter-spacing: -0.04em;
line-height: 1.1;
color: var(--color-black);
}
.titleAccent {
color: var(--color-gray-600);
}
.subtitle {
font-size: var(--text-lg);
color: var(--color-gray-600);
line-height: 1.6;
}
/* Grid */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-4);
}
/* Card */
.card {
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-xl);
padding: var(--space-8);
display: flex;
flex-direction: column;
gap: var(--space-4);
transition: all var(--transition-base);
cursor: default;
}
.card:hover {
border-color: var(--color-black);
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
.cardIcon {
width: 48px;
height: 48px;
background: var(--color-black);
color: var(--color-white);
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: transform var(--transition-base);
}
.card:hover .cardIcon {
transform: scale(1.05) rotate(-2deg);
}
.cardContent {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.cardTag {
font-size: var(--text-xs);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-gray-400);
}
.cardTitle {
font-size: var(--text-xl);
font-weight: 800;
letter-spacing: -0.03em;
color: var(--color-black);
line-height: 1.2;
}
.cardDesc {
font-size: var(--text-sm);
color: var(--color-gray-600);
line-height: 1.65;
}
/* Bottom CTA */
.bottomCta {
text-align: center;
margin-top: var(--space-16);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
}
.ctaText {
font-size: var(--text-xl);
font-weight: 700;
color: var(--color-black);
letter-spacing: -0.02em;
}
/* Responsive */
@media (max-width: 1024px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.grid {
grid-template-columns: 1fr;
}
.card {
padding: var(--space-6);
}
}

View File

@@ -1,13 +1,18 @@
'use client'; 'use client';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef } from 'react';
import styles from './Features.module.css';
const features = [ const features = [
{ {
icon: ( icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> <path
d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg> </svg>
), ),
title: 'Automatización inteligente', title: 'Automatización inteligente',
@@ -18,7 +23,13 @@ const features = [
{ {
icon: ( icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2M9 11a4 4 0 100-8 4 4 0 000 8zM23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> <path
d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2M9 11a4 4 0 100-8 4 4 0 000 8zM23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg> </svg>
), ),
title: 'Colaboración en tiempo real', title: 'Colaboración en tiempo real',
@@ -29,7 +40,13 @@ const features = [
{ {
icon: ( icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> <polyline
points="22 12 18 12 15 21 9 3 6 12 2 12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg> </svg>
), ),
title: 'Analíticas avanzadas', title: 'Analíticas avanzadas',
@@ -53,7 +70,11 @@ const features = [
icon: ( icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" /> <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" />
<path d="M2 12h20M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z" stroke="currentColor" strokeWidth="2" /> <path
d="M2 12h20M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"
stroke="currentColor"
strokeWidth="2"
/>
</svg> </svg>
), ),
title: '200+ integraciones', title: '200+ integraciones',
@@ -64,7 +85,13 @@ const features = [
{ {
icon: ( icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> <path
d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg> </svg>
), ),
title: 'Soporte 24/7', title: 'Soporte 24/7',
@@ -82,57 +109,74 @@ export default function Features() {
(entries) => { (entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
entry.target.classList.add(styles.visible); entry.target.classList.add('opacity-100', 'translate-y-0');
entry.target.classList.remove('opacity-0', 'translate-y-6');
} }
}); });
}, },
{ threshold: 0.1 } { threshold: 0.1 }
); );
const elements = sectionRef.current?.querySelectorAll(`.${styles.reveal}`); const elements = sectionRef.current?.querySelectorAll('.reveal');
elements?.forEach((el) => observer.observe(el)); elements?.forEach((el) => observer.observe(el));
return () => observer.disconnect(); return () => observer.disconnect();
}, []); }, []);
return ( return (
<section className={`section ${styles.features}`} id="features" ref={sectionRef} aria-labelledby="features-heading"> <section
className="bg-gray-50 border-y border-gray-200 py-24"
id="features"
ref={sectionRef}
aria-labelledby="features-heading"
>
<div className="container"> <div className="container">
{/* Header */} {/* Header */}
<div className={`${styles.reveal} ${styles.header}`}> <div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out text-center max-w-[600px] mx-auto mb-16 flex flex-col items-center gap-4">
<span className="badge badge-accent">Características</span> <span className="badge badge-accent">Características</span>
<h2 id="features-heading" className={styles.title}> <h2
id="features-heading"
className="text-[clamp(1.875rem,5vw,3rem)] font-black tracking-[-0.04em] leading-[1.1] text-black"
>
Todo lo que necesitas, Todo lo que necesitas,
<br /> <br />
<span className={styles.titleAccent}>nada que no necesitas</span> <span className="text-gray-600">nada que no necesitas</span>
</h2> </h2>
<p className={styles.subtitle}> <p className="text-lg text-gray-600 leading-relaxed">
Diseñado para equipos que quieren mover rápido sin romper cosas. Diseñado para equipos que quieren mover rápido sin romper cosas.
Potente cuando lo necesitas, simple cuando no. Potente cuando lo necesitas, simple cuando no.
</p> </p>
</div> </div>
{/* Grid */} {/* Grid */}
<div className={styles.grid}> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{features.map((feature, i) => ( {features.map((feature, i) => (
<article <article
key={feature.title} key={feature.title}
className={`${styles.reveal} ${styles.card}`} className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out bg-white border border-gray-200 rounded-xl p-8 max-sm:p-6 flex flex-col gap-4 cursor-default hover:border-black hover:shadow-lg hover:-translate-y-0.5 group"
style={{ transitionDelay: `${i * 0.08}s` }} style={{ transitionDelay: `${i * 80}ms` }}
aria-label={feature.title} aria-label={feature.title}
> >
<div className={styles.cardIcon}>{feature.icon}</div> <div className="w-12 h-12 bg-black text-white rounded-lg flex items-center justify-center shrink-0 transition-transform duration-250 ease-out group-hover:scale-105 group-hover:-rotate-2">
<div className={styles.cardContent}> {feature.icon}
<div className={styles.cardTag}>{feature.tag}</div> </div>
<h3 className={styles.cardTitle}>{feature.title}</h3> <div className="flex flex-col gap-2">
<p className={styles.cardDesc}>{feature.description}</p> <div className="text-xs font-bold uppercase tracking-widest text-gray-400">
{feature.tag}
</div>
<h3 className="text-xl font-extrabold tracking-tight text-black leading-tight">
{feature.title}
</h3>
<p className="text-sm text-gray-600 leading-relaxed">
{feature.description}
</p>
</div> </div>
</article> </article>
))} ))}
</div> </div>
{/* Bottom CTA */} {/* Bottom CTA */}
<div className={`${styles.reveal} ${styles.bottomCta}`}> <div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out text-center mt-16 flex flex-col items-center gap-4">
<p className={styles.ctaText}> <p className="text-xl font-bold text-black tracking-tight">
¿Listo para transformar cómo trabaja tu equipo? ¿Listo para transformar cómo trabaja tu equipo?
</p> </p>
<button <button

View File

@@ -1,230 +0,0 @@
.footer {
background: var(--color-black);
color: var(--color-white);
}
/* CTA Banner */
.ctaBanner {
background: var(--color-gray-900);
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.ctaInner {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-8);
padding: var(--space-12) 0;
flex-wrap: wrap;
}
.ctaText {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.ctaTitle {
font-size: clamp(var(--text-2xl), 4vw, var(--text-4xl));
font-weight: 900;
letter-spacing: -0.04em;
color: var(--color-white);
}
.ctaSubtitle {
font-size: var(--text-base);
color: rgba(255,255,255,0.5);
}
.ctaBtns {
display: flex;
gap: var(--space-3);
flex-shrink: 0;
flex-wrap: wrap;
}
/* Main footer */
.main {
padding: var(--space-16) 0 var(--space-8);
}
.topRow {
display: grid;
grid-template-columns: 280px 1fr;
gap: var(--space-16);
margin-bottom: var(--space-12);
}
/* Brand */
.brand {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.brandLogo {
display: flex;
align-items: center;
gap: var(--space-2);
}
.logoMark {
width: 36px;
height: 36px;
background: var(--color-white);
color: var(--color-black);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-xs);
font-weight: 800;
letter-spacing: -0.02em;
}
.logoText {
font-size: var(--text-lg);
font-weight: 800;
letter-spacing: -0.04em;
color: var(--color-white);
}
.brandDesc {
font-size: var(--text-sm);
color: rgba(255,255,255,0.45);
line-height: 1.6;
}
/* Socials */
.socials {
display: flex;
gap: var(--space-2);
}
.socialLink {
width: 36px;
height: 36px;
background: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.6);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition-fast);
}
.socialLink:hover {
background: rgba(255,255,255,0.15);
color: var(--color-white);
}
/* Links grid */
.linksGrid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-8);
}
.linkColumn {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.linkCategory {
font-size: var(--text-xs);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: rgba(255,255,255,0.35);
}
.linkList {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.footerLink {
font-size: var(--text-sm);
color: rgba(255,255,255,0.55);
text-decoration: none;
transition: color var(--transition-fast);
}
.footerLink:hover {
color: var(--color-white);
}
/* Bottom bar */
.bottomBar {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: var(--space-8);
border-top: 1px solid rgba(255,255,255,0.08);
flex-wrap: wrap;
gap: var(--space-4);
}
.copyright {
font-size: var(--text-sm);
color: rgba(255,255,255,0.35);
}
.statusBadge {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-xs);
font-weight: 500;
color: rgba(255,255,255,0.45);
}
.statusDot {
width: 8px;
height: 8px;
background: var(--color-success);
border-radius: 50%;
animation: pulse 2s ease infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(0, 200, 83, 0.4); }
50% { opacity: 0.8; box-shadow: 0 0 0 4px rgba(0, 200, 83, 0); }
}
/* Responsive */
@media (max-width: 1024px) {
.topRow {
grid-template-columns: 1fr;
gap: var(--space-12);
}
.linksGrid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.ctaInner {
flex-direction: column;
text-align: center;
}
.ctaBtns {
width: 100%;
flex-direction: column;
}
.ctaBtns .btn {
width: 100%;
justify-content: center;
}
.linksGrid {
grid-template-columns: repeat(2, 1fr);
gap: var(--space-6);
}
}

View File

@@ -1,7 +1,5 @@
'use client'; 'use client';
import styles from './Footer.module.css';
const footerLinks = { const footerLinks = {
Producto: ['Características', 'Precios', 'Integraciones', 'Changelog', 'Roadmap'], Producto: ['Características', 'Precios', 'Integraciones', 'Changelog', 'Roadmap'],
Empresa: ['Sobre nosotros', 'Blog', 'Carreras', 'Prensa', 'Partners'], Empresa: ['Sobre nosotros', 'Blog', 'Carreras', 'Prensa', 'Partners'],
@@ -41,31 +39,30 @@ const socials = [
export default function Footer() { export default function Footer() {
return ( return (
<footer className={styles.footer} role="contentinfo"> <footer className="bg-black text-white" role="contentinfo">
{/* CTA Banner */} {/* CTA Banner */}
<div className={styles.ctaBanner}> <div className="bg-gray-900 border-b border-white/10">
<div className="container"> <div className="container">
<div className={styles.ctaInner}> <div className="flex items-center justify-between gap-8 py-12 flex-wrap max-sm:flex-col max-sm:text-center">
<div className={styles.ctaText}> <div className="flex flex-col gap-2">
<h2 className={styles.ctaTitle}> <h2 className="text-[clamp(1.5rem,4vw,2.25rem)] font-black tracking-[-0.04em] text-white">
Empieza hoy mismo es gratis Empieza hoy mismo es gratis
</h2> </h2>
<p className={styles.ctaSubtitle}> <p className="text-base text-white/50">
Únete a más de 10,000 equipos que ya usan FlowSync Únete a más de 10,000 equipos que ya usan FlowSync
</p> </p>
</div> </div>
<div className={styles.ctaBtns}> <div className="flex gap-3 shrink-0 flex-wrap max-sm:w-full max-sm:flex-col">
<button <button
className="btn btn-accent btn-lg" className="btn btn-accent btn-lg max-sm:w-full max-sm:justify-center"
id="footer-cta-btn" id="footer-cta-btn"
onClick={() => document.querySelector('#contact')?.scrollIntoView({ behavior: 'smooth' })} onClick={() => document.querySelector('#contact')?.scrollIntoView({ behavior: 'smooth' })}
> >
Prueba gratis 14 días Prueba gratis 14 días
</button> </button>
<button <button
className="btn btn-secondary btn-lg" className="btn btn-secondary btn-lg border-white/20 text-white bg-white/10 hover:bg-white/20 hover:border-white/30 max-sm:w-full max-sm:justify-center"
id="footer-demo-btn" id="footer-demo-btn"
style={{ background: 'rgba(255,255,255,0.1)', borderColor: 'rgba(255,255,255,0.2)', color: 'white' }}
> >
Ver demo Ver demo
</button> </button>
@@ -75,24 +72,28 @@ export default function Footer() {
</div> </div>
{/* Main footer */} {/* Main footer */}
<div className={styles.main}> <div className="py-16 pb-8">
<div className="container"> <div className="container">
<div className={styles.topRow}> <div className="grid grid-cols-[280px_1fr] max-lg:grid-cols-1 gap-16 lg:gap-16 max-lg:gap-12 mb-12">
{/* Brand */} {/* Brand */}
<div className={styles.brand}> <div className="flex flex-col gap-4">
<div className={styles.brandLogo}> <div className="flex items-center gap-2">
<span className={styles.logoMark}>FS</span> <span className="w-9 h-9 bg-white text-black rounded-md flex items-center justify-center text-xs font-extrabold tracking-[-0.02em]">
<span className={styles.logoText}>FlowSync</span> FS
</span>
<span className="text-lg font-extrabold tracking-[-0.04em] text-white">
FlowSync
</span>
</div> </div>
<p className={styles.brandDesc}> <p className="text-sm text-white/45 leading-relaxed">
La plataforma todo-en-uno para equipos que quieren moverse rápido sin perder el control. La plataforma todo-en-uno para equipos que quieren moverse rápido sin perder el control.
</p> </p>
<div className={styles.socials}> <div className="flex gap-2">
{socials.map((s) => ( {socials.map((s) => (
<a <a
key={s.name} key={s.name}
href={s.href} href={s.href}
className={styles.socialLink} className="w-9 h-9 bg-white/10 text-white/60 rounded-md flex items-center justify-center transition-all duration-150 hover:bg-white/15 hover:text-white"
aria-label={s.name} aria-label={s.name}
id={`footer-social-${s.name.toLowerCase()}`} id={`footer-social-${s.name.toLowerCase()}`}
> >
@@ -103,14 +104,19 @@ export default function Footer() {
</div> </div>
{/* Links */} {/* Links */}
<nav className={styles.linksGrid} aria-label="Footer navigation"> <nav className="grid grid-cols-4 max-lg:grid-cols-2 max-sm:grid-cols-2 max-sm:gap-6 gap-8" aria-label="Footer navigation">
{Object.entries(footerLinks).map(([category, links]) => ( {Object.entries(footerLinks).map(([category, links]) => (
<div key={category} className={styles.linkColumn}> <div key={category} className="flex flex-col gap-4">
<h3 className={styles.linkCategory}>{category}</h3> <h3 className="text-xs font-bold uppercase tracking-widest text-white/35">
<ul className={styles.linkList} role="list"> {category}
</h3>
<ul className="list-none flex flex-col gap-3" role="list">
{links.map((link) => ( {links.map((link) => (
<li key={link}> <li key={link}>
<a href="#" className={styles.footerLink}> <a
href="#"
className="text-sm text-white/55 no-underline transition-colors duration-150 hover:text-white"
>
{link} {link}
</a> </a>
</li> </li>
@@ -122,12 +128,12 @@ export default function Footer() {
</div> </div>
{/* Bottom bar */} {/* Bottom bar */}
<div className={styles.bottomBar}> <div className="flex items-center justify-between pt-8 border-t border-white/10 flex-wrap gap-4">
<p className={styles.copyright}> <p className="text-sm text-white/35">
© {new Date().getFullYear()} FlowSync, Inc. Todos los derechos reservados. © {new Date().getFullYear()} FlowSync, Inc. Todos los derechos reservados.
</p> </p>
<div className={styles.statusBadge}> <div className="flex items-center gap-2 text-xs font-medium text-white/45">
<span className={styles.statusDot} /> <span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
Todos los sistemas operativos Todos los sistemas operativos
</div> </div>
</div> </div>

View File

@@ -1,370 +0,0 @@
.hero {
padding-top: 72px; /* navbar height */
background: var(--color-white);
overflow: hidden;
}
.inner {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding-top: var(--space-20);
padding-bottom: var(--space-16);
gap: var(--space-6);
}
/* Reveal animation */
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.7s ease, transform 0.7s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
.reveal:nth-child(2) { transition-delay: 0.1s; }
.reveal:nth-child(3) { transition-delay: 0.2s; }
.reveal:nth-child(4) { transition-delay: 0.3s; }
.reveal:nth-child(5) { transition-delay: 0.4s; }
.reveal:nth-child(6) { transition-delay: 0.5s; }
.reveal:nth-child(7) { transition-delay: 0.6s; }
.reveal:nth-child(8) { transition-delay: 0.7s; }
/* Badge */
.badgeWrap {
margin-bottom: var(--space-2);
}
/* Heading */
.heading {
font-size: clamp(2.5rem, 7vw, 5rem);
font-weight: 900;
letter-spacing: -0.04em;
line-height: 1.05;
color: var(--color-black);
max-width: 800px;
}
.emphasis {
font-style: italic;
font-weight: 900;
}
/* Subheading */
.subheading {
font-size: clamp(var(--text-base), 2vw, var(--text-xl));
color: var(--color-gray-600);
max-width: 520px;
line-height: 1.6;
margin-top: var(--space-2);
}
/* CTAs */
.ctas {
display: flex;
gap: var(--space-3);
flex-wrap: wrap;
justify-content: center;
margin-top: var(--space-2);
}
/* Trust note */
.trustNote {
font-size: var(--text-sm);
color: var(--color-gray-400);
margin-top: var(--space-1);
}
/* Dashboard Mockup */
.mockupWrap {
width: 100%;
max-width: 900px;
margin-top: var(--space-8);
perspective: 1200px;
}
.mockup {
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl), 0 0 0 1px rgba(0, 0, 0, 0.03);
overflow: hidden;
transform: rotateX(4deg);
transition: transform 0.4s ease;
}
.mockup:hover {
transform: rotateX(0deg);
}
/* Window bar */
.mockupBar {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
background: var(--color-gray-50);
border-bottom: 1px solid var(--color-gray-200);
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
flex-shrink: 0;
}
.mockupUrl {
flex: 1;
text-align: center;
font-size: var(--text-xs);
color: var(--color-gray-400);
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-full);
padding: 2px var(--space-3);
max-width: 280px;
margin: 0 auto;
}
/* Mockup body */
.mockupBody {
display: flex;
height: 380px;
}
/* Sidebar */
.mockupSidebar {
width: 56px;
background: var(--color-gray-50);
border-right: 1px solid var(--color-gray-200);
padding: var(--space-4) var(--space-3);
display: flex;
flex-direction: column;
gap: var(--space-3);
flex-shrink: 0;
}
.sidebarLogo {
width: 32px;
height: 32px;
background: var(--color-black);
border-radius: var(--radius-md);
margin-bottom: var(--space-4);
}
.sidebarItem {
width: 32px;
height: 32px;
background: var(--color-gray-200);
border-radius: var(--radius-md);
}
.sidebarActive {
background: var(--color-black) !important;
}
/* Main dashboard */
.mockupMain {
flex: 1;
padding: var(--space-6);
display: flex;
flex-direction: column;
gap: var(--space-4);
overflow: hidden;
}
.mockupHeader {
display: flex;
justify-content: space-between;
align-items: center;
}
.mockupTitle {
height: 20px;
width: 160px;
background: var(--color-gray-200);
border-radius: var(--radius-sm);
}
.mockupBtn {
height: 32px;
width: 100px;
background: var(--color-black);
border-radius: var(--radius-md);
}
/* Metric cards */
.metricsRow {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-3);
}
.metricCard {
background: var(--color-gray-50);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-lg);
padding: var(--space-3);
display: flex;
gap: var(--space-2);
align-items: center;
}
.metricIcon {
width: 32px;
height: 32px;
border-radius: var(--radius-md);
flex-shrink: 0;
}
.metricLines {
display: flex;
flex-direction: column;
gap: 6px;
flex: 1;
}
.metricValue {
height: 14px;
background: var(--color-gray-300);
border-radius: var(--radius-sm);
width: 70%;
}
.metricLabel {
height: 10px;
background: var(--color-gray-200);
border-radius: var(--radius-sm);
width: 90%;
}
/* Chart */
.chartArea {
flex: 1;
background: var(--color-gray-50);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-lg);
padding: var(--space-4);
overflow: hidden;
}
.chartBars {
display: flex;
align-items: flex-end;
gap: var(--space-2);
height: 100%;
}
@keyframes growBar {
from { height: 0; }
to { height: var(--target-height); }
}
.bar {
flex: 1;
background: var(--color-black);
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
opacity: 0.12;
animation: fadeIn 0.5s ease forwards;
}
.bar:nth-child(odd) {
opacity: 0.08;
}
.bar:nth-child(4),
.bar:nth-child(8) {
opacity: 0.9;
background: var(--color-black);
}
/* Task list */
.taskList {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.taskRow {
display: flex;
align-items: center;
gap: var(--space-3);
}
.taskCheck {
width: 16px;
height: 16px;
border: 2px solid var(--color-gray-300);
border-radius: var(--radius-sm);
flex-shrink: 0;
}
.taskLine {
height: 10px;
background: var(--color-gray-200);
border-radius: var(--radius-sm);
}
/* Stats */
.stats {
display: flex;
gap: var(--space-12);
margin-top: var(--space-8);
flex-wrap: wrap;
justify-content: center;
}
.statItem {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-1);
}
.statValue {
font-size: var(--text-3xl);
font-weight: 900;
letter-spacing: -0.04em;
color: var(--color-black);
}
.statLabel {
font-size: var(--text-sm);
color: var(--color-gray-500, #666);
}
/* Responsive */
@media (max-width: 768px) {
.mockupBody {
height: 260px;
}
.mockupSidebar {
display: none;
}
.metricsRow {
grid-template-columns: repeat(2, 1fr);
}
.stats {
gap: var(--space-8);
}
}
@media (max-width: 480px) {
.ctas {
flex-direction: column;
width: 100%;
max-width: 320px;
}
.ctas .btn {
width: 100%;
}
}

View File

@@ -1,7 +1,6 @@
'use client'; 'use client';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import styles from './Hero.module.css';
const stats = [ const stats = [
{ value: '10K+', label: 'Equipos activos' }, { value: '10K+', label: 'Equipos activos' },
@@ -18,14 +17,15 @@ export default function Hero() {
(entries) => { (entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
entry.target.classList.add(styles.visible); entry.target.classList.add('opacity-100', 'translate-y-0');
entry.target.classList.remove('opacity-0', 'translate-y-6');
} }
}); });
}, },
{ threshold: 0.1 } { threshold: 0.1 }
); );
const elements = heroRef.current?.querySelectorAll(`.${styles.reveal}`); const elements = heroRef.current?.querySelectorAll('.reveal');
elements?.forEach((el) => observer.observe(el)); elements?.forEach((el) => observer.observe(el));
return () => observer.disconnect(); return () => observer.disconnect();
@@ -40,10 +40,15 @@ export default function Hero() {
}; };
return ( return (
<section className={styles.hero} id="hero" ref={heroRef} aria-label="Sección principal"> <section
<div className={`container ${styles.inner}`}> className="pt-[72px] bg-white overflow-hidden"
id="hero"
ref={heroRef}
aria-label="Sección principal"
>
<div className="container flex flex-col items-center text-center pt-20 pb-16 gap-6">
{/* Badge */} {/* Badge */}
<div className={`${styles.reveal} ${styles.badgeWrap}`}> <div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-100 mb-2">
<span className="badge badge-dark"> <span className="badge badge-dark">
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" aria-hidden="true"> <svg width="8" height="8" viewBox="0 0 8 8" fill="none" aria-hidden="true">
<circle cx="4" cy="4" r="4" fill="#00c853" /> <circle cx="4" cy="4" r="4" fill="#00c853" />
@@ -53,34 +58,40 @@ export default function Hero() {
</div> </div>
{/* Heading */} {/* Heading */}
<h1 className={`${styles.reveal} ${styles.heading}`}> <h1 className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-200 text-[clamp(2.5rem,7vw,5rem)] font-black tracking-[-0.04em] leading-[1.05] text-black max-w-[800px]">
El flujo de trabajo El flujo de trabajo
<br /> <br />
<em className={styles.emphasis}>que tu equipo</em> <em className="italic font-black">que tu equipo</em>
<br /> <br />
siempre necesitó siempre necesitó
</h1> </h1>
{/* Subheading */} {/* Subheading */}
<p className={`${styles.reveal} ${styles.subheading}`}> <p className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-300 text-[clamp(1rem,2vw,1.25rem)] text-gray-600 max-w-[520px] leading-relaxed mt-2">
FlowSync conecta a tu equipo, automatiza tareas repetitivas y te da FlowSync conecta a tu equipo, automatiza tareas repetitivas y te da
visibilidad total de cada proyecto todo en un solo lugar. visibilidad total de cada proyecto todo en un solo lugar.
</p> </p>
{/* CTA Buttons */} {/* CTA Buttons */}
<div className={`${styles.reveal} ${styles.ctas}`}> <div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-400 flex flex-wrap justify-center gap-3 mt-2 max-sm:flex-col max-sm:w-full max-sm:max-w-[320px]">
<button <button
className="btn btn-primary btn-lg" className="btn btn-primary btn-lg max-sm:w-full"
id="hero-cta-primary" id="hero-cta-primary"
onClick={handleScrollToContact} onClick={handleScrollToContact}
> >
Empieza gratis Empieza gratis
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> <path
d="M3 8h10M9 4l4 4-4 4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg> </svg>
</button> </button>
<button <button
className="btn btn-secondary btn-lg" className="btn btn-secondary btn-lg max-sm:w-full"
id="hero-cta-secondary" id="hero-cta-secondary"
onClick={handleScrollToFeatures} onClick={handleScrollToFeatures}
> >
@@ -89,71 +100,92 @@ export default function Hero() {
</div> </div>
{/* Trust note */} {/* Trust note */}
<p className={`${styles.reveal} ${styles.trustNote}`}> <p className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-500 text-sm text-gray-400 mt-1">
Sin tarjeta de crédito &nbsp;·&nbsp; 14 días gratis &nbsp;·&nbsp; Cancela cuando quieras Sin tarjeta de crédito &nbsp;·&nbsp; 14 días gratis &nbsp;·&nbsp; Cancela cuando quieras
</p> </p>
{/* Dashboard mockup */} {/* Dashboard mockup */}
<div className={`${styles.reveal} ${styles.mockupWrap}`}> <div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-600 w-full max-w-[900px] mt-8 [perspective:1200px]">
<div className={styles.mockup} role="img" aria-label="Vista previa del dashboard de FlowSync"> <div
className="bg-white border border-gray-200 rounded-2xl shadow-xl overflow-hidden transform rotate-x-4 transition-transform duration-400 ease-out hover:rotate-x-0"
role="img"
aria-label="Vista previa del dashboard de FlowSync"
style={{ transform: 'rotateX(4deg)' }}
>
{/* Window chrome */} {/* Window chrome */}
<div className={styles.mockupBar}> <div className="flex items-center gap-2 px-4 py-3 bg-gray-50 border-b border-gray-200">
<span className={styles.dot} style={{ background: '#ff5f57' }} /> <span className="w-3 h-3 rounded-full shrink-0 bg-[#ff5f57]" />
<span className={styles.dot} style={{ background: '#febc2e' }} /> <span className="w-3 h-3 rounded-full shrink-0 bg-[#febc2e]" />
<span className={styles.dot} style={{ background: '#28c840' }} /> <span className="w-3 h-3 rounded-full shrink-0 bg-[#28c840]" />
<span className={styles.mockupUrl}>app.flowsync.io/dashboard</span> <span className="flex-1 text-center text-xs text-gray-400 bg-white border border-gray-200 rounded-full py-[2px] px-3 max-w-[280px] mx-auto">
app.flowsync.io/dashboard
</span>
</div> </div>
{/* Mock dashboard content */} {/* Mock dashboard content */}
<div className={styles.mockupBody}> <div className="flex h-[380px] max-md:h-[260px]">
{/* Sidebar */} {/* Sidebar */}
<nav className={styles.mockupSidebar} aria-hidden="true"> <nav className="w-14 bg-gray-50 border-r border-gray-200 px-3 py-4 flex flex-col gap-3 shrink-0 max-md:hidden" aria-hidden="true">
<div className={styles.sidebarLogo} /> <div className="w-8 h-8 bg-black rounded-md mb-4" />
{[...Array(5)].map((_, i) => ( {[...Array(5)].map((_, i) => (
<div key={i} className={`${styles.sidebarItem} ${i === 0 ? styles.sidebarActive : ''}`} /> <div
key={i}
className={`w-8 h-8 rounded-md ${
i === 0 ? 'bg-black' : 'bg-gray-200'
}`}
/>
))} ))}
</nav> </nav>
{/* Main content */} {/* Main content */}
<main className={styles.mockupMain} aria-hidden="true"> <main className="flex-1 p-6 flex flex-col gap-4 overflow-hidden" aria-hidden="true">
{/* Header row */} {/* Header row */}
<div className={styles.mockupHeader}> <div className="flex justify-between items-center">
<div className={styles.mockupTitle} /> <div className="h-5 w-40 bg-gray-200 rounded-sm" />
<div className={styles.mockupBtn} /> <div className="h-8 w-24 bg-black rounded-md" />
</div> </div>
{/* Metric cards */} {/* Metric cards */}
<div className={styles.metricsRow}> <div className="grid grid-cols-4 max-md:grid-cols-2 gap-3">
{['#0066ff', '#00c853', '#ff9500', '#6c5ce7'].map((color, i) => ( {['#0066ff', '#00c853', '#ff9500', '#6c5ce7'].map((color, i) => (
<div key={i} className={styles.metricCard}> <div key={i} className="bg-gray-50 border border-gray-200 rounded-xl p-3 flex gap-2 items-center">
<div className={styles.metricIcon} style={{ background: `${color}18`, color }} /> <div
<div className={styles.metricLines}> className="w-8 h-8 rounded-md shrink-0"
<div className={styles.metricValue} /> style={{ background: `${color}18`, color }}
<div className={styles.metricLabel} /> />
<div className="flex flex-col gap-[6px] flex-1">
<div className="h-[14px] bg-gray-300 rounded-sm w-[70%]" />
<div className="h-[10px] bg-gray-200 rounded-sm w-[90%]" />
</div> </div>
</div> </div>
))} ))}
</div> </div>
{/* Chart area */} {/* Chart area */}
<div className={styles.chartArea}> <div className="flex-1 bg-gray-50 border border-gray-200 rounded-xl p-4 overflow-hidden">
<div className={styles.chartBars}> <div className="flex items-end gap-2 h-100">
{[60, 80, 50, 90, 70, 85, 65, 95, 75, 88].map((h, i) => ( {[60, 80, 50, 90, 70, 85, 65, 95, 75, 88].map((h, i) => (
<div <div
key={i} key={i}
className={styles.bar} className={`flex-1 rounded-t-sm animate-fadeIn opacity-100 ${
style={{ height: `${h}%`, animationDelay: `${i * 0.05}s` }} i === 3 || i === 7 ? 'bg-black opacity-90' : 'bg-black opacity-10'
}`}
style={{
height: `${h}%`,
animationDelay: `${i * 0.05}s`,
opacity: i === 3 || i === 7 ? 0.9 : (i % 2 === 0 ? 0.12 : 0.08)
}}
/> />
))} ))}
</div> </div>
</div> </div>
{/* Task list */} {/* Task list */}
<div className={styles.taskList}> <div className="flex flex-col gap-2">
{[85, 60, 95, 70].map((w, i) => ( {[85, 60, 95, 70].map((w, i) => (
<div key={i} className={styles.taskRow}> <div key={i} className="flex items-center gap-3">
<div className={styles.taskCheck} /> <div className="w-4 h-4 border-2 border-gray-300 rounded-sm shrink-0" />
<div className={styles.taskLine} style={{ width: `${w}%` }} /> <div className="h-[10px] bg-gray-200 rounded-sm" style={{ width: `${w}%` }} />
</div> </div>
))} ))}
</div> </div>
@@ -163,11 +195,15 @@ export default function Hero() {
</div> </div>
{/* Stats */} {/* Stats */}
<div className={`${styles.reveal} ${styles.stats}`}> <div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out delay-700 flex flex-wrap justify-center gap-12 max-md:gap-8 mt-8">
{stats.map((stat) => ( {stats.map((stat) => (
<div key={stat.label} className={styles.statItem}> <div key={stat.label} className="flex flex-col items-center gap-1">
<span className={styles.statValue}>{stat.value}</span> <span className="text-3xl font-black tracking-[-0.04em] text-black">
<span className={styles.statLabel}>{stat.label}</span> {stat.value}
</span>
<span className="text-sm text-gray-500">
{stat.label}
</span>
</div> </div>
))} ))}
</div> </div>

View File

@@ -1,177 +0,0 @@
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid transparent;
transition: all var(--transition-base);
}
.scrolled {
border-bottom-color: var(--color-gray-200);
box-shadow: var(--shadow-sm);
}
.inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-8);
height: 72px;
}
/* Logo */
.logo {
display: flex;
align-items: center;
gap: var(--space-2);
text-decoration: none;
flex-shrink: 0;
}
.logoMark {
width: 36px;
height: 36px;
background: var(--color-black);
color: var(--color-white);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-xs);
font-weight: 800;
letter-spacing: -0.02em;
}
.logoText {
font-size: var(--text-lg);
font-weight: 800;
color: var(--color-black);
letter-spacing: -0.04em;
}
/* Desktop links */
.links {
display: flex;
align-items: center;
gap: var(--space-1);
list-style: none;
flex: 1;
justify-content: center;
}
.link {
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-gray-600);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
background: none;
border: none;
cursor: pointer;
font-family: var(--font-sans);
}
.link:hover {
color: var(--color-black);
background: var(--color-gray-100);
}
/* Desktop actions */
.actions {
display: flex;
align-items: center;
gap: var(--space-3);
flex-shrink: 0;
}
/* Mobile toggle */
.menuToggle {
display: none;
flex-direction: column;
gap: 5px;
padding: var(--space-2);
border-radius: var(--radius-md);
background: none;
border: none;
cursor: pointer;
}
.bar {
display: block;
width: 22px;
height: 2px;
background: var(--color-black);
border-radius: 2px;
transition: all var(--transition-fast);
transform-origin: center;
}
.bar.open:nth-child(1) {
transform: translateY(7px) rotate(45deg);
}
.bar.open:nth-child(2) {
opacity: 0;
transform: scaleX(0);
}
.bar.open:nth-child(3) {
transform: translateY(-7px) rotate(-45deg);
}
/* Mobile menu */
.mobileMenu {
background: var(--color-white);
border-top: 1px solid var(--color-gray-200);
padding: var(--space-4) var(--space-6);
animation: fadeInUp 0.2s ease;
}
.mobileLinks {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-1);
margin-bottom: var(--space-4);
}
.mobileLink {
display: block;
width: 100%;
text-align: left;
padding: var(--space-3) var(--space-4);
font-size: var(--text-base);
font-weight: 500;
color: var(--color-dark);
border-radius: var(--radius-md);
background: none;
border: none;
cursor: pointer;
font-family: var(--font-sans);
transition: background var(--transition-fast);
}
.mobileLink:hover {
background: var(--color-gray-100);
}
.mobileActions {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
@media (max-width: 768px) {
.links,
.actions {
display: none;
}
.menuToggle {
display: flex;
}
}

View File

@@ -1,7 +1,6 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import styles from './Navbar.module.css';
const navLinks = [ const navLinks = [
{ label: 'Características', href: '#features' }, { label: 'Características', href: '#features' },
@@ -26,20 +25,39 @@ export default function Navbar() {
}; };
return ( return (
<header className={`${styles.navbar} ${scrolled ? styles.scrolled : ''}`} role="banner"> <header
<nav className={`${styles.inner} container`} aria-label="Navegación principal"> className={`fixed top-0 left-0 right-0 z-[1000] bg-white/90 backdrop-blur-md border-b transition-all duration-250 ease-out ${
scrolled ? 'border-gray-200 shadow-sm' : 'border-transparent'
}`}
role="banner"
>
<nav
className="container flex items-center justify-between h-[72px] gap-8"
aria-label="Navegación principal"
>
{/* Logo */} {/* Logo */}
<a href="#" className={styles.logo} aria-label="FlowSync - inicio"> <a
<span className={styles.logoMark}>FS</span> href="#"
<span className={styles.logoText}>FlowSync</span> className="flex items-center gap-2 no-underline shrink-0"
aria-label="FlowSync - inicio"
>
<span className="w-9 h-9 bg-black text-white rounded-md flex items-center justify-center text-xs font-extrabold tracking-[-0.02em]">
FS
</span>
<span className="text-lg font-extrabold text-black tracking-[-0.04em]">
FlowSync
</span>
</a> </a>
{/* Desktop links */} {/* Desktop links */}
<ul className={styles.links} role="list"> <ul
className="hidden md:flex items-center justify-center gap-1 list-none flex-1"
role="list"
>
{navLinks.map((link) => ( {navLinks.map((link) => (
<li key={link.href}> <li key={link.href}>
<button <button
className={styles.link} className="px-3 py-2 text-sm font-medium text-gray-600 rounded-md transition-colors duration-150 ease-out bg-transparent border-none cursor-pointer hover:text-black hover:bg-gray-100"
onClick={() => handleNavClick(link.href)} onClick={() => handleNavClick(link.href)}
aria-label={`Ir a sección ${link.label}`} aria-label={`Ir a sección ${link.label}`}
> >
@@ -50,7 +68,7 @@ export default function Navbar() {
</ul> </ul>
{/* Desktop CTA */} {/* Desktop CTA */}
<div className={styles.actions}> <div className="hidden md:flex items-center gap-3 shrink-0">
<button className="btn btn-secondary" id="nav-login-btn"> <button className="btn btn-secondary" id="nav-login-btn">
Iniciar sesión Iniciar sesión
</button> </button>
@@ -65,26 +83,42 @@ export default function Navbar() {
{/* Mobile toggle */} {/* Mobile toggle */}
<button <button
className={styles.menuToggle} className="md:hidden flex flex-col gap-[5px] p-2 rounded-md bg-transparent border-none cursor-pointer"
onClick={() => setMenuOpen(!menuOpen)} onClick={() => setMenuOpen(!menuOpen)}
aria-label={menuOpen ? 'Cerrar menú' : 'Abrir menú'} aria-label={menuOpen ? 'Cerrar menú' : 'Abrir menú'}
aria-expanded={menuOpen} aria-expanded={menuOpen}
id="nav-menu-toggle" id="nav-menu-toggle"
> >
<span className={`${styles.bar} ${menuOpen ? styles.open : ''}`} /> <span
<span className={`${styles.bar} ${menuOpen ? styles.open : ''}`} /> className={`block w-[22px] h-[2px] bg-black rounded-sm transition-transform duration-150 ease-out origin-center ${
<span className={`${styles.bar} ${menuOpen ? styles.open : ''}`} /> menuOpen ? 'translate-y-[7px] rotate-45' : ''
}`}
/>
<span
className={`block w-[22px] h-[2px] bg-black rounded-sm transition-all duration-150 ease-out ${
menuOpen ? 'opacity-0 scale-x-0' : ''
}`}
/>
<span
className={`block w-[22px] h-[2px] bg-black rounded-sm transition-transform duration-150 ease-out origin-center ${
menuOpen ? '-translate-y-[7px] -rotate-45' : ''
}`}
/>
</button> </button>
</nav> </nav>
{/* Mobile menu */} {/* Mobile menu */}
{menuOpen && ( {menuOpen && (
<div className={styles.mobileMenu} role="dialog" aria-label="Menú móvil"> <div
<ul className={styles.mobileLinks} role="list"> className="md:hidden bg-white border-t border-gray-200 px-6 py-4 animate-fadeInUp"
role="dialog"
aria-label="Menú móvil"
>
<ul className="list-none flex flex-col gap-1 mb-4" role="list">
{navLinks.map((link) => ( {navLinks.map((link) => (
<li key={link.href}> <li key={link.href}>
<button <button
className={styles.mobileLink} className="block w-full text-left px-4 py-3 text-base font-medium text-dark rounded-md bg-transparent border-none cursor-pointer transition-colors duration-150 ease-out hover:bg-gray-100"
onClick={() => handleNavClick(link.href)} onClick={() => handleNavClick(link.href)}
> >
{link.label} {link.label}
@@ -92,13 +126,10 @@ export default function Navbar() {
</li> </li>
))} ))}
</ul> </ul>
<div className={styles.mobileActions}> <div className="flex flex-col gap-3">
<button className="btn btn-secondary" style={{ width: '100%' }}> <button className="btn btn-secondary w-full">Iniciar sesión</button>
Iniciar sesión
</button>
<button <button
className="btn btn-primary" className="btn btn-primary w-full"
style={{ width: '100%' }}
onClick={() => handleNavClick('#contact')} onClick={() => handleNavClick('#contact')}
> >
Prueba gratis Prueba gratis

View File

@@ -1,278 +0,0 @@
.reveal {
opacity: 0;
transform: translateY(24px);
transition: opacity 0.65s ease, transform 0.65s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
/* Header */
.header {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
margin-bottom: var(--space-16);
}
.title {
font-size: clamp(var(--text-3xl), 5vw, var(--text-5xl));
font-weight: 900;
letter-spacing: -0.04em;
line-height: 1.1;
color: var(--color-black);
}
.subtitle {
font-size: var(--text-lg);
color: var(--color-gray-600);
}
/* Toggle */
.toggle {
display: flex;
align-items: center;
gap: var(--space-3);
margin-top: var(--space-2);
}
.toggleLabel {
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-gray-400);
transition: color var(--transition-fast);
}
.toggleLabelActive {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-black);
}
.toggleSwitch {
width: 48px;
height: 26px;
background: var(--color-gray-200);
border-radius: var(--radius-full);
border: none;
cursor: pointer;
position: relative;
transition: background var(--transition-base);
}
.toggleSwitch[aria-pressed='true'] {
background: var(--color-black);
}
.toggleKnob {
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
background: var(--color-white);
border-radius: 50%;
transition: transform var(--transition-base);
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
}
.toggleKnobOn {
transform: translateX(22px);
}
.saveBadge {
display: inline-block;
margin-left: var(--space-2);
padding: 2px var(--space-2);
background: #dcfce7;
color: #15803d;
font-size: var(--text-xs);
font-weight: 700;
border-radius: var(--radius-full);
}
/* Plans grid */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-6);
align-items: start;
}
/* Card */
.card {
background: var(--color-white);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-2xl);
padding: var(--space-8);
display: flex;
flex-direction: column;
gap: var(--space-6);
position: relative;
transition: all var(--transition-base);
}
.card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
.cardHighlight {
background: var(--color-black);
border-color: var(--color-black);
color: var(--color-white);
}
.cardHighlight:hover {
box-shadow: 0 24px 64px rgba(0,0,0,0.2);
}
/* Popular badge */
.popularBadge {
position: absolute;
top: -14px;
left: 50%;
transform: translateX(-50%);
background: var(--color-accent);
color: var(--color-white);
font-size: var(--text-xs);
font-weight: 700;
padding: var(--space-1) var(--space-4);
border-radius: var(--radius-full);
white-space: nowrap;
letter-spacing: 0.04em;
}
/* Plan header */
.planHeader {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.planName {
font-size: var(--text-2xl);
font-weight: 800;
letter-spacing: -0.03em;
}
.cardHighlight .planName {
color: var(--color-white);
}
.planDesc {
font-size: var(--text-sm);
color: var(--color-gray-600);
line-height: 1.5;
}
.cardHighlight .planDesc {
color: rgba(255,255,255,0.6);
}
/* Price */
.priceRow {
display: flex;
align-items: flex-end;
gap: 2px;
}
.priceCurrency {
font-size: var(--text-xl);
font-weight: 700;
padding-bottom: 4px;
}
.priceValue {
font-size: clamp(var(--text-4xl), 6vw, 3.5rem);
font-weight: 900;
letter-spacing: -0.05em;
line-height: 1;
}
.priceUnit {
font-size: var(--text-sm);
color: var(--color-gray-400);
padding-bottom: 4px;
margin-left: var(--space-1);
}
.cardHighlight .priceUnit {
color: rgba(255,255,255,0.5);
}
.annualNote {
font-size: var(--text-xs);
color: var(--color-gray-400);
margin-top: calc(var(--space-6) * -1);
}
.cardHighlight .annualNote {
color: rgba(255,255,255,0.4);
}
/* Feature list */
.featureList {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.featureItem {
display: flex;
align-items: center;
gap: var(--space-3);
font-size: var(--text-sm);
color: var(--color-gray-700, #444);
}
.cardHighlight .featureItem {
color: rgba(255,255,255,0.8);
}
.checkIcon {
flex-shrink: 0;
color: var(--color-black);
}
.cardHighlight .checkIcon {
color: var(--color-white);
}
/* Enterprise note */
.enterpriseNote {
text-align: center;
margin-top: var(--space-12);
font-size: var(--text-base);
color: var(--color-gray-600);
}
.inlineLink {
background: none;
border: none;
cursor: pointer;
font-family: var(--font-sans);
font-size: var(--text-base);
font-weight: 600;
color: var(--color-black);
text-decoration: underline;
text-underline-offset: 3px;
transition: opacity var(--transition-fast);
}
.inlineLink:hover {
opacity: 0.6;
}
/* Responsive */
@media (max-width: 1024px) {
.grid {
grid-template-columns: 1fr;
max-width: 480px;
margin: 0 auto;
}
}

View File

@@ -1,7 +1,6 @@
'use client'; 'use client';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import styles from './Pricing.module.css';
const plans = [ const plans = [
{ {
@@ -69,12 +68,15 @@ export default function Pricing() {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => { (entries) => {
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting) entry.target.classList.add(styles.visible); if (entry.isIntersecting) {
entry.target.classList.add('opacity-100', 'translate-y-0');
entry.target.classList.remove('opacity-0', 'translate-y-6');
}
}); });
}, },
{ threshold: 0.1 } { threshold: 0.1 }
); );
const elements = sectionRef.current?.querySelectorAll(`.${styles.reveal}`); const elements = sectionRef.current?.querySelectorAll('.reveal');
elements?.forEach((el) => observer.observe(el)); elements?.forEach((el) => observer.observe(el));
return () => observer.disconnect(); return () => observer.disconnect();
}, []); }, []);
@@ -87,78 +89,132 @@ export default function Pricing() {
<section className="section" id="pricing" ref={sectionRef} aria-labelledby="pricing-heading"> <section className="section" id="pricing" ref={sectionRef} aria-labelledby="pricing-heading">
<div className="container"> <div className="container">
{/* Header */} {/* Header */}
<div className={`${styles.reveal} ${styles.header}`}> <div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out flex flex-col items-center text-center gap-4 mb-16">
<span className="badge badge-dark">Precios</span> <span className="badge badge-dark">Precios</span>
<h2 id="pricing-heading" className={styles.title}> <h2
id="pricing-heading"
className="text-[clamp(1.875rem,5vw,3rem)] font-black tracking-[-0.04em] leading-[1.1] text-black"
>
Transparente. Simple. Transparente. Simple.
<br /> <br />
Sin sorpresas. Sin sorpresas.
</h2> </h2>
<p className={styles.subtitle}> <p className="text-lg text-gray-600">
Comienza gratis. Escala cuando estés listo. Sin contratos a largo plazo. Comienza gratis. Escala cuando estés listo. Sin contratos a largo plazo.
</p> </p>
{/* Toggle */} {/* Toggle */}
<div className={styles.toggle}> <div className="flex items-center gap-3 mt-2">
<span className={!annual ? styles.toggleLabelActive : styles.toggleLabel}>Mensual</span> <span
className={`text-sm transition-colors duration-150 ease-out ${
!annual ? 'font-semibold text-black' : 'font-medium text-gray-400'
}`}
>
Mensual
</span>
<button <button
className={styles.toggleSwitch} className={`w-12 h-[26px] rounded-full border-none cursor-pointer relative transition-colors duration-250 ease-out ${
annual ? 'bg-black' : 'bg-gray-200'
}`}
onClick={() => setAnnual(!annual)} onClick={() => setAnnual(!annual)}
aria-pressed={annual} aria-pressed={annual}
aria-label="Cambiar a facturación anual" aria-label="Cambiar a facturación anual"
id="pricing-toggle-btn" id="pricing-toggle-btn"
> >
<span className={`${styles.toggleKnob} ${annual ? styles.toggleKnobOn : ''}`} /> <span
className={`absolute top-[3px] left-[3px] w-5 h-5 bg-white rounded-full transition-transform duration-250 ease-out shadow-[0_1px_4px_rgba(0,0,0,0.2)] ${
annual ? 'translate-x-[22px]' : ''
}`}
/>
</button> </button>
<span className={annual ? styles.toggleLabelActive : styles.toggleLabel}> <span
className={`text-sm flex items-center transition-colors duration-150 ease-out ${
annual ? 'font-semibold text-black' : 'font-medium text-gray-400'
}`}
>
Anual Anual
<span className={styles.saveBadge}>Ahorra 20%</span> <span className="inline-block ml-2 px-2 py-[2px] bg-green-100 text-green-700 text-xs font-bold rounded-full">
Ahorra 20%
</span>
</span> </span>
</div> </div>
</div> </div>
{/* Plans */} {/* Plans */}
<div className={styles.grid}> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start max-w-[480px] lg:max-w-none mx-auto">
{plans.map((plan, i) => ( {plans.map((plan, i) => (
<article <article
key={plan.id} key={plan.id}
className={`${styles.reveal} ${styles.card} ${plan.highlight ? styles.cardHighlight : ''}`} className={`reveal opacity-0 translate-y-6 transition-all duration-700 ease-out border rounded-2xl p-8 flex flex-col gap-6 relative group ${
style={{ transitionDelay: `${i * 0.1}s` }} plan.highlight
? 'bg-black border-black text-white hover:shadow-[0_24px_64px_rgba(0,0,0,0.2)]'
: 'bg-white border-gray-200 hover:shadow-lg hover:-translate-y-0.5'
}`}
style={{ transitionDelay: `${i * 100}ms` }}
aria-label={`Plan ${plan.name}`} aria-label={`Plan ${plan.name}`}
> >
{plan.badge && ( {plan.badge && (
<div className={styles.popularBadge} aria-label="Plan más popular"> <div
className="absolute -top-[14px] left-1/2 -translate-x-1/2 bg-accent text-white text-xs font-bold px-4 py-1 rounded-full whitespace-nowrap tracking-wider"
aria-label="Plan más popular"
>
{plan.badge} {plan.badge}
</div> </div>
)} )}
<div className={styles.planHeader}> <div className="flex flex-col gap-2">
<h3 className={styles.planName}>{plan.name}</h3> <h3
<p className={styles.planDesc}>{plan.description}</p> className={`text-2xl font-extrabold tracking-tight ${
plan.highlight ? 'text-white' : ''
}`}
>
{plan.name}
</h3>
<p
className={`text-sm leading-relaxed ${
plan.highlight ? 'text-white/60' : 'text-gray-600'
}`}
>
{plan.description}
</p>
</div> </div>
<div className={styles.priceRow}> <div className="flex items-end gap-[2px]">
{plan.price.monthly === 0 ? ( {plan.price.monthly === 0 ? (
<span className={styles.priceValue}>Gratis</span> <span className="text-[clamp(2.25rem,6vw,3.5rem)] font-black tracking-[-0.05em] leading-none">
Gratis
</span>
) : ( ) : (
<> <>
<span className={styles.priceCurrency}>$</span> <span className="text-xl font-bold pb-1">$</span>
<span className={styles.priceValue}> <span className="text-[clamp(2.25rem,6vw,3.5rem)] font-black tracking-[-0.05em] leading-none">
{annual ? plan.price.annual : plan.price.monthly} {annual ? plan.price.annual : plan.price.monthly}
</span> </span>
<span className={styles.priceUnit}>/mes</span> <span
className={`text-sm pb-1 ml-1 ${
plan.highlight ? 'text-white/50' : 'text-gray-400'
}`}
>
/mes
</span>
</> </>
)} )}
</div> </div>
{annual && plan.price.monthly > 0 && ( {annual && plan.price.monthly > 0 && (
<p className={styles.annualNote}> <p
className={`text-xs -mt-6 ${
plan.highlight ? 'text-white/40' : 'text-gray-400'
}`}
>
Facturado anualmente ${plan.price.annual * 12}/año Facturado anualmente ${plan.price.annual * 12}/año
</p> </p>
)} )}
<button <button
className={`btn btn-lg ${plan.highlight ? 'btn-accent' : 'btn-secondary'}`} className={`btn btn-lg ${
plan.highlight ? 'btn-accent' : 'btn-secondary'
}`}
id={`pricing-${plan.id}-btn`} id={`pricing-${plan.id}-btn`}
onClick={handleContactScroll} onClick={handleContactScroll}
aria-label={`${plan.cta} — Plan ${plan.name}`} aria-label={`${plan.cta} — Plan ${plan.name}`}
@@ -166,16 +222,21 @@ export default function Pricing() {
{plan.cta} {plan.cta}
</button> </button>
<ul className={styles.featureList} role="list"> <ul className="list-none flex flex-col gap-3" role="list">
{plan.features.map((f) => ( {plan.features.map((f) => (
<li key={f} className={styles.featureItem}> <li
key={f}
className={`flex items-center gap-3 text-sm ${
plan.highlight ? 'text-white/80' : 'text-gray-700'
}`}
>
<svg <svg
width="16" width="16"
height="16" height="16"
viewBox="0 0 16 16" viewBox="0 0 16 16"
fill="none" fill="none"
aria-hidden="true" aria-hidden="true"
className={styles.checkIcon} className={`shrink-0 ${plan.highlight ? 'text-white' : 'text-black'}`}
> >
<path <path
d="M3 8l3.5 3.5L13 4.5" d="M3 8l3.5 3.5L13 4.5"
@@ -194,11 +255,11 @@ export default function Pricing() {
</div> </div>
{/* Enterprise note */} {/* Enterprise note */}
<div className={`${styles.reveal} ${styles.enterpriseNote}`}> <div className="reveal opacity-0 translate-y-6 transition-all duration-700 ease-out text-center mt-12 text-base text-gray-600">
<p> <p>
¿Necesitas algo personalizado? &nbsp; ¿Necesitas algo personalizado? &nbsp;
<button <button
className={styles.inlineLink} className="bg-transparent border-none cursor-pointer font-sans text-base font-semibold text-black underline underline-offset-4 transition-opacity duration-150 ease-out hover:opacity-60"
onClick={handleContactScroll} onClick={handleContactScroll}
id="pricing-enterprise-contact-btn" id="pricing-enterprise-contact-btn"
> >