feat: Revamped design - improved dark mode visibility, mobile UX fixes, updated trust signals

This commit is contained in:
Satyam
2026-02-16 01:49:50 +05:30
parent e88e1e4da5
commit e0da94aa0c
15 changed files with 1887 additions and 377 deletions
+4 -1
View File
@@ -11,6 +11,8 @@
"preview": "vite preview"
},
"dependencies": {
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@vercel/analytics": "^1.6.1",
"autoprefixer": "^10.4.23",
"class-variance-authority": "^0.7.1",
@@ -30,7 +32,8 @@
"recharts": "^3.7.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^3.4.1"
"tailwindcss": "^3.4.1",
"three": "^0.182.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
+511
View File
@@ -8,6 +8,12 @@ importers:
.:
dependencies:
'@react-three/drei':
specifier: ^10.7.7
version: 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.10)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.182.0))(@types/react@19.2.10)(@types/three@0.182.0)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.182.0)
'@react-three/fiber':
specifier: ^9.5.0
version: 9.5.0(@types/react@19.2.10)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.182.0)
'@vercel/analytics':
specifier: ^1.6.1
version: 1.6.1(react@19.2.4)
@@ -68,6 +74,9 @@ importers:
tailwindcss:
specifier: ^3.4.1
version: 3.4.19
three:
specifier: ^0.182.0
version: 0.182.0
devDependencies:
'@eslint/js':
specifier: ^9.39.1
@@ -174,6 +183,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/runtime@7.28.6':
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
engines: {node: '>=6.9.0'}
'@babel/template@7.28.6':
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
engines: {node: '>=6.9.0'}
@@ -186,6 +199,9 @@ packages:
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@dimforge/rapier3d-compat@0.12.0':
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
'@esbuild/aix-ppc64@0.27.2':
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
engines: {node: '>=18'}
@@ -412,6 +428,14 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@mediapipe/tasks-vision@0.10.17':
resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==}
'@monogrid/gainmap-js@3.4.0':
resolution: {integrity: sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==}
peerDependencies:
three: '>= 0.159.0'
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -431,6 +455,42 @@ packages:
react: ^19.0.0
react-dom: ^19.0.0
'@react-three/drei@10.7.7':
resolution: {integrity: sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==}
peerDependencies:
'@react-three/fiber': ^9.0.0
react: ^19
react-dom: ^19
three: '>=0.159'
peerDependenciesMeta:
react-dom:
optional: true
'@react-three/fiber@9.5.0':
resolution: {integrity: sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==}
peerDependencies:
expo: '>=43.0'
expo-asset: '>=8.4'
expo-file-system: '>=11.0'
expo-gl: '>=11.0'
react: '>=19 <19.3'
react-dom: '>=19 <19.3'
react-native: '>=0.78'
three: '>=0.156'
peerDependenciesMeta:
expo:
optional: true
expo-asset:
optional: true
expo-file-system:
optional: true
expo-gl:
optional: true
react-dom:
optional: true
react-native:
optional: true
'@reduxjs/toolkit@2.11.2':
resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==}
peerDependencies:
@@ -576,6 +636,9 @@ packages:
'@standard-schema/utils@0.3.0':
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
'@tweenjs/tween.js@23.1.3':
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@@ -618,6 +681,9 @@ packages:
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/draco3d@1.4.10':
resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==}
'@types/estree-jsx@1.0.5':
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
@@ -642,14 +708,28 @@ packages:
'@types/node@18.19.130':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
'@types/offscreencanvas@2019.7.3':
resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==}
'@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
'@types/react': ^19.2.0
'@types/react-reconciler@0.28.9':
resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==}
peerDependencies:
'@types/react': '*'
'@types/react@19.2.10':
resolution: {integrity: sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==}
'@types/stats.js@0.17.4':
resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==}
'@types/three@0.182.0':
resolution: {integrity: sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==}
'@types/unist@2.0.11':
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
@@ -659,9 +739,20 @@ packages:
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
'@types/webxr@0.5.24':
resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==}
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
'@use-gesture/core@10.3.1':
resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==}
'@use-gesture/react@10.3.1':
resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==}
peerDependencies:
react: '>= 16.8.0'
'@vercel/analytics@1.6.1':
resolution: {integrity: sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==}
peerDependencies:
@@ -694,6 +785,9 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
'@webgpu/types@0.1.69':
resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==}
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
@@ -748,10 +842,16 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.9.19:
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
hasBin: true
bidi-js@1.0.3:
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
@@ -768,6 +868,9 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -780,6 +883,12 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
camera-controls@3.1.2:
resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==}
engines: {node: '>=22.0.0', npm: '>=10.5.1'}
peerDependencies:
three: '>=0.126.1'
caniuse-lite@1.0.30001766:
resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==}
@@ -841,6 +950,11 @@ packages:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
cross-env@7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
hasBin: true
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -923,6 +1037,9 @@ packages:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
detect-gpu@5.0.70:
resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==}
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
@@ -932,6 +1049,9 @@ packages:
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
draco3d@1.5.7:
resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -1062,6 +1182,12 @@ packages:
picomatch:
optional: true
fflate@0.6.10:
resolution: {integrity: sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==}
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@@ -1145,6 +1271,9 @@ packages:
resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
engines: {node: '>=18'}
glsl-noise@0.0.0:
resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@@ -1183,16 +1312,25 @@ packages:
hermes-parser@0.25.1:
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
hls.js@1.6.15:
resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==}
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immer@10.2.0:
resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
@@ -1250,9 +1388,17 @@ packages:
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
engines: {node: '>=12'}
is-promise@2.2.2:
resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
its-fine@2.0.0:
resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==}
peerDependencies:
react: ^19.0.0
jiti@1.21.7:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true
@@ -1307,6 +1453,9 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
@@ -1332,6 +1481,12 @@ packages:
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
maath@0.10.8:
resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==}
peerDependencies:
'@types/three': '>=0.134.0'
three: '>=0.134.0'
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -1364,6 +1519,14 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
meshline@3.3.1:
resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==}
peerDependencies:
three: '>=0.137'
meshoptimizer@0.22.0:
resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==}
micromark-core-commonmark@2.0.3:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
@@ -1587,10 +1750,16 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
potpack@1.0.2:
resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
promise-worker-transferable@1.0.4:
resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==}
property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
@@ -1655,6 +1824,15 @@ packages:
react-dom:
optional: true
react-use-measure@2.1.7:
resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==}
peerDependencies:
react: '>=16.13'
react-dom: '>=16.13'
peerDependenciesMeta:
react-dom:
optional: true
react@19.2.4:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
@@ -1688,6 +1866,10 @@ packages:
remark-rehype@11.1.2:
resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
@@ -1743,6 +1925,15 @@ packages:
space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
stats-gl@2.4.2:
resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==}
peerDependencies:
'@types/three': '*'
three: '*'
stats.js@0.17.0:
resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==}
stringify-entities@4.0.4:
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
@@ -1769,6 +1960,11 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
suspend-react@0.1.3:
resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==}
peerDependencies:
react: '>=17.0'
tailwind-merge@3.4.0:
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
@@ -1784,6 +1980,19 @@ packages:
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
three-mesh-bvh@0.8.3:
resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==}
peerDependencies:
three: '>= 0.159.0'
three-stdlib@2.36.1:
resolution: {integrity: sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==}
peerDependencies:
three: '>=0.128.0'
three@0.182.0:
resolution: {integrity: sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==}
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -1801,6 +2010,19 @@ packages:
trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
troika-three-text@0.52.4:
resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==}
peerDependencies:
three: '>=0.125.0'
troika-three-utils@0.52.4:
resolution: {integrity: sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==}
peerDependencies:
three: '>=0.125.0'
troika-worker-utils@0.52.0:
resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==}
trough@2.2.0:
resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
@@ -1810,6 +2032,9 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tunnel-rat@0.1.2:
resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -1852,6 +2077,10 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
utility-types@3.11.0:
resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==}
engines: {node: '>= 4'}
vfile-message@4.0.3:
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
@@ -1905,6 +2134,12 @@ packages:
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
engines: {node: '>= 14'}
webgl-constants@1.1.1:
resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==}
webgl-sdf-generator@1.1.1:
resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -1936,6 +2171,39 @@ packages:
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
zustand@4.5.7:
resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
engines: {node: '>=12.7.0'}
peerDependencies:
'@types/react': '>=16.8'
immer: '>=9.0.6'
react: '>=16.8'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
zustand@5.0.11:
resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
immer: '>=9.0.6'
react: '>=18.0.0'
use-sync-external-store: '>=1.2.0'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
use-sync-external-store:
optional: true
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@@ -2032,6 +2300,8 @@ snapshots:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
'@babel/runtime@7.28.6': {}
'@babel/template@7.28.6':
dependencies:
'@babel/code-frame': 7.29.0
@@ -2055,6 +2325,8 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@dimforge/rapier3d-compat@0.12.0': {}
'@esbuild/aix-ppc64@0.27.2':
optional: true
@@ -2209,6 +2481,13 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@mediapipe/tasks-vision@0.10.17': {}
'@monogrid/gainmap-js@3.4.0(three@0.182.0)':
dependencies:
promise-worker-transferable: 1.0.4
three: 0.182.0
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -2227,6 +2506,59 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@react-three/drei@10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.10)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.182.0))(@types/react@19.2.10)(@types/three@0.182.0)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.182.0)':
dependencies:
'@babel/runtime': 7.28.6
'@mediapipe/tasks-vision': 0.10.17
'@monogrid/gainmap-js': 3.4.0(three@0.182.0)
'@react-three/fiber': 9.5.0(@types/react@19.2.10)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.182.0)
'@use-gesture/react': 10.3.1(react@19.2.4)
camera-controls: 3.1.2(three@0.182.0)
cross-env: 7.0.3
detect-gpu: 5.0.70
glsl-noise: 0.0.0
hls.js: 1.6.15
maath: 0.10.8(@types/three@0.182.0)(three@0.182.0)
meshline: 3.3.1(three@0.182.0)
react: 19.2.4
stats-gl: 2.4.2(@types/three@0.182.0)(three@0.182.0)
stats.js: 0.17.0
suspend-react: 0.1.3(react@19.2.4)
three: 0.182.0
three-mesh-bvh: 0.8.3(three@0.182.0)
three-stdlib: 2.36.1(three@0.182.0)
troika-three-text: 0.52.4(three@0.182.0)
tunnel-rat: 0.1.2(@types/react@19.2.10)(immer@11.1.3)(react@19.2.4)
use-sync-external-store: 1.6.0(react@19.2.4)
utility-types: 3.11.0
zustand: 5.0.11(@types/react@19.2.10)(immer@11.1.3)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
optionalDependencies:
react-dom: 19.2.4(react@19.2.4)
transitivePeerDependencies:
- '@types/react'
- '@types/three'
- immer
'@react-three/fiber@9.5.0(@types/react@19.2.10)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.182.0)':
dependencies:
'@babel/runtime': 7.28.6
'@types/webxr': 0.5.24
base64-js: 1.5.1
buffer: 6.0.3
its-fine: 2.0.0(@types/react@19.2.10)(react@19.2.4)
react: 19.2.4
react-use-measure: 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
scheduler: 0.27.0
suspend-react: 0.1.3(react@19.2.4)
three: 0.182.0
use-sync-external-store: 1.6.0(react@19.2.4)
zustand: 5.0.11(@types/react@19.2.10)(immer@11.1.3)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
optionalDependencies:
react-dom: 19.2.4(react@19.2.4)
transitivePeerDependencies:
- '@types/react'
- immer
'@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.10)(react@19.2.4)(redux@5.0.1))(react@19.2.4)':
dependencies:
'@standard-schema/spec': 1.1.0
@@ -2320,6 +2652,8 @@ snapshots:
'@standard-schema/utils@0.3.0': {}
'@tweenjs/tween.js@23.1.3': {}
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.29.0
@@ -2369,6 +2703,8 @@ snapshots:
dependencies:
'@types/ms': 2.1.0
'@types/draco3d@1.4.10': {}
'@types/estree-jsx@1.0.5':
dependencies:
'@types/estree': 1.0.8
@@ -2396,22 +2732,49 @@ snapshots:
dependencies:
undici-types: 5.26.5
'@types/offscreencanvas@2019.7.3': {}
'@types/react-dom@19.2.3(@types/react@19.2.10)':
dependencies:
'@types/react': 19.2.10
'@types/react-reconciler@0.28.9(@types/react@19.2.10)':
dependencies:
'@types/react': 19.2.10
'@types/react@19.2.10':
dependencies:
csstype: 3.2.3
'@types/stats.js@0.17.4': {}
'@types/three@0.182.0':
dependencies:
'@dimforge/rapier3d-compat': 0.12.0
'@tweenjs/tween.js': 23.1.3
'@types/stats.js': 0.17.4
'@types/webxr': 0.5.24
'@webgpu/types': 0.1.69
fflate: 0.8.2
meshoptimizer: 0.22.0
'@types/unist@2.0.11': {}
'@types/unist@3.0.3': {}
'@types/use-sync-external-store@0.0.6': {}
'@types/webxr@0.5.24': {}
'@ungap/structured-clone@1.3.0': {}
'@use-gesture/core@10.3.1': {}
'@use-gesture/react@10.3.1(react@19.2.4)':
dependencies:
'@use-gesture/core': 10.3.1
react: 19.2.4
'@vercel/analytics@1.6.1(react@19.2.4)':
optionalDependencies:
react: 19.2.4
@@ -2428,6 +2791,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@webgpu/types@0.1.69': {}
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
@@ -2479,8 +2844,14 @@ snapshots:
balanced-match@1.0.2: {}
base64-js@1.5.1: {}
baseline-browser-mapping@2.9.19: {}
bidi-js@1.0.3:
dependencies:
require-from-string: 2.0.2
binary-extensions@2.3.0: {}
brace-expansion@1.1.12:
@@ -2500,6 +2871,11 @@ snapshots:
node-releases: 2.0.27
update-browserslist-db: 1.2.3(browserslist@4.28.1)
buffer@6.0.3:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -2509,6 +2885,10 @@ snapshots:
camelcase-css@2.0.1: {}
camera-controls@3.1.2(three@0.182.0):
dependencies:
three: 0.182.0
caniuse-lite@1.0.30001766: {}
ccount@2.0.1: {}
@@ -2564,6 +2944,10 @@ snapshots:
cookie@1.1.1: {}
cross-env@7.0.3:
dependencies:
cross-spawn: 7.0.6
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -2628,6 +3012,10 @@ snapshots:
dequal@2.0.3: {}
detect-gpu@5.0.70:
dependencies:
webgl-constants: 1.1.1
devlop@1.1.0:
dependencies:
dequal: 2.0.3
@@ -2636,6 +3024,8 @@ snapshots:
dlv@1.1.3: {}
draco3d@1.5.7: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -2807,6 +3197,10 @@ snapshots:
optionalDependencies:
picomatch: 4.0.3
fflate@0.6.10: {}
fflate@0.8.2: {}
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@@ -2890,6 +3284,8 @@ snapshots:
globals@16.5.0: {}
glsl-noise@0.0.0: {}
gopd@1.2.0: {}
groq-sdk@0.37.0:
@@ -2948,14 +3344,20 @@ snapshots:
dependencies:
hermes-estree: 0.25.1
hls.js@1.6.15: {}
html-url-attributes@3.0.1: {}
humanize-ms@1.2.1:
dependencies:
ms: 2.1.3
ieee754@1.2.1: {}
ignore@5.3.2: {}
immediate@3.0.6: {}
immer@10.2.0: {}
immer@11.1.3: {}
@@ -3000,8 +3402,17 @@ snapshots:
is-plain-obj@4.1.0: {}
is-promise@2.2.2: {}
isexe@2.0.0: {}
its-fine@2.0.0(@types/react@19.2.10)(react@19.2.4):
dependencies:
'@types/react-reconciler': 0.28.9(@types/react@19.2.10)
react: 19.2.4
transitivePeerDependencies:
- '@types/react'
jiti@1.21.7: {}
js-tokens@4.0.0: {}
@@ -3035,6 +3446,10 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
lie@3.3.0:
dependencies:
immediate: 3.0.6
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
@@ -3055,6 +3470,11 @@ snapshots:
dependencies:
react: 19.2.4
maath@0.10.8(@types/three@0.182.0)(three@0.182.0):
dependencies:
'@types/three': 0.182.0
three: 0.182.0
math-intrinsics@1.1.0: {}
mdast-util-from-markdown@2.0.2:
@@ -3148,6 +3568,12 @@ snapshots:
merge2@1.4.1: {}
meshline@3.3.1(three@0.182.0):
dependencies:
three: 0.182.0
meshoptimizer@0.22.0: {}
micromark-core-commonmark@2.0.3:
dependencies:
decode-named-character-reference: 1.3.0
@@ -3412,8 +3838,15 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
potpack@1.0.2: {}
prelude-ls@1.2.1: {}
promise-worker-transferable@1.0.4:
dependencies:
is-promise: 2.2.2
lie: 3.3.0
property-information@7.1.0: {}
punycode@2.3.1: {}
@@ -3477,6 +3910,12 @@ snapshots:
optionalDependencies:
react-dom: 19.2.4(react@19.2.4)
react-use-measure@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
react: 19.2.4
optionalDependencies:
react-dom: 19.2.4(react@19.2.4)
react@19.2.4: {}
read-cache@1.0.0:
@@ -3530,6 +3969,8 @@ snapshots:
unified: 11.0.5
vfile: 6.0.3
require-from-string@2.0.2: {}
reselect@5.1.1: {}
resolve-from@4.0.0: {}
@@ -3598,6 +4039,13 @@ snapshots:
space-separated-tokens@2.0.2: {}
stats-gl@2.4.2(@types/three@0.182.0)(three@0.182.0):
dependencies:
'@types/three': 0.182.0
three: 0.182.0
stats.js@0.17.0: {}
stringify-entities@4.0.4:
dependencies:
character-entities-html4: 2.1.0
@@ -3629,6 +4077,10 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
suspend-react@0.1.3(react@19.2.4):
dependencies:
react: 19.2.4
tailwind-merge@3.4.0: {}
tailwindcss@3.4.19:
@@ -3667,6 +4119,22 @@ snapshots:
dependencies:
any-promise: 1.3.0
three-mesh-bvh@0.8.3(three@0.182.0):
dependencies:
three: 0.182.0
three-stdlib@2.36.1(three@0.182.0):
dependencies:
'@types/draco3d': 1.4.10
'@types/offscreencanvas': 2019.7.3
'@types/webxr': 0.5.24
draco3d: 1.5.7
fflate: 0.6.10
potpack: 1.0.2
three: 0.182.0
three@0.182.0: {}
tiny-invariant@1.3.3: {}
tinyglobby@0.2.15:
@@ -3682,12 +4150,34 @@ snapshots:
trim-lines@3.0.1: {}
troika-three-text@0.52.4(three@0.182.0):
dependencies:
bidi-js: 1.0.3
three: 0.182.0
troika-three-utils: 0.52.4(three@0.182.0)
troika-worker-utils: 0.52.0
webgl-sdf-generator: 1.1.1
troika-three-utils@0.52.4(three@0.182.0):
dependencies:
three: 0.182.0
troika-worker-utils@0.52.0: {}
trough@2.2.0: {}
ts-interface-checker@0.1.13: {}
tslib@2.8.1: {}
tunnel-rat@0.1.2(@types/react@19.2.10)(immer@11.1.3)(react@19.2.4):
dependencies:
zustand: 4.5.7(@types/react@19.2.10)(immer@11.1.3)(react@19.2.4)
transitivePeerDependencies:
- '@types/react'
- immer
- react
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@@ -3743,6 +4233,8 @@ snapshots:
util-deprecate@1.0.2: {}
utility-types@3.11.0: {}
vfile-message@4.0.3:
dependencies:
'@types/unist': 3.0.3
@@ -3784,6 +4276,10 @@ snapshots:
web-streams-polyfill@4.0.0-beta.3: {}
webgl-constants@1.1.1: {}
webgl-sdf-generator@1.1.1: {}
webidl-conversions@3.0.1: {}
whatwg-url@5.0.0:
@@ -3807,4 +4303,19 @@ snapshots:
zod@4.3.6: {}
zustand@4.5.7(@types/react@19.2.10)(immer@11.1.3)(react@19.2.4):
dependencies:
use-sync-external-store: 1.6.0(react@19.2.4)
optionalDependencies:
'@types/react': 19.2.10
immer: 11.1.3
react: 19.2.4
zustand@5.0.11(@types/react@19.2.10)(immer@11.1.3)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)):
optionalDependencies:
'@types/react': 19.2.10
immer: 11.1.3
react: 19.2.4
use-sync-external-store: 1.6.0(react@19.2.4)
zwitch@2.0.4: {}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

+433 -125
View File
@@ -1,197 +1,453 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
import { useNavigate } from 'react-router-dom';
import { Icons } from './landing/constants';
import MapBg from '../assets/images/hyper_intelligence_bg_image.jpg';
import MapBg from '../assets/images/intel_map_dfw_twilight.png';
import IntelligenceSidePanel from './dashboard/IntelligenceSidePanel';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
// Updated with Residential & Commercial mix
gsap.registerPlugin(ScrollTrigger);
// ── 10 Hotspots — 5 Commercial + 5 Residential, DFW Metro ──
const hotspots = [
// Commercial
{
id: 1,
top: '45%',
left: '65%',
address: 'Preston Royal Village',
id: 1, top: '22%', left: '58%',
address: '8333 Douglas Ave, Dallas',
type: 'Commercial',
problem: 'Roof Membrane Uplift',
gain: '+$8.5M Value',
gain: '+$8.5M',
weather: 'Hail Expected',
status: 'Urgent',
specs: { built: '1998', maintenance: '5 years since last coating' },
specs: { built: '1998', maintenance: '5 yrs since last coating' },
management: { name: 'Sterling Properties', contact: 'Sign in for contact details' },
photos: [
{ id: 1, url: '/assets/images/properties/Warehouse_Roof_Aerial.jpg', caption: 'Warehouse Roof Aerial' },
{ id: 2, url: '/assets/images/properties/Office_Roof_Top_View.jpg', caption: 'Office Roof Top' }
{ id: 'c1-1', url: '/assets/images/properties/Apartment_Complex.jpg', caption: 'Exterior View' },
{ id: 'c1-2', url: '/assets/images/properties/Office_Roof_Top_View.jpg', caption: 'Roof Aerial' },
{ id: 'c1-3', url: '/assets/images/properties/Broken_Roof_Surface.jpg', caption: 'Membrane Damage' },
]
},
{
id: 2,
top: '28%',
left: '42%',
address: 'Highland Park Place',
id: 2, top: '35%', left: '38%',
address: '5600 Tennyson Pkwy, Plano',
type: 'Commercial',
problem: 'Critical Membrane Failure',
gain: '$22.5 Million',
gain: '+$22.5M',
weather: 'Clear',
status: 'Monitoring',
specs: { built: '1982', maintenance: '20 years since last roof replacement permit' },
specs: { built: '1982', maintenance: '20 yrs since roof replacement' },
management: { name: 'Goldman Group', contact: 'Sign in for contact details' },
photos: [
{ id: 3, url: '/assets/images/properties/Mall_Rooftop_HVAC_Units.jpg', caption: 'Rooftop HVAC Units' }
{ id: 'c2-1', url: '/assets/images/properties/Warehouse_Roof_Aerial.jpg', caption: 'Aerial Survey' },
{ id: 'c2-2', url: '/assets/images/properties/Mall_Rooftop_HVAC_Units.jpg', caption: 'HVAC Systems' },
{ id: 'c2-3', url: '/assets/images/properties/Storm_Worn_Roof.jpg', caption: 'Surface Wear' },
{ id: 'c2-4', url: '/assets/images/properties/Cracked_Asphalt_Shingles.jpg', caption: 'Substrate Cracking' },
]
},
{
id: 3,
top: '55%',
left: '25%',
address: '4200 Beverly Dr',
id: 3, top: '48%', left: '72%',
address: '1900 Preston Rd, Frisco',
type: 'Commercial',
problem: 'Ponding Water Accumulation',
gain: '+$4.2M',
weather: 'Storm Watch',
status: 'Action Required',
specs: { built: '2005', maintenance: '3 yrs since drainage check' },
management: { name: 'Hillcrest Capital', contact: 'Sign in for contact details' },
photos: [
{ id: 'c3-1', url: '/assets/images/properties/Apartment_Complex.jpg', caption: 'Building Exterior' },
{ id: 'c3-2', url: '/assets/images/properties/Mall_Rooftop_HVAC_Units.jpg', caption: 'Rooftop Overview' },
{ id: 'c3-3', url: '/assets/images/properties/Broken_Roof_Surface.jpg', caption: 'Ponding Damage' },
]
},
{
id: 4, top: '62%', left: '28%',
address: '2100 N Hwy 360, Grand Prairie',
type: 'Commercial',
problem: 'HVAC Penetration Leak',
gain: '+$6.8M',
weather: 'Windy',
status: 'Monitoring',
specs: { built: '1995', maintenance: '8 yrs since waterproofing' },
management: { name: 'Apex Industrial Partners', contact: 'Sign in for contact details' },
photos: [
{ id: 'c4-1', url: '/assets/images/properties/Warehouse_Roof_Aerial.jpg', caption: 'Facility Aerial' },
{ id: 'c4-2', url: '/assets/images/properties/Mall_Rooftop_HVAC_Units.jpg', caption: 'HVAC Penetration' },
{ id: 'c4-3', url: '/assets/images/properties/Cracked_Storm_Shingles.jpg', caption: 'Seal Failure' },
]
},
{
id: 5, top: '15%', left: '20%',
address: '401 Commerce St, Fort Worth',
type: 'Commercial',
problem: 'Flashing Deterioration',
gain: '+$11.3M',
weather: 'Hail Expected',
status: 'Urgent',
specs: { built: '1990', maintenance: '12 yrs since seal replacement' },
management: { name: 'Tarrant Realty Holdings', contact: 'Sign in for contact details' },
photos: [
{ id: 'c5-1', url: '/assets/images/properties/Office_Roof_Top_View.jpg', caption: 'Roof Survey' },
{ id: 'c5-2', url: '/assets/images/properties/Apartment_Complex.jpg', caption: 'Building Facade' },
{ id: 'c5-3', url: '/assets/images/properties/Curled_Aging_Shingles.jpg', caption: 'Flashing Wear' },
{ id: 'c5-4', url: '/assets/images/properties/Storm_Worn_Roof.jpg', caption: 'Wind Exposure' },
]
},
// Residential
{
id: 6, top: '55%', left: '50%',
address: '4200 Beverly Dr, Highland Park',
type: 'Residential',
problem: 'Severe Hail Impact',
gain: '$1.1M',
gain: '+$1.1M',
weather: 'Rain Soon',
status: 'Action Required',
specs: { roofSize: '62 sq Roof', eventHistory: '8 years since last known hail event' },
specs: { roofSize: '62 sq', eventHistory: '8 yrs since last hail event' },
owner: { fico: '715', profile: 'Early Adopter / Long-term value' },
photos: [
{ id: 4, url: '/assets/images/properties/Hail_Damaged_Shingles.jpg', caption: 'Hail Damage Close-up' },
{ id: 5, url: '/assets/images/properties/Broken_Roof_Surface.jpg', caption: 'Broken Surface' }
{ id: 'r6-1', url: '/assets/images/properties/Luxury_Driveway_Residence.jpg', caption: 'Front Exterior' },
{ id: 'r6-2', url: '/assets/images/properties/Hail_Damaged_Shingles.jpg', caption: 'Hail Damage' },
{ id: 'r6-3', url: '/assets/images/properties/Cracked_Storm_Shingles.jpg', caption: 'Storm Damage' },
]
},
{
id: 4,
top: '70%',
left: '48%',
address: '3505 Lexington Ave',
id: 7, top: '72%', left: '65%',
address: '3505 Lexington Ave, University Park',
type: 'Residential',
problem: 'Asphalt Shingle Loss',
gain: '+$950k Home',
gain: '+$950K',
weather: 'Windy',
status: 'Low Priority',
specs: { roofSize: '45 sq Roof', eventHistory: 'High Wind Zone' },
specs: { roofSize: '45 sq', eventHistory: 'High Wind Zone' },
owner: { fico: '680', profile: 'First-time Buyer' },
photos: [
{ id: 6, url: '/assets/images/properties/Cracked_Asphalt_Shingles.jpg', caption: 'Cracked Shingles' },
{ id: 7, url: '/assets/images/properties/Curled_Aging_Shingles.jpg', caption: 'Aging Shingles' }
{ id: 'r7-1', url: '/assets/images/properties/Modern_White_Suburban_Home.jpg', caption: 'Front View' },
{ id: 'r7-2', url: '/assets/images/properties/Cracked_Asphalt_Shingles.jpg', caption: 'Shingle Loss' },
{ id: 'r7-3', url: '/assets/images/properties/Roof_Inspection_Angle.jpg', caption: 'Drone Inspection' },
]
},
{
id: 8, top: '40%', left: '15%',
address: '7212 Covewood Dr, Garland',
type: 'Residential',
problem: 'Aging Composite Shingles',
gain: '+$420K',
weather: 'Clear',
status: 'Low Priority',
specs: { roofSize: '38 sq', eventHistory: 'No recent events' },
owner: { fico: '740', profile: 'Long-term Owner' },
photos: [
{ id: 'r8-1', url: '/assets/images/properties/Beige_Two_Story_House.jpg', caption: 'Street View' },
{ id: 'r8-2', url: '/assets/images/properties/Curled_Aging_Shingles.jpg', caption: 'Aging Shingles' },
]
},
{
id: 9, top: '75%', left: '42%',
address: '1520 Creekside Dr, Allen',
type: 'Residential',
problem: 'Ridge Cap Separation',
gain: '+$780K',
weather: 'Hail Expected',
status: 'Urgent',
specs: { roofSize: '55 sq', eventHistory: '2 hail events in 3 years' },
owner: { fico: '695', profile: 'Insurance-motivated' },
photos: [
{ id: 'r9-1', url: '/assets/images/properties/Brick_Front_Porch_Home.jpg', caption: 'Entryway' },
{ id: 'r9-2', url: '/assets/images/properties/Overlapping_Roof_Shingles.jpg', caption: 'Ridge Detail' },
{ id: 'r9-3', url: '/assets/images/properties/Hail_Damaged_Shingles.jpg', caption: 'Hail Damage' },
{ id: 'r9-4', url: '/assets/images/properties/Storm_Worn_Roof.jpg', caption: 'Worn Surface' },
]
},
{
id: 10, top: '30%', left: '82%',
address: '905 W Main St, Richardson',
type: 'Residential',
problem: 'Granule Loss Pattern',
gain: '+$560K',
weather: 'Storm Watch',
status: 'Action Required',
specs: { roofSize: '42 sq', eventHistory: '5 yrs since installation' },
owner: { fico: '720', profile: 'Recently Purchased' },
photos: [
{ id: 'r10-1', url: '/assets/images/properties/Red_Shingle_Cottage.jpg', caption: 'Cottage Exterior' },
{ id: 'r10-2', url: '/assets/images/properties/Residential_Roof_Texture.jpg', caption: 'Roof Texture' },
{ id: 'r10-3', url: '/assets/images/properties/Cracked_Asphalt_Shingles.jpg', caption: 'Granule Loss' },
]
},
];
const FILTERS = ['All', 'Commercial', 'Residential', 'Storm Path'];
const STORM_WEATHER = ['Hail Expected', 'Storm Watch', 'Rain Soon'];
const metrics = [
{ label: 'Total Pipeline', value: 57.1, prefix: '$', suffix: 'M', color: 'text-blue-500', stamp: '\u25CF Active', stampClass: 'permit-stamp--accent' },
{ label: 'Active Alerts', value: 7, prefix: '', suffix: '', color: 'text-amber-500', stamp: '\u25B2 Monitoring', stampClass: 'permit-stamp--accent' },
{ label: 'Urgent', value: 3, prefix: '', suffix: '', color: 'text-red-500', stamp: '\u2715 Critical', stampClass: 'permit-stamp--danger' },
{ label: 'Avg Roof Age', value: 18, prefix: '', suffix: ' yrs', color: 'text-emerald-500', stamp: '\u2713 Assessed', stampClass: 'permit-stamp--verified' },
];
const isSpotVisible = (spot, filter) => {
if (filter === 'All') return true;
if (filter === 'Commercial') return spot.type === 'Commercial';
if (filter === 'Residential') return spot.type === 'Residential';
if (filter === 'Storm Path') return STORM_WEATHER.includes(spot.weather);
return true;
};
const IntelligenceMap = () => {
const navigate = useNavigate();
const [activeSpot, setActiveSpot] = useState(null);
const [activeFilter, setActiveFilter] = useState('All');
const [hoveredSpot, setHoveredSpot] = useState(null);
const [showRescheduleDialog, setShowRescheduleDialog] = useState(false);
const sectionRef = useRef(null);
const pinRefs = useRef([]);
const metricRefs = useRef([]);
const mapGlowRef = useRef(null);
// Reschedule dialog timer
useEffect(() => {
// Simulate incoming weather alert
const timer = setTimeout(() => {
setShowRescheduleDialog(true);
}, 2500);
const timer = setTimeout(() => setShowRescheduleDialog(true), 2500);
return () => clearTimeout(timer);
}, []);
// GSAP: Staggered pin entrance + metric counters
useEffect(() => {
const ctx = gsap.context(() => {
// Pin entrance
const pins = pinRefs.current.filter(Boolean);
pins.forEach(pin => gsap.set(pin, { opacity: 0, scale: 0, y: 20 }));
gsap.to(pins, {
opacity: 1, scale: 1, y: 0,
duration: 0.6, stagger: 0.08,
ease: 'back.out(1.7)',
scrollTrigger: {
trigger: sectionRef.current,
start: 'top 70%',
toggleActions: 'play none none none',
},
onComplete: () => {
// Clear GSAP inline styles so CSS transitions take over
pins.forEach(pin => {
if (pin) gsap.set(pin, { clearProps: 'opacity,transform' });
});
},
});
// Metric counters — proxy object approach
metricRefs.current.forEach((el) => {
if (!el) return;
const target = parseFloat(el.dataset.target);
const suffix = el.dataset.suffix || '';
const prefix = el.dataset.prefix || '';
const counter = { value: 0 };
el.innerText = prefix + '0' + suffix;
gsap.to(counter, {
value: target,
duration: 2,
ease: 'power1.out',
scrollTrigger: {
trigger: el,
start: 'top 85%',
toggleActions: 'play none none none',
},
onUpdate: () => {
const display = target % 1 !== 0
? counter.value.toFixed(1)
: Math.ceil(counter.value).toString();
el.innerText = prefix + display + suffix;
},
});
});
}, sectionRef);
return () => ctx.revert();
}, []);
// Mouse-follow glow on map
const handleMapMouseMove = (e) => {
if (!mapGlowRef.current) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
mapGlowRef.current.style.background =
`radial-gradient(350px circle at ${x}px ${y}px, rgba(6,182,212,0.08), transparent 60%)`;
};
const handleMapMouseLeave = () => {
if (mapGlowRef.current) mapGlowRef.current.style.background = 'transparent';
};
return (
<section className="py-24 bg-gray-900 border-t border-white/5 relative overflow-hidden">
<div className="container mx-auto px-4 relative z-10">
<div className="mb-12 text-center max-w-4xl mx-auto">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-cyan-400/10 border border-cyan-400/20 mb-4">
<div className="w-1.5 h-1.5 bg-cyan-400 rounded-full animate-pulse"></div>
<span className="text-cyan-400 text-xs font-mono font-bold uppercase tracking-widest">Live CRM Data Feed</span>
<section
ref={sectionRef}
className="py-24 bg-zinc-100 dark:bg-zinc-900 border-t border-zinc-200 dark:border-white/5 relative overflow-hidden"
>
{/* Blueprint grid bg */}
<div className="absolute inset-0 blueprint-grid-fine"></div>
<div className="max-w-7xl mx-auto px-6 relative z-10">
{/* ── Header ── */}
<div className="mb-8 text-center max-w-4xl mx-auto">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-cyan-500/10 dark:bg-cyan-400/10 border border-cyan-500/20 dark:border-cyan-400/20 mb-4">
<div className="w-1.5 h-1.5 bg-cyan-500 dark:bg-cyan-400 rounded-full animate-pulse"></div>
<span className="text-cyan-600 dark:text-cyan-400 text-xs font-mono font-bold uppercase tracking-widest">Live CRM Data Feed</span>
</div>
<h2 className="text-3xl md:text-5xl font-heading font-bold text-white mb-6">Hyper-Local Sales Intelligence</h2>
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
<h2 className="text-3xl md:text-5xl font-black text-zinc-900 dark:text-white mb-4 tracking-tight">
Hyper-Local Sales Intelligence
</h2>
<p className="text-zinc-500 dark:text-zinc-400 text-lg max-w-2xl mx-auto">
Our systems overlay live NOAA weather data with property value insights. Maximize revenue by targeting neighborhoods directly in the storm path.
</p>
</div>
{/* Map Container Frame */}
<div className="relative w-full h-[650px] bg-black rounded-2xl border border-white/20 shadow-2xl overflow-hidden group isolate ring-1 ring-white/10">
{/* ── Filter Bar ── */}
<div className="flex flex-wrap items-center justify-center gap-2 sm:gap-3 mb-8">
{FILTERS.map((filter) => (
<button
key={filter}
onClick={() => { setActiveFilter(filter); setActiveSpot(null); }}
className={`font-mono text-[10px] font-bold uppercase tracking-widest px-4 py-2 rounded-md border transition-all duration-200
${activeFilter === filter
? 'bg-cyan-500/10 dark:bg-cyan-400/10 text-cyan-600 dark:text-cyan-400 border-cyan-500/30 dark:border-cyan-400/30'
: 'bg-white dark:bg-white/5 text-zinc-500 dark:text-gray-500 border-zinc-200 dark:border-white/10 hover:text-zinc-900 dark:hover:text-white hover:border-zinc-400 dark:hover:border-white/20'
}
`}
>
{filter}
</button>
))}
</div>
{/* Map Background - DFW Satellite Style */}
{/* ══════════════════════════════════════════════ */}
{/* MAP CONTAINER — always dark (satellite image) */}
{/* ══════════════════════════════════════════════ */}
<div
className="relative w-full h-[400px] md:h-[550px] lg:h-[650px] bg-black rounded-2xl border border-zinc-300 dark:border-white/20 shadow-2xl overflow-hidden isolate ring-1 ring-zinc-200 dark:ring-white/10"
onMouseMove={handleMapMouseMove}
onMouseLeave={handleMapMouseLeave}
>
{/* Map Background */}
<div className="absolute inset-0 bg-[#0f172a]">
{/* Aerial/Satellite View of a Grid City */}
<img
src={MapBg}
alt="DFW Metroplex Map Overlay"
className="w-full h-full object-cover opacity-60 mix-blend-luminosity hover:scale-105 transition-transform duration-[20s]"
className="w-full h-full object-cover opacity-60 mix-blend-luminosity"
/>
{/* Map Grid Overlay */}
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20"></div>
<div className="absolute inset-0 bg-grid-pattern opacity-10 bg-[length:40px_40px] border-t border-white/5"></div>
</div>
{/* Region Indicator - HUD Style */}
<div className="absolute top-6 left-6 z-30">
<div className="bg-slate-900/80 backdrop-blur-md text-white pl-4 pr-6 py-2 rounded-r-lg border-l-4 border-cyan-400 shadow-[0_0_15px_rgba(0,229,255,0.2)] flex items-center gap-3">
<div>
<div className="text-[10px] text-cyan-400 font-bold uppercase tracking-widest mb-0.5">Active Region</div>
<div className="font-heading font-bold text-lg leading-none">DFW Zone 2</div>
</div>
</div>
{/* Mouse-follow glow */}
<div
ref={mapGlowRef}
className="absolute inset-0 pointer-events-none z-10 transition-[background] duration-150"
/>
{/* Region Indicator — HUD */}
<div className="absolute top-4 left-4 z-30">
<div className="bg-slate-900/80 backdrop-blur-md text-white pl-3 pr-4 py-1.5 md:pl-4 md:pr-6 md:py-2 rounded-r-lg border-l-4 border-cyan-400 shadow-[0_0_15px_rgba(0,229,255,0.2)]">
<div className="text-[9px] md:text-[10px] text-cyan-400 font-bold uppercase tracking-widest mb-0.5">Active Region</div>
<div className="font-black text-sm md:text-lg leading-none">DFW Zone 2</div>
</div>
</div>
{/* Weather Overlay Widget */}
<div className="absolute top-0 right-0 w-full h-full bg-gradient-to-bl from-slate-900/80 via-transparent to-transparent pointer-events-none"></div>
<div className="absolute top-6 right-6 p-4 bg-black/60 backdrop-blur-md rounded-lg border border-white/10 max-w-xs z-30">
<div className="flex items-center gap-4">
<div className="bg-white/10 p-2 rounded-full text-white">
<span className="text-2xl"></span>
</div>
<div>
<div className="font-bold text-white text-sm">Severe Hail Watch</div>
<div className="text-xs text-cyan-400 animate-pulse">Approaching Tarrant County</div>
<div className="text-[10px] text-gray-500 font-mono mt-1">ETA: 15 MINS</div>
</div>
{/* Weather Widget */}
<div className="absolute top-0 right-0 w-full h-full bg-gradient-to-bl from-slate-900/80 via-transparent to-transparent pointer-events-none"></div>
<div className="absolute top-4 right-4 p-3 md:p-4 bg-black/60 backdrop-blur-md rounded-lg border border-white/10 max-w-[180px] md:max-w-xs z-30 hidden sm:block">
<div className="flex items-center gap-3 md:gap-4">
<div className="bg-white/10 p-1.5 md:p-2 rounded-full text-white">
<span className="text-xl md:text-2xl">{'\u26C8'}</span>
</div>
<div>
<div className="font-bold text-white text-xs md:text-sm">Severe Hail Watch</div>
<div className="text-[10px] md:text-xs text-cyan-400 animate-pulse">Approaching Tarrant County</div>
<div className="text-[9px] md:text-[10px] text-gray-500 font-mono mt-1">ETA: 15 MINS</div>
</div>
</div>
</div>
{/* Interactive Pins */}
{hotspots.map((spot) => (
<div
key={spot.id}
className="absolute group/pin z-20 cursor-pointer"
style={{ top: spot.top, left: spot.left }}
onClick={() => setActiveSpot(spot)}
>
{/* Radar Ripple Effect */}
<div className={`absolute -inset-4 rounded-full animate-ping opacity-75 ${spot.type === 'Commercial' ? 'bg-purple-500/20' : 'bg-cyan-400/20'}`}></div>
{/* ── Interactive Pins ── */}
{hotspots.map((spot, index) => {
const visible = isSpotVisible(spot, activeFilter);
const isDimmed = activeSpot && activeSpot.id !== spot.id;
const isCommercial = spot.type === 'Commercial';
{/* Price Tag Pin */}
<div className="relative flex flex-col items-center transform group-hover/pin:scale-110 transition-transform duration-200">
<div className={`bg-slate-900/90 backdrop-blur text-[10px] font-mono font-bold px-2 py-1 rounded border shadow-[0_0_10px_rgba(0,0,0,0.5)] whitespace-nowrap mb-1 opacity-0 group-hover/pin:opacity-100 transition-opacity ${spot.type === 'Commercial' ? 'text-purple-400 border-purple-400/30' : 'text-cyan-400 border-cyan-400/30'}`}>
{spot.gain}
return (
<div
key={spot.id}
ref={(el) => { pinRefs.current[index] = el; }}
className={`absolute group/pin z-20 cursor-pointer transition-all duration-300 ease-in-out
${!visible ? 'opacity-0 scale-0 pointer-events-none' : ''}
${visible && isDimmed ? 'opacity-30' : ''}
`}
style={{ top: spot.top, left: spot.left }}
onClick={() => setActiveSpot(activeSpot?.id === spot.id ? null : spot)}
onMouseEnter={() => setHoveredSpot(spot.id)}
onMouseLeave={() => setHoveredSpot(null)}
>
{/* Larger invisible touch target */}
<div className="absolute -inset-4 md:-inset-2" />
{/* Radar Ripple */}
{visible && (
<div className={`absolute ${isCommercial ? '-inset-5' : '-inset-4'} rounded-full animate-ping opacity-75 ${isCommercial ? 'bg-purple-500/20' : 'bg-cyan-400/20'
}`} />
)}
{/* Selected glow ring */}
{activeSpot?.id === spot.id && (
<div className={`absolute -inset-6 rounded-full blur-md animate-pulse ${isCommercial ? 'bg-purple-500/25' : 'bg-cyan-400/25'
}`} />
)}
{/* Pin cluster */}
<div className="relative flex flex-col items-center">
{/* Pin Head */}
<div className={`${isCommercial ? 'w-4 h-4' : 'w-3 h-3'} rounded-full border-2 border-white shadow-lg relative z-10 transition-transform duration-200 group-hover/pin:scale-125 ${isCommercial
? 'bg-purple-500 shadow-[0_0_15px_rgba(168,85,247,0.6)]'
: 'bg-cyan-400 shadow-[0_0_15px_rgba(0,229,255,0.6)]'
}`} />
{/* Pin Line */}
<div className="w-[1px] h-6 md:h-8 bg-gradient-to-b from-white to-transparent opacity-50" />
{/* Ground Shadow */}
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-5 h-1 bg-black/60 blur-sm rounded-[100%] translate-y-1" />
</div>
{/* Pin Head */}
<div className={`w-3 h-3 rounded-full border-2 border-white shadow-lg relative z-10 ${spot.type === 'Commercial' ? 'bg-purple-500 shadow-[0_0_15px_rgba(168,85,247,0.6)]' : 'bg-cyan-400 shadow-[0_0_15px_rgba(0,229,255,0.6)]'}`}></div>
{/* Pin Line */}
<div className="w-[1px] h-8 bg-gradient-to-b from-white to-transparent opacity-50"></div>
{/* Ground Shadow */}
<div className="absolute bottom-0 w-6 h-1.5 bg-black/60 blur-sm rounded-[100%] translate-y-1"></div>
{/* Hover Tooltip */}
<div className={`absolute bottom-full left-1/2 -translate-x-1/2 mb-3 bg-slate-900/90 backdrop-blur-md text-[10px] font-mono px-3 py-2 rounded-lg border border-white/10 shadow-lg whitespace-nowrap pointer-events-none transition-all duration-200 z-30
${hoveredSpot === spot.id && activeSpot?.id !== spot.id ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-1'}
`}>
<div className="font-bold text-white text-[11px]">{spot.address}</div>
<div className={`font-bold mt-0.5 ${isCommercial ? 'text-purple-400' : 'text-cyan-400'}`}>
{spot.gain}
</div>
<div className="absolute top-full left-1/2 -translate-x-1/2 w-2 h-2 bg-slate-900/90 border-r border-b border-white/10 rotate-45 -mt-1" />
</div>
</div>
</div>
))}
);
})}
{/* Reschedule Dialog Overlay */}
{/* Reschedule Dialog */}
{showRescheduleDialog && (
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 md:left-8 md:translate-x-0 z-50 w-[90%] md:w-96">
<div className="bg-white/95 backdrop-blur-xl rounded-xl shadow-2xl p-6 border-l-4 border-red-600 animate-[slideUp_0.5s_ease-out]">
<div className="absolute bottom-6 left-1/2 transform -translate-x-1/2 md:left-6 md:translate-x-0 z-50 w-[90%] md:w-96">
<div className="bg-white/95 backdrop-blur-xl rounded-xl shadow-2xl p-5 md:p-6 border-l-4 border-red-600 animate-[slideUp_0.5s_ease-out]">
<div className="flex justify-between items-start mb-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center text-red-600 font-bold text-lg">!</div>
<div>
<h4 className="font-bold text-slate-900 text-sm uppercase tracking-wide">Schedule Conflict</h4>
</div>
<h4 className="font-bold text-slate-900 text-sm uppercase tracking-wide">Schedule Conflict</h4>
</div>
<button onClick={() => setShowRescheduleDialog(false)} className="text-gray-400 hover:text-black"></button>
<button onClick={() => setShowRescheduleDialog(false)} className="text-gray-400 hover:text-black">{'\u2715'}</button>
</div>
<p className="text-gray-600 text-sm mb-4 leading-relaxed">
<strong>Heavy rainfall expected in Plano at 2:00 PM.</strong> <br />
<strong>Heavy rainfall expected in Plano at 2:00 PM.</strong><br />
This conflicts with the inspection at <span className="text-slate-900 font-semibold">Legacy West</span>.
</p>
<div className="flex gap-3">
<button onClick={() => navigate('/login')} className="flex-1 bg-slate-900 text-white text-xs font-bold py-2.5 rounded hover:bg-slate-800 transition-colors uppercase tracking-wider">
Reschedule
@@ -204,29 +460,81 @@ const IntelligenceMap = () => {
</div>
)}
{/* Right Sidebar Panel (Glass Detail View) */}
{/* Desktop Side Panel */}
{activeSpot && (
<IntelligenceSidePanel
data={activeSpot}
onClose={() => setActiveSpot(null)}
/>
<div className="hidden md:block">
<IntelligenceSidePanel
data={activeSpot}
onClose={() => setActiveSpot(null)}
/>
</div>
)}
</div>
{/* ── Mobile Bottom Sheet ── */}
{activeSpot && ReactDOM.createPortal(
<div className="md:hidden fixed inset-0 z-[9999]">
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={() => setActiveSpot(null)} />
<div data-lenis-prevent className="absolute bottom-0 left-0 right-0 max-h-[80vh] bg-white dark:bg-zinc-900 rounded-t-2xl border-t border-zinc-200 dark:border-white/10 overflow-y-auto overscroll-contain animate-[slideUpSheet_0.3s_ease-out] shadow-2xl touch-pan-y">
{/* Sheet header */}
<div className="sticky top-0 bg-white dark:bg-zinc-900 px-6 pt-4 pb-2 border-b border-zinc-100 dark:border-white/5 z-10">
<div className="w-10 h-1 bg-zinc-300 dark:bg-white/20 rounded-full mx-auto mb-3" />
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${activeSpot.type === 'Commercial' ? 'bg-purple-500' : 'bg-cyan-400'}`} />
<h3 className="font-bold text-zinc-900 dark:text-white text-sm">{activeSpot.address}</h3>
</div>
<button onClick={() => setActiveSpot(null)} className="text-zinc-400 hover:text-zinc-900 dark:hover:text-white p-1">{'\u2715'}</button>
</div>
</div>
<IntelligenceSidePanel
data={activeSpot}
onClose={() => setActiveSpot(null)}
variant="sheet"
/>
</div>
</div>,
document.body
)}
{/* ── Aggregate Metrics ── */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-8">
{metrics.map((m, i) => (
<div key={i} className="bg-white dark:bg-white/5 p-5 rounded-xl border border-zinc-200 dark:border-white/10 relative overflow-hidden hover:-translate-y-1 transition-transform duration-200">
<div className="measure-ticks measure-ticks-top absolute top-0 left-3 right-3 h-2.5"></div>
<div className="text-[10px] font-mono font-bold uppercase tracking-widest text-zinc-400 dark:text-gray-500 mb-1">
{m.label}
</div>
<div
ref={(el) => { metricRefs.current[i] = el; }}
data-target={m.value}
data-suffix={m.suffix}
data-prefix={m.prefix}
className={`text-3xl font-mono font-bold ${m.color} tracking-tight`}
>
{m.prefix}0{m.suffix}
</div>
<div className="mt-3 pt-2 border-t border-zinc-100 dark:border-white/5">
<span className={`permit-stamp ${m.stampClass}`}>{m.stamp}</span>
</div>
<div className="measure-ticks absolute bottom-0 left-3 right-3 h-2.5"></div>
</div>
))}
</div>
</div>
<style>{`
@keyframes slideUp {
from { opacity: 0; transform: translate(-50%, 20px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
@media (min-width: 768px) {
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
}
`}</style>
@keyframes slideUp {
from { opacity: 0; transform: translate(-50%, 20px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
@media (min-width: 768px) {
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
}
`}</style>
</section>
);
};
+74 -33
View File
@@ -4,26 +4,34 @@ import { Icons } from './landing/constants';
const services = [
{
title: "Commercial and Residential Repair and Diagnostics",
desc: "Leak detection & shingle replacement in record time. We identify micro-fractures invisible to the naked eye.",
title: "Commercial & Residential Repair and Diagnostics",
desc: "Leak detection & shingle replacement in record time. We identify micro-fractures invisible to the naked eye using drone-captured LiDAR scans and AI-powered thermal analysis.",
icon: Icons.Home,
color: "from-blue-400 to-blue-600",
glow: "shadow-blue-500/20"
glow: "shadow-blue-500/20",
badge: "Licensed",
badgeClass: "permit-stamp--verified",
badgeIcon: "\u2713",
stat: "2,847",
statLabel: "Properties Inspected",
},
// Commercial Development Removed as per request
{
title: "Lead & Data Solutions",
desc: "Advanced lead scoring and real time storm tracking alerts. Know where the damage is before the phone rings. Powered by intelligent data models and predictive algorithms, we direct your reps and marketing into high-impact zones; where probability, urgency and opportunity align.",
desc: "Advanced lead scoring and real-time storm tracking alerts. Know where the damage is before the phone rings. We direct your reps and marketing into high-impact zones where probability, urgency and opportunity align.",
icon: Icons.Chart,
color: "from-purple-400 to-indigo-600",
glow: "shadow-purple-500/20"
glow: "shadow-purple-500/20",
badge: "Certified",
badgeClass: "permit-stamp--accent",
badgeIcon: "\u25B9",
stat: "96%",
statLabel: "Lead Accuracy",
}
];
const Services = () => {
const navigate = useNavigate();
// Simple Tilt Logic
const handleMouseMove = (e) => {
const card = e.currentTarget;
const rect = card.getBoundingClientRect();
@@ -31,11 +39,8 @@ const Services = () => {
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
// Rotation calc (Max 5deg)
const rotateX = ((y - centerY) / centerY) * -5;
const rotateY = ((x - centerX) / centerX) * 5;
const rotateX = ((y - centerY) / centerY) * -4;
const rotateY = ((x - centerX) / centerX) * 4;
card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(1.02)`;
};
@@ -44,52 +49,88 @@ const Services = () => {
};
return (
<section className="py-24 bg-white relative overflow-hidden dark:bg-black/40">
<div className="absolute inset-0 opacity-5 bg-grid-pattern bg-[length:40px_40px]"></div>
<section className="py-24 bg-zinc-100 dark:bg-zinc-900 relative overflow-hidden">
<div className="absolute inset-0 blueprint-grid-fine opacity-25"></div>
<div className="container mx-auto px-4 relative z-10">
<div className="max-w-5xl mx-auto px-6 relative z-10">
<div className="text-center mb-16">
<h2 className="text-slate-900 dark:text-white font-heading text-4xl font-bold">Our Core Services</h2>
<p className="text-gray-500 mt-4 max-w-2xl mx-auto font-light">Leveraging military-grade technology for civilian roofing applications.</p>
<span className="permit-stamp permit-stamp--accent mb-6 inline-flex">&#9655; Core Operations</span>
<h2 className="text-3xl md:text-5xl font-black text-zinc-900 dark:text-white mb-4 mt-4 tracking-tight">What We Do Best</h2>
<p className="text-zinc-500 dark:text-zinc-400 max-w-2xl mx-auto">Military-grade technology applied to civilian roofing. Two pillars, one platform.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
<div className="grid md:grid-cols-2 gap-8">
{services.map((service, idx) => (
<div
key={idx}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className="group p-8 rounded-2xl border border-gray-100 dark:border-white/10 bg-white/80 dark:bg-white/5 backdrop-blur-sm shadow-xl transition-all duration-200 ease-out cursor-pointer relative overflow-hidden"
className="group relative p-8 rounded-2xl border border-zinc-200 dark:border-white/10 bg-white dark:bg-white/[0.04] hover:bg-zinc-50 dark:hover:bg-white/[0.07] backdrop-blur-sm transition-all duration-200 cursor-pointer overflow-hidden shadow-xl dark:shadow-[0_20px_60px_rgba(0,0,0,0.4)]"
style={{ transformStyle: 'preserve-3d', transition: 'transform 0.1s ease-out' }}
>
{/* Gradient Glow on Hover */}
<div className={`absolute -inset-0.5 bg-gradient-to-br ${service.color} opacity-0 group-hover:opacity-20 blur-xl transition-opacity duration-300 pointer-events-none`}></div>
{/* Measurement ticks — top */}
<div className="measure-ticks measure-ticks-top absolute top-0 left-4 right-4 h-3"></div>
{/* Gradient glow on hover */}
<div className={`absolute -inset-0.5 bg-gradient-to-br ${service.color} opacity-0 group-hover:opacity-15 blur-xl transition-opacity duration-300 pointer-events-none`}></div>
<div className="relative z-10 flex flex-col h-full">
<div className={`w-16 h-16 mb-6 rounded-xl flex items-center justify-center text-white bg-gradient-to-br ${service.color} shadow-lg ${service.glow} transform group-hover:scale-110 transition-transform duration-300`}>
<div className="w-8 h-8 flex items-center justify-center">
<service.icon />
{/* Top row: Icon + Service number */}
<div className="flex items-start justify-between mb-6 mt-1">
<div className={`w-14 h-14 rounded-xl flex items-center justify-center text-white bg-gradient-to-br ${service.color} shadow-lg ${service.glow} group-hover:scale-110 transition-transform duration-300`}>
<div className="w-7 h-7 flex items-center justify-center">
<service.icon />
</div>
</div>
<span className="font-mono text-[11px] font-bold text-zinc-300 dark:text-white/15 tracking-wider">0{idx + 1}</span>
</div>
<h3 className="text-2xl font-heading font-bold text-slate-900 dark:text-white mb-3 group-hover:text-transparent group-hover:bg-clip-text group-hover:bg-gradient-to-r group-hover:from-blue-600 group-hover:to-cyan-500 dark:group-hover:from-blue-400 dark:group-hover:to-cyan-400 transition-all duration-300">
<h3 className="text-xl font-bold text-zinc-900 dark:text-white mb-3 leading-snug">
{service.title}
</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm leading-relaxed mb-8 flex-grow">
<p className="text-zinc-500 dark:text-zinc-400 text-sm leading-relaxed mb-6 flex-grow">
{service.desc}
</p>
<div
onClick={(e) => { e.stopPropagation(); navigate('/login'); }}
className="pt-6 border-t border-gray-100 dark:border-white/10 flex items-center justify-between mt-auto group-hover:border-cyan/20 transition-colors cursor-pointer"
>
<span className="text-xs font-bold text-gray-400 uppercase tracking-wider group-hover:text-cyan transition-colors">Explore Solution</span>
<div className="w-8 h-8 rounded-full border border-gray-200 dark:border-white/20 text-gray-400 flex items-center justify-center transform group-hover:rotate-45 group-hover:border-cyan group-hover:text-cyan transition-all duration-300">
{/* Inline stat */}
<div className="flex items-baseline gap-2 mb-6">
<span className="font-mono text-2xl font-bold text-zinc-900 dark:text-white tracking-tight">{service.stat}</span>
<span className="text-xs text-zinc-500 font-medium uppercase tracking-wider">{service.statLabel}</span>
</div>
{/* Footer row: Permit stamp + CTA */}
<div className="pt-4 border-t border-zinc-100 dark:border-white/5 flex items-center justify-between mt-auto">
<span className={`permit-stamp ${service.badgeClass}`}>{service.badgeIcon} {service.badge}</span>
<div
onClick={(e) => { e.stopPropagation(); navigate('/login'); }}
className="flex items-center gap-2 text-xs font-bold text-zinc-500 uppercase tracking-wider group-hover:text-blue-400 transition-colors"
>
Explore
<div className="w-6 h-6 rounded-full border border-zinc-300 dark:border-white/10 flex items-center justify-center group-hover:border-blue-400/30 group-hover:rotate-45 transition-all duration-300 text-[10px]">
</div>
</div>
</div>
</div>
{/* Measurement ticks — bottom */}
<div className="measure-ticks absolute bottom-0 left-4 right-4 h-3"></div>
</div>
))}
</div>
{/* Trust signals strip */}
<div className="mt-12 flex flex-wrap items-center justify-center gap-x-8 gap-y-3 text-zinc-500">
{[
{ label: "AI-Powered Analysis", icon: "◉" },
{ label: "15-Min Turnaround", icon: "◉" },
{ label: "Drone-Powered Inspections", icon: "◉" },
{ label: "Insurance-Ready Reports", icon: "◉" },
].map((item, i) => (
<div key={i} className="flex items-center gap-2 text-xs font-mono uppercase tracking-wider">
<span className="text-blue-500/60 text-[6px]">{item.icon}</span>
<span>{item.label}</span>
</div>
))}
</div>
+1 -1
View File
@@ -44,7 +44,7 @@ export const SpotlightCard = ({ children, className = "", spotlightColor = "rgba
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
// Light: bg-white, Dark: bg-zinc-800
className={`group relative ${rounded} bg-white dark:bg-zinc-800 overflow-hidden shadow-sm dark:shadow-none ${className}`}
className={`group relative ${rounded} bg-white dark:bg-zinc-800 overflow-hidden shadow-xl dark:shadow-[0_20px_60px_rgba(0,0,0,0.4)] ${className}`}
>
{/*
Rainbow Glow Layer
@@ -4,7 +4,8 @@ import { useNavigate } from 'react-router-dom';
import { Icons } from '../landing/constants';
import { ChevronLeft, ChevronRight, X } from 'lucide-react';
const IntelligenceSidePanel = ({ data, onClose }) => {
const IntelligenceSidePanel = ({ data, onClose, variant = 'default' }) => {
const isSheet = variant === 'sheet';
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState('details');
// Initialize photos from data or use empty array if not present
@@ -65,31 +66,41 @@ const IntelligenceSidePanel = ({ data, onClose }) => {
return (
<div
ref={panelRef}
className="absolute top-4 right-4 bottom-4 w-80 bg-slate-900/80 backdrop-blur-xl rounded-xl p-6 shadow-2xl border border-white/10 transform transition-transform duration-300 z-40 flex flex-col text-white"
className={isSheet
? 'px-6 pb-6 pt-2 flex flex-col text-zinc-900 dark:text-white'
: 'absolute top-4 right-4 bottom-4 w-80 bg-slate-900/80 backdrop-blur-xl rounded-xl p-6 shadow-2xl border border-white/10 transform transition-transform duration-300 z-40 flex flex-col text-white'
}
role="dialog"
aria-modal="true"
aria-label="Property Intelligence Details"
>
{/* Header */}
<div className="flex justify-between items-center mb-4 pb-4 border-b border-white/10">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-cyan-400 rounded-full animate-pulse"></div>
<h3 className="font-heading font-bold text-lg tracking-wide">INTEL_FEED</h3>
{/* Header — only on desktop panel, mobile sheet provides its own */}
{!isSheet && (
<div className="flex justify-between items-center mb-4 pb-4 border-b border-white/10">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-cyan-400 rounded-full animate-pulse"></div>
<h3 className="font-heading font-bold text-lg tracking-wide">INTEL_FEED</h3>
</div>
<button
ref={closeButtonRef}
onClick={onClose}
className="text-gray-400 hover:text-white transition-colors focus:outline-none focus:ring-2 focus:ring-cyan-400 rounded-full p-1"
aria-label="Close details panel"
>
</button>
</div>
<button
ref={closeButtonRef}
onClick={onClose}
className="text-gray-400 hover:text-white transition-colors focus:outline-none focus:ring-2 focus:ring-cyan-400 rounded-full p-1"
aria-label="Close details panel"
>
</button>
</div>
)}
{/* Tabs */}
<div className="flex border-b border-white/10 mb-4" role="tablist">
<div className={`flex mb-4 border-b ${isSheet ? 'border-zinc-200 dark:border-white/10' : 'border-white/10'}`} role="tablist">
<button
className={`flex-1 py-2 text-sm font-bold uppercase tracking-wider transition-colors border-b-2 ${activeTab === 'details' ? 'border-cyan-400 text-cyan-400' : 'border-transparent text-gray-400 hover:text-white'}`}
className={`flex-1 py-2 text-sm font-bold uppercase tracking-wider transition-colors border-b-2 ${activeTab === 'details'
? 'border-cyan-400 text-cyan-400'
: isSheet
? 'border-transparent text-zinc-400 dark:text-gray-400 hover:text-zinc-900 dark:hover:text-white'
: 'border-transparent text-gray-400 hover:text-white'
}`}
onClick={() => setActiveTab('details')}
role="tab"
aria-selected={activeTab === 'details'}
@@ -99,7 +110,12 @@ const IntelligenceSidePanel = ({ data, onClose }) => {
Details
</button>
<button
className={`flex-1 py-2 text-sm font-bold uppercase tracking-wider transition-colors border-b-2 ${activeTab === 'photos' ? 'border-cyan-400 text-cyan-400' : 'border-transparent text-gray-400 hover:text-white'}`}
className={`flex-1 py-2 text-sm font-bold uppercase tracking-wider transition-colors border-b-2 ${activeTab === 'photos'
? 'border-cyan-400 text-cyan-400'
: isSheet
? 'border-transparent text-zinc-400 dark:text-gray-400 hover:text-zinc-900 dark:hover:text-white'
: 'border-transparent text-gray-400 hover:text-white'
}`}
onClick={() => setActiveTab('photos')}
role="tab"
aria-selected={activeTab === 'photos'}
@@ -110,31 +126,64 @@ const IntelligenceSidePanel = ({ data, onClose }) => {
</button>
</div>
{/* Content Area */}
<div className="flex-grow overflow-y-auto custom-scrollbar relative">
{/* Status / Weather Quick Bar */}
<div className={`flex items-center gap-2 mb-4 flex-wrap ${isSheet ? '' : ''}`}>
{data.status && (
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider
${data.status === 'Urgent'
? 'bg-red-500/15 text-red-400 border border-red-500/25'
: data.status === 'Action Required'
? 'bg-amber-500/15 text-amber-400 border border-amber-500/25'
: data.status === 'Monitoring'
? 'bg-blue-500/15 text-blue-400 border border-blue-500/25'
: 'bg-emerald-500/15 text-emerald-400 border border-emerald-500/25'
}
`}>
<span className={`w-1.5 h-1.5 rounded-full ${
data.status === 'Urgent' ? 'bg-red-400 animate-pulse' :
data.status === 'Action Required' ? 'bg-amber-400' :
data.status === 'Monitoring' ? 'bg-blue-400' : 'bg-emerald-400'
}`} />
{data.status}
</span>
)}
{data.weather && data.weather !== 'Clear' && (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider bg-yellow-500/10 text-yellow-400 border border-yellow-500/20">
{'\u26C8'} {data.weather}
</span>
)}
{data.weather === 'Clear' && (
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider ${isSheet ? 'bg-zinc-100 dark:bg-white/5 text-zinc-400 dark:text-gray-500 border border-zinc-200 dark:border-white/10' : 'bg-white/5 text-gray-500 border border-white/10'}`}>
{'\u2600'} Clear
</span>
)}
</div>
{/* Content Area — data-lenis-prevent stops Lenis from hijacking wheel/touch scroll */}
<div data-lenis-prevent className="flex-grow overflow-y-auto overscroll-contain relative">
{/* DETAILS TAB */}
{activeTab === 'details' && (
<div role="tabpanel" id="panel-details" aria-labelledby="tab-details" className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-300">
<div className="p-4 bg-cyan-400/5 rounded-lg border border-cyan-400/20 relative overflow-hidden group/card hover:bg-cyan-400/10 transition-colors">
<div className={`p-4 rounded-lg border relative overflow-hidden group/card transition-colors ${isSheet ? 'bg-cyan-500/5 dark:bg-cyan-400/5 border-cyan-500/20 dark:border-cyan-400/20 hover:bg-cyan-500/10 dark:hover:bg-cyan-400/10' : 'bg-cyan-400/5 border-cyan-400/20 hover:bg-cyan-400/10'}`}>
<div className="absolute top-0 right-0 p-1 opacity-20 group-hover/card:opacity-40 transition-opacity">
<Icons.Chart />
</div>
<label className="text-[10px] text-cyan-400 font-bold uppercase tracking-widest mb-1 block">Projected Value</label>
<div className="text-3xl font-heading font-bold text-white">{data.gain}</div>
<label className={`text-[10px] font-bold uppercase tracking-widest mb-1 block ${isSheet ? 'text-cyan-600 dark:text-cyan-400' : 'text-cyan-400'}`}>Projected Value</label>
<div className={`text-3xl font-heading font-bold ${isSheet ? 'text-zinc-900 dark:text-white' : 'text-white'}`}>{data.gain}</div>
</div>
<div>
<label className="text-[10px] text-gray-500 font-bold uppercase tracking-widest mb-1 block">Location Target</label>
<div className="text-white font-bold text-lg leading-tight mb-1">{data.address}</div>
<div className="text-xs text-gray-400">Dallas-Fort Worth Metroplex {data.type} Zone</div>
<label className={`text-[10px] font-bold uppercase tracking-widest mb-1 block ${isSheet ? 'text-zinc-400 dark:text-gray-500' : 'text-gray-500'}`}>Location Target</label>
<div className={`font-bold text-lg leading-tight mb-1 ${isSheet ? 'text-zinc-900 dark:text-white' : 'text-white'}`}>{data.address}</div>
<div className={`text-xs ${isSheet ? 'text-zinc-500 dark:text-gray-400' : 'text-gray-400'}`}>Dallas-Fort Worth Metroplex {data.type} Zone</div>
</div>
{/* Divider */}
<div className="h-px bg-white/10 w-full"></div>
<div className={`h-px w-full ${isSheet ? 'bg-zinc-200 dark:bg-white/10' : 'bg-white/10'}`}></div>
<div>
<label className="text-[10px] text-gray-500 font-bold uppercase tracking-widest mb-2 block">Detected Issue</label>
<label className={`text-[10px] font-bold uppercase tracking-widest mb-2 block ${isSheet ? 'text-zinc-400 dark:text-gray-500' : 'text-gray-500'}`}>Detected Issue</label>
<div className={`flex items-center gap-2 font-bold ${data.problem.includes('Severe') ? 'text-red-500' : 'text-yellow-400'}`}>
<span className="animate-pulse"></span> {data.problem}
</div>
@@ -145,17 +194,17 @@ const IntelligenceSidePanel = ({ data, onClose }) => {
<div className="space-y-4 animate-in slide-in-from-right-4 duration-300">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-[10px] text-gray-500 font-bold uppercase tracking-widest mb-1 block">Built</label>
<div className="text-sm text-white font-medium">{data.specs.built}</div>
<label className={`text-[10px] font-bold uppercase tracking-widest mb-1 block ${isSheet ? 'text-zinc-400 dark:text-gray-500' : 'text-gray-500'}`}>Built</label>
<div className={`text-sm font-medium ${isSheet ? 'text-zinc-900 dark:text-white' : 'text-white'}`}>{data.specs.built}</div>
</div>
<div>
<label className="text-[10px] text-gray-500 font-bold uppercase tracking-widest mb-1 block">Maintenance</label>
<div className="text-sm text-white font-medium">{data.specs.maintenance}</div>
<label className={`text-[10px] font-bold uppercase tracking-widest mb-1 block ${isSheet ? 'text-zinc-400 dark:text-gray-500' : 'text-gray-500'}`}>Maintenance</label>
<div className={`text-sm font-medium ${isSheet ? 'text-zinc-900 dark:text-white' : 'text-white'}`}>{data.specs.maintenance}</div>
</div>
</div>
<div className="p-3 bg-slate-800/50 rounded-lg border border-white/5">
<div className={`p-3 rounded-lg border ${isSheet ? 'bg-zinc-100 dark:bg-slate-800/50 border-zinc-200 dark:border-white/5' : 'bg-slate-800/50 border-white/5'}`}>
<label className="text-[10px] text-purple-400 font-bold uppercase tracking-widest mb-1 block">Management</label>
<div className="text-sm text-white font-bold">{data.management.name}</div>
<div className={`text-sm font-bold ${isSheet ? 'text-zinc-900 dark:text-white' : 'text-white'}`}>{data.management.name}</div>
<div
onClick={() => navigate('/login')}
className="text-xs text-blue-400 cursor-pointer hover:underline mt-1 focus:outline-none focus:ring-1 focus:ring-blue-400 rounded"
@@ -171,30 +220,30 @@ const IntelligenceSidePanel = ({ data, onClose }) => {
<div className="space-y-4 animate-in slide-in-from-right-4 duration-300">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-[10px] text-gray-500 font-bold uppercase tracking-widest mb-1 block">Roof Specs</label>
<div className="text-sm text-white font-medium">{data.specs.roofSize}</div>
<label className={`text-[10px] font-bold uppercase tracking-widest mb-1 block ${isSheet ? 'text-zinc-400 dark:text-gray-500' : 'text-gray-500'}`}>Roof Specs</label>
<div className={`text-sm font-medium ${isSheet ? 'text-zinc-900 dark:text-white' : 'text-white'}`}>{data.specs.roofSize}</div>
</div>
<div>
<label className="text-[10px] text-gray-500 font-bold uppercase tracking-widest mb-1 block">Risk History</label>
<div className="text-sm text-white font-medium">{data.specs.eventHistory}</div>
<label className={`text-[10px] font-bold uppercase tracking-widest mb-1 block ${isSheet ? 'text-zinc-400 dark:text-gray-500' : 'text-gray-500'}`}>Risk History</label>
<div className={`text-sm font-medium ${isSheet ? 'text-zinc-900 dark:text-white' : 'text-white'}`}>{data.specs.eventHistory}</div>
</div>
</div>
<div className="p-3 bg-slate-800/50 rounded-lg border border-white/5">
<div className={`p-3 rounded-lg border ${isSheet ? 'bg-zinc-100 dark:bg-slate-800/50 border-zinc-200 dark:border-white/5' : 'bg-slate-800/50 border-white/5'}`}>
<label className="text-[10px] text-emerald-400 font-bold uppercase tracking-widest mb-1 block">Owner Intelligence</label>
<div className="flex justify-between items-end">
<div>
<div className="text-xs text-gray-400 mb-0.5">FICO Score</div>
<div className="text-sm text-white font-bold">{data.owner.fico}</div>
<div className={`text-xs mb-0.5 ${isSheet ? 'text-zinc-500 dark:text-gray-400' : 'text-gray-400'}`}>FICO Score</div>
<div className={`text-sm font-bold ${isSheet ? 'text-zinc-900 dark:text-white' : 'text-white'}`}>{data.owner.fico}</div>
</div>
<div className="text-right">
<div className="text-xs text-gray-400 mb-0.5">Profile</div>
<div className="text-xs text-white font-medium">{data.owner.profile}</div>
<div className={`text-xs mb-0.5 ${isSheet ? 'text-zinc-500 dark:text-gray-400' : 'text-gray-400'}`}>Profile</div>
<div className={`text-xs font-medium ${isSheet ? 'text-zinc-900 dark:text-white' : 'text-white'}`}>{data.owner.profile}</div>
</div>
</div>
</div>
</div>
) : (
<div className="text-xs text-gray-400 italic">
<div className={`text-xs italic ${isSheet ? 'text-zinc-500 dark:text-gray-400' : 'text-gray-400'}`}>
&gt; ANALYSIS: {data.type === 'Commercial' ? 'High-value commercial asset at risk. Immediate inspection recommended.' : 'High probability of insurance approval due to recent storm activity.'}
</div>
)}
@@ -214,11 +263,11 @@ const IntelligenceSidePanel = ({ data, onClose }) => {
<div role="tabpanel" id="panel-photos" aria-labelledby="tab-photos" className="space-y-4 animate-in fade-in slide-in-from-right-4 duration-300 h-full flex flex-col">
{/* Photo Grid */}
<div className="grid grid-cols-2 gap-3 pb-20"> {/* pb-20 for bottom fixed button space if needed, though button is inline here */}
<div className="grid grid-cols-2 gap-3 pb-4">
{photos.map((photo, index) => (
<div
key={photo.id}
className="relative aspect-square rounded-lg overflow-hidden group border border-white/10 hover:border-cyan-400/50 transition-colors cursor-pointer"
className={`relative aspect-square rounded-lg overflow-hidden group border transition-colors cursor-pointer ${isSheet ? 'border-zinc-200 dark:border-white/10 hover:border-cyan-500/50 dark:hover:border-cyan-400/50' : 'border-white/10 hover:border-cyan-400/50'}`}
onClick={() => setLightboxIndex(index)}
>
<img
@@ -226,18 +275,19 @@ const IntelligenceSidePanel = ({ data, onClose }) => {
alt={photo.caption || 'Property view'}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<span className="text-cyan-400 text-xs font-bold uppercase tracking-widest border border-cyan-400/50 px-2 py-1 rounded-full backdrop-blur-sm shadow-[0_0_10px_rgba(34,211,238,0.3)]">View</span>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex flex-col items-center justify-end p-2">
<span className="text-cyan-400 text-xs font-bold uppercase tracking-widest border border-cyan-400/50 px-2 py-1 rounded-full backdrop-blur-sm shadow-[0_0_10px_rgba(34,211,238,0.3)] mb-1.5">View</span>
{photo.caption && (
<span className="text-white/80 text-[9px] text-center leading-tight font-mono">{photo.caption}</span>
)}
</div>
</div>
))}
{/* Upload Placeholders if empty */}
{photos.length === 0 && (
<div className="col-span-2 py-8 text-center border-2 border-dashed border-white/10 rounded-lg bg-white/5">
<p className="text-gray-400 text-xs mb-2">No photos available</p>
<div className={`col-span-2 py-8 text-center border-2 border-dashed rounded-lg ${isSheet ? 'border-zinc-200 dark:border-white/10 bg-zinc-50 dark:bg-white/5' : 'border-white/10 bg-white/5'}`}>
<p className={`text-xs mb-2 ${isSheet ? 'text-zinc-400 dark:text-gray-400' : 'text-gray-400'}`}>No photos available</p>
</div>
)}
</div>
+114
View File
@@ -0,0 +1,114 @@
import React, { useRef, useMemo } from 'react';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import * as THREE from 'three';
/**
* ParticleField Internal R3F component
* Renders ~800 small white dots with gentle organic drift and mouse parallax.
* Uses Points geometry with BufferAttribute for maximum performance.
* Aesthetic: "aerospace night sky" calm, precise, not chaotic.
*/
function ParticleField({ count = 800 }) {
const points = useRef();
const { pointer } = useThree();
// Generate initial positions + per-particle phase seeds
const { geometry, basePositions, seeds } = useMemo(() => {
const positions = new Float32Array(count * 3);
const seedArr = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const i3 = i * 3;
// Wide rectangular spread matches hero viewport proportions
positions[i3] = (Math.random() - 0.5) * 26; // x: wide
positions[i3 + 1] = (Math.random() - 0.5) * 16; // y: tall
positions[i3 + 2] = (Math.random() - 0.5) * 10; // z: depth layers
// Random phase offsets for organic non-uniform motion
seedArr[i3] = Math.random() * Math.PI * 2;
seedArr[i3 + 1] = Math.random() * Math.PI * 2;
seedArr[i3 + 2] = Math.random() * Math.PI * 2;
}
const geo = new THREE.BufferGeometry();
const posAttr = new THREE.BufferAttribute(positions, 3);
posAttr.setUsage(THREE.DynamicDrawUsage); // Optimized for frequent updates
geo.setAttribute('position', posAttr);
return {
geometry: geo,
basePositions: new Float32Array(positions), // Immutable copy
seeds: seedArr,
};
}, [count]);
useFrame((state) => {
if (!points.current) return;
const time = state.clock.elapsedTime * 0.25; // Very slow time factor
const posArray = points.current.geometry.attributes.position.array;
// Per-particle gentle drift
for (let i = 0; i < count; i++) {
const i3 = i * 3;
posArray[i3] = basePositions[i3] + Math.sin(time + seeds[i3]) * 0.12;
posArray[i3 + 1] = basePositions[i3 + 1] + Math.cos(time * 0.7 + seeds[i3 + 1]) * 0.10;
posArray[i3 + 2] = basePositions[i3 + 2] + Math.sin(time * 0.4 + seeds[i3 + 2]) * 0.06;
}
points.current.geometry.attributes.position.needsUpdate = true;
// Mouse parallax gentle rotation of the entire field
points.current.rotation.y = THREE.MathUtils.lerp(
points.current.rotation.y,
pointer.x * 0.06,
0.012
);
points.current.rotation.x = THREE.MathUtils.lerp(
points.current.rotation.x,
-pointer.y * 0.04,
0.012
);
});
return (
<points ref={points} geometry={geometry}>
<pointsMaterial
size={0.025}
color="#ffffff"
transparent
opacity={0.3}
sizeAttenuation
depthWrite={false}
blending={THREE.AdditiveBlending}
/>
</points>
);
}
/**
* HeroParticles Exported wrapper
* Transparent R3F Canvas overlaying the hero section.
* pointer-events: none so clicks pass through to CTAs below.
*/
export default function HeroParticles() {
return (
<Canvas
camera={{ position: [0, 0, 12], fov: 55 }}
gl={{
alpha: true,
antialias: false,
powerPreference: 'default',
stencil: false,
depth: false,
}}
dpr={[1, 1.5]}
style={{
position: 'absolute',
inset: 0,
pointerEvents: 'none',
zIndex: 10,
}}
>
<ParticleField />
</Canvas>
);
}
+202 -57
View File
@@ -1,7 +1,55 @@
/* === Font Imports === */
/* Instrument Serif: "The Authority" — headlines, section titles, pull-quotes */
@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=swap');
/* JetBrains Mono: "The Instrument" — data readouts, stat counters, HUD labels */
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
/* Inter: "The Engineer" — body text, UI, navigation (already system-available, loaded for consistency) */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
/* === Base Layer: Global Resets & Defaults === */
@layer base {
:root {
font-family: 'Inter', system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Crane SVG theme-aware stroke/fill tokens */
--crane-line-strong: rgba(0, 0, 0, 0.22);
--crane-line-medium: rgba(0, 0, 0, 0.14);
--crane-line-subtle: rgba(0, 0, 0, 0.08);
--crane-fill-surface: rgba(0, 0, 0, 0.04);
}
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
width: 100%;
background-color: #ffffff;
color: #1a1a1a;
}
/* Dark mode base for landing page */
.dark {
background-color: #09090b;
color: rgba(255, 255, 255, 0.92);
--crane-line-strong: rgba(255, 255, 255, 0.16);
--crane-line-medium: rgba(255, 255, 255, 0.10);
--crane-line-subtle: rgba(255, 255, 255, 0.06);
--crane-fill-surface: rgba(255, 255, 255, 0.03);
}
}
/* === Utility Animations === */
@layer utilities {
.animate-in {
animation-duration: 0.5s;
@@ -21,13 +69,8 @@
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideInBottom {
@@ -35,52 +78,173 @@
transform: translateY(1rem);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* === Scrollbar: Hidden Globally === */
* {
-ms-overflow-style: none;
scrollbar-width: none;
}
html,
body,
#root {
height: 100%;
margin: 0;
padding: 0;
width: 100%;
background-color: #ffffff;
color: #1a1a1a;
*::-webkit-scrollbar {
display: none;
}
@layer utilities {
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar {
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
}
/* Type 1: Invisible but copyable */
/* Type 1: Invisible but copyable */
/* === Construction Design Elements === */
/* Permit stamp — "VERIFIED" / "INSPECTED" badge treatment */
@layer components {
.permit-stamp {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1.5px solid currentColor;
padding: 3px 10px;
font-family: 'JetBrains Mono', monospace;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
transform: rotate(-2deg);
border-radius: 3px;
opacity: 0.7;
}
.permit-stamp--accent {
color: #3b82f6;
border-color: rgba(59, 130, 246, 0.4);
}
.permit-stamp--verified {
color: #10b981;
border-color: rgba(16, 185, 129, 0.4);
}
.permit-stamp--danger {
color: #ef4444;
border-color: rgba(239, 68, 68, 0.4);
}
}
/* Shingle divider — overlapping parallelogram pattern mimicking roof shingles */
@layer components {
.shingle-divider {
width: 100%;
height: 20px;
background:
linear-gradient(135deg, rgba(0,0,0,0.06) 25%, transparent 25%) -24px 0,
linear-gradient(225deg, rgba(0,0,0,0.06) 25%, transparent 25%) -24px 0,
linear-gradient(315deg, rgba(0,0,0,0.06) 25%, transparent 25%),
linear-gradient(45deg, rgba(0,0,0,0.06) 25%, transparent 25%);
background-size: 48px 20px;
background-color: transparent;
}
.dark .shingle-divider {
background:
linear-gradient(135deg, rgba(255,255,255,0.04) 25%, transparent 25%) -24px 0,
linear-gradient(225deg, rgba(255,255,255,0.04) 25%, transparent 25%) -24px 0,
linear-gradient(315deg, rgba(255,255,255,0.04) 25%, transparent 25%),
linear-gradient(45deg, rgba(255,255,255,0.04) 25%, transparent 25%);
background-size: 48px 20px;
}
}
/* Safety stripe — diagonal hazard pattern for urgent/warning elements */
@layer components {
.safety-stripe {
background: repeating-linear-gradient(
-45deg,
transparent,
transparent 4px,
rgba(245, 158, 11, 0.12) 4px,
rgba(245, 158, 11, 0.12) 8px
);
}
}
/* Measurement tick marks — ruler/tape aesthetic for stat counters */
@layer components {
.measure-ticks {
background-image:
repeating-linear-gradient(90deg, rgba(0,0,0,0.15) 0px, rgba(0,0,0,0.15) 1.5px, transparent 1.5px, transparent 24px),
repeating-linear-gradient(90deg, rgba(0,0,0,0.07) 0px, rgba(0,0,0,0.07) 1px, transparent 1px, transparent 6px);
background-size: 100% 100%, 100% 50%;
background-repeat: no-repeat;
background-position: bottom, bottom;
}
.dark .measure-ticks {
background-image:
repeating-linear-gradient(90deg, rgba(255,255,255,0.22) 0px, rgba(255,255,255,0.22) 1.5px, transparent 1.5px, transparent 24px),
repeating-linear-gradient(90deg, rgba(255,255,255,0.1) 0px, rgba(255,255,255,0.1) 1px, transparent 1px, transparent 6px);
}
.measure-ticks-top {
background-position: top, top;
}
}
/* Blueprint grid — combined background-image + background-size */
@layer components {
.blueprint-grid {
background-image:
linear-gradient(rgba(59,130,246,0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(59,130,246,0.1) 1px, transparent 1px);
background-size: 48px 48px;
}
.dark .blueprint-grid {
background-image:
linear-gradient(rgba(59,130,246,0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(59,130,246,0.05) 1px, transparent 1px);
}
.blueprint-grid-fine {
background-image:
linear-gradient(rgba(59,130,246,0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(59,130,246,0.06) 1px, transparent 1px),
linear-gradient(rgba(59,130,246,0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(59,130,246,0.1) 1px, transparent 1px);
background-size: 12px 12px, 12px 12px, 48px 48px, 48px 48px;
}
.dark .blueprint-grid-fine {
background-image:
linear-gradient(rgba(59,130,246,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(59,130,246,0.03) 1px, transparent 1px),
linear-gradient(rgba(59,130,246,0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(59,130,246,0.06) 1px, transparent 1px);
}
}
/* Mobile bottom sheet slide-up for Intelligence Map */
@keyframes slideUpSheet {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
/* Crane swing — gentle pendulum sway for suspended CTA card */
@keyframes craneSwing {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(1.5deg); }
75% { transform: rotate(-1.5deg); }
}
@layer components {
.crane-swing {
animation: craneSwing 4s ease-in-out infinite;
transform-origin: top center;
}
.crane-swing:hover {
animation-play-state: paused;
}
}
/* === Attribution (existing, preserved) === */
@layer utilities {
.attribution-ghost,
.content-ref-x7,
.sys-ref-x7,
@@ -89,44 +253,25 @@ body,
display: inline-block;
opacity: 0;
font-size: 1px;
/* Must have size to be selected */
color: transparent;
/* Text exists but is invisible */
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
/* Standard sr-only hide */
white-space: nowrap;
user-select: text;
/* Critical */
pointer-events: none;
z-index: -1;
}
/* Type 2: Hidden until selected (General) */
/* Type 2: Hidden until selected (General) */
.attribution-reveal {
color: transparent;
background: transparent;
user-select: text;
}
.attribution-reveal::selection {
background: #3b82f6;
color: white;
}
}
/* Global Hidden Scrollbar (User Requested) */
* {
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
*::-webkit-scrollbar {
display: none;
}
+386 -106
View File
@@ -1,8 +1,8 @@
import React, { useEffect, useRef } from 'react';
import Chatbot from '../components/Chatbot';
import { useAuth } from '../context/AuthContext';
import { useTheme } from '../context/ThemeContext'; // Import useTheme
import { ArrowRight, CheckCircle2, Home, Star, ShieldCheck, Clock, Award, TrendingDown, Users, MapPin, Phone, Mail, Instagram, Twitter, Youtube, Sun, Moon, LogOut, LayoutDashboard, User, Menu, X } from 'lucide-react';
import { useTheme } from '../context/ThemeContext';
import { ArrowRight, CheckCircle2, Home, Star, ShieldCheck, Clock, Award, TrendingDown, Users, MapPin, Phone, Mail, Instagram, Twitter, Youtube, Sun, Moon, LogOut, LayoutDashboard, User, Menu, X, ChevronDown } from 'lucide-react';
import { Link } from 'react-router-dom';
import { SpotlightCard } from '../components/SpotlightCard';
@@ -12,7 +12,7 @@ import IntelligenceMap from '../components/IntelligenceMap';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
import HeroImg from '../assets/images/hero.png';
import HeroImg from '../assets/images/hero_aerial_scan.png';
import ProcessImg1 from '../assets/images/process.png';
import ProcessImg2 from '../assets/images/process-2.png';
import ProcessImg3 from '../assets/images/process-3.png';
@@ -27,6 +27,19 @@ const Landing = () => {
const sectionsRef = useRef([]);
const statRefs = useRef([]);
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
const cursorGlowRef = useRef(null);
// Mouse-follow ambient light tracks cursor across the page
useEffect(() => {
const handleMouseMove = (e) => {
if (cursorGlowRef.current) {
cursorGlowRef.current.style.setProperty('--glow-x', `${e.clientX}px`);
cursorGlowRef.current.style.setProperty('--glow-y', `${e.clientY}px`);
}
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
useEffect(() => {
// Console Signature (Info level to persist in prod)
@@ -56,32 +69,29 @@ const Landing = () => {
);
});
// Animated Stat Counters
// Animated Stat Counters proxy object approach (avoids GSAP innerText parsing conflicts)
statRefs.current.forEach((el) => {
if (!el) return;
const target = parseFloat(el.dataset.target);
const suffix = el.dataset.suffix || '';
const counter = { value: 0 };
gsap.fromTo(el,
{ innerText: 0 },
{
innerText: target,
duration: 2,
ease: "power1.out",
scrollTrigger: {
trigger: el,
start: "top 85%",
toggleActions: "play none none none",
},
snap: { innerText: suffix === '%' ? 1 : 0.1 },
onUpdate: function () {
const value = this.targets()[0].innerText;
const formattedValue = suffix === '%' ? Math.ceil(value) : Math.ceil(value);
el.innerText = formattedValue + suffix;
}
el.innerText = '0' + suffix;
gsap.to(counter, {
value: target,
duration: 2,
ease: "power1.out",
scrollTrigger: {
trigger: el,
start: "top 85%",
toggleActions: "play none none none",
},
onUpdate: () => {
el.innerText = Math.ceil(counter.value) + suffix;
}
);
});
});
}, []);
@@ -95,8 +105,15 @@ const Landing = () => {
return (
<div className="min-h-screen bg-zinc-50 dark:bg-[#050505] text-zinc-900 dark:text-white transition-colors duration-300 overflow-x-hidden selection:bg-emerald-500/30">
{/* DITHER EFFECT - Fixed Overlay */}
<div className="fixed inset-0 pointer-events-none z-50 opacity-[0.03] dark:opacity-[0.05]" style={{ backgroundImage: 'url("data:image/svg+xml,%3Csvg viewBox=%220 0 200 200%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noiseFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%220.65%22 numOctaves=%223%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noiseFilter)%22/%3E%3C/svg%3E")' }}></div>
{/* CURSOR AMBIENT LIGHT — subtle glow that follows mouse, desktop only */}
<div
ref={cursorGlowRef}
className="fixed inset-0 pointer-events-none z-50 hidden md:block"
style={{
background: 'radial-gradient(600px circle at var(--glow-x, 50%) var(--glow-y, 50%), rgba(59,130,246,0.04), transparent 60%)',
transition: 'background 0.15s ease',
}}
/>
{/* Nav */}
<nav className="border-b border-zinc-200 dark:border-white/5 backdrop-blur-xl sticky top-0 z-40 bg-white/70 dark:bg-black/50">
@@ -188,54 +205,124 @@ const Landing = () => {
)}
</nav>
{/* HERO SECTION */}
<div className="relative min-h-[90vh] flex items-center justify-center pt-32 pb-40 overflow-hidden" ref={heroRef}>
{/* Background Image with Overlay */}
{/* ============================================ */}
{/* HERO SECTION — "The Overhead View" */}
{/* Aerospace Precision & Hometown Trust */}
{/* ============================================ */}
<section className="relative min-h-screen flex items-center justify-center overflow-hidden bg-white dark:bg-[#09090b]" ref={heroRef}>
{/* Layer 0: Background image */}
<div className="absolute inset-0 z-0">
<img src={HeroImg} alt="Modern Roof" className="w-full h-full object-cover object-[center_35%] scale-110 opacity-20 dark:opacity-40" />
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-zinc-50/50 to-zinc-50 dark:via-[#050505]/50 dark:to-[#050505]"></div>
<img
src={HeroImg}
alt=""
className="w-full h-full object-cover object-center scale-105 opacity-15 dark:opacity-65 saturate-[0.35] brightness-75 dark:brightness-[0.70]"
/>
<div className="absolute inset-0 bg-gradient-to-b from-white/60 dark:from-[#09090b]/40 via-transparent to-white dark:to-[#09090b]" />
<div className="absolute inset-0 bg-gradient-to-r from-white/70 dark:from-[#09090b]/60 via-transparent to-white/70 dark:to-[#09090b]/60" />
</div>
<div className="max-w-7xl mx-auto px-6 relative z-10 text-center" ref={textRef}>
<div className="inline-flex items-center px-4 py-1.5 rounded-full bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-500/20 text-[10px] font-bold tracking-widest uppercase mb-8 backdrop-blur-sm shadow-sm text-blue-800 dark:text-blue-300">
<span className="mr-2 text-base">🇺🇸</span>
Crafted to Level the Field for Those Who Build America
{/* Layer 1: Subtle ambient glow behind content */}
<div className="absolute inset-0 z-[5]">
<div className="absolute top-1/3 left-1/2 -translate-x-1/2 w-[600px] h-[400px] bg-blue-500/[0.04] rounded-full blur-[120px]" />
</div>
{/* Layer 3: Content */}
<div className="relative z-20 max-w-5xl mx-auto px-6 text-center pt-24 pb-32" ref={textRef}>
{/* Patriotic badge — identity + mission statement */}
<div className="inline-flex items-center px-3 sm:px-4 py-1.5 rounded-full bg-blue-100 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-500/20 backdrop-blur-sm mb-6 sm:mb-8 shadow-sm max-w-[95%] sm:max-w-none">
<span className="mr-1.5 sm:mr-2.5 text-sm sm:text-base">🇺🇸</span>
<span className="text-[8px] sm:text-[9px] md:text-[10px] font-bold tracking-wider sm:tracking-widest uppercase text-blue-600 dark:text-blue-300 leading-tight">
Crafted to Level the Field for Those Who Build{' '}
<span className="text-red-400">America</span>
</span>
</div>
<h1 className="text-6xl md:text-9xl font-black tracking-tighter mb-6 leading-[0.9]">
ROOFING <br />
<span className="text-transparent bg-clip-text bg-gradient-to-b from-blue-700 to-red-600 dark:from-blue-400 dark:to-red-400 text-4xl md:text-9xl">REVOLUTIONIZED</span>
{/* Headline — Inter font-black: BOLD, commanding, statement piece */}
<h1 className="text-5xl sm:text-6xl md:text-7xl lg:text-8xl xl:text-9xl font-black tracking-tighter mb-4 sm:mb-6 leading-[0.9] px-2">
<span className="text-zinc-900 dark:text-white">ROOFING</span>
<br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-blue-500 to-cyan-400">
REVOLUTIONIZED
</span>
</h1>
<p className="text-xl md:text-2xl text-zinc-500 dark:text-zinc-400 max-w-2xl mx-auto mb-12 font-medium">
{/* Subtitle — the value proposition with credibility */}
<p className="text-base sm:text-lg md:text-xl lg:text-2xl text-zinc-500 dark:text-white/50 max-w-2xl mx-auto mb-12 font-medium leading-relaxed px-2">
Automated drone inspections. Instant AI estimates.
<br className="hidden md:block" />
Restoring your home's integrity with 20+ years of precision.
<br />
Restoring your home's integrity with{' '}
<span className="text-zinc-900 dark:text-white font-bold">20+ years</span> of precision.
</p>
{/* Dual CTAs */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-12">
<Link to="/login" className="px-8 py-4 rounded-full bg-blue-700 hover:bg-blue-800 text-white font-bold transition-all flex items-center shadow-lg shadow-blue-600/20 hover:scale-105 ring-4 ring-blue-500/10 dark:ring-blue-400/10">
{/* B2C: Homeowners */}
<Link
to="/login"
className="group px-8 py-4 rounded-full bg-blue-600 hover:bg-blue-500 text-white font-bold text-sm transition-all flex items-center shadow-lg shadow-blue-600/20 hover:shadow-blue-500/30 hover:scale-[1.02] ring-4 ring-blue-500/10"
>
Schedule Demo
<ArrowRight size={18} className="ml-2" />
<ArrowRight size={18} className="ml-2 transition-transform group-hover:translate-x-0.5" />
</Link>
{/* B2B: Contractors */}
<Link
to="/login"
className="group px-8 py-4 rounded-full bg-zinc-100 dark:bg-white/[0.06] hover:bg-zinc-200 dark:hover:bg-white/[0.10] border border-zinc-300 dark:border-white/[0.10] hover:border-zinc-400 dark:hover:border-white/[0.15] text-zinc-700 dark:text-white/90 font-bold text-sm transition-all flex items-center hover:scale-[1.02]"
>
Start Free Trial
<ArrowRight size={18} className="ml-2 transition-transform group-hover:translate-x-0.5" />
</Link>
<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank" rel="noopener noreferrer" className="px-8 py-4 rounded-full bg-zinc-100 dark:bg-white/5 border border-zinc-200 dark:border-white/10 hover:bg-zinc-200 dark:hover:bg-white/10 font-semibold transition-colors">
See The Process
</a>
</div>
{/* Patriotic Discount Banner */}
<div className="max-w-md mx-auto bg-white/50 dark:bg-zinc-900/50 backdrop-blur-md rounded-2xl border border-red-100 dark:border-red-900/30 p-4 flex items-center justify-center space-x-4 shadow-sm">
<div className="w-10 h-10 rounded-full bg-red-100 dark:bg-red-900/20 flex items-center justify-center text-red-600 dark:text-red-400">
{/* Discount banner — Veterans, Seniors, Active Duty */}
<div className="max-w-md mx-4 sm:mx-auto bg-red-50 dark:bg-white/[0.04] backdrop-blur-md rounded-2xl border border-red-200 dark:border-red-500/20 p-3 sm:p-4 flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-0 sm:space-x-4 shadow-sm">
<div className="w-10 h-10 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center text-red-500 dark:text-red-400 shrink-0">
<Award size={20} />
</div>
<div className="text-left">
<h3 className="font-bold text-sm text-zinc-900 dark:text-white">Active Duty, Veterans & Seniors</h3>
<p className="text-xs text-zinc-500 dark:text-zinc-400">Get an exclusive <span className="text-red-600 dark:text-red-400 font-bold">15% Discount</span> on all services.</p>
<div className="text-center sm:text-left">
<h3 className="font-bold text-xs sm:text-sm text-zinc-900 dark:text-white">Active Duty, Veterans & Seniors</h3>
<p className="text-[10px] sm:text-xs text-zinc-500 dark:text-white/50">Get an exclusive <span className="text-red-500 dark:text-red-400 font-bold">15% Discount</span> on all services.</p>
</div>
</div>
</div>
</div>
{/* Layer 4: HUD Annotations — desktop only */}
<div className="hidden lg:block absolute bottom-16 left-8 z-20">
<div className="border-l border-cyan-500/40 dark:border-cyan-500/30 pl-3 space-y-1.5">
<p className="font-mono text-[11px] font-medium tracking-wide text-cyan-600 dark:text-cyan-400/80">
<span className="text-cyan-600 dark:text-cyan-400 mr-1.5">&#9656;</span>
2,847 PROPERTIES TRACKED
</p>
<p className="font-mono text-[10px] tracking-wide text-zinc-400 dark:text-white/30">
DFW METRO &middot; ZONE 2
</p>
</div>
</div>
<div className="hidden lg:block absolute bottom-16 right-8 z-20">
<div className="border-r border-amber-500/40 dark:border-amber-500/30 pr-3 text-right space-y-1.5">
<p className="font-mono text-[11px] font-medium tracking-wide text-amber-600 dark:text-amber-400/80">
<span className="mr-1.5">&#9656;</span>
HAIL WATCH ACTIVE
</p>
<p className="font-mono text-[10px] tracking-wide text-zinc-400 dark:text-white/30">
ETA 35 MIN &middot; WIND 24 MPH
</p>
</div>
</div>
{/* Scroll indicator */}
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-20 flex flex-col items-center gap-2">
<span className="font-mono text-[9px] tracking-[0.25em] uppercase text-zinc-400 dark:text-white/20">Scroll</span>
<ChevronDown size={18} className="text-zinc-400 dark:text-white/20 animate-bounce" />
</div>
</section>
{/* ── Shingle Divider ── */}
<div className="shingle-divider" />
{/* PROCESS SECTION (From Sky to Safety) */}
<div className="py-24 bg-white dark:bg-black/40 border-y border-zinc-100 dark:border-white/5" ref={addToRefs}>
@@ -342,57 +429,90 @@ const Landing = () => {
{/* NEW INTELLIGENCE MAP SECTION */}
<IntelligenceMap />
{/* FACT CHECK & STATS */}
<div className="py-24 bg-zinc-900 text-white relative overflow-hidden" ref={addToRefs}>
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')] opacity-10"></div>
{/* ── Shingle Divider ── */}
<div className="shingle-divider" />
{/* FACT CHECK & STATS — Blueprint grid + Measurement tape aesthetic */}
<div className="py-24 bg-zinc-100 dark:bg-zinc-900 text-zinc-900 dark:text-white relative overflow-hidden" ref={addToRefs}>
<div className="absolute inset-0 blueprint-grid"></div>
<div className="max-w-7xl mx-auto px-6 relative z-10">
<div className="text-center mb-16">
<h2 className="text-3xl md:text-5xl font-black mb-4">Why Risk The Wait?</h2>
<p className="text-zinc-400 max-w-2xl mx-auto">Delaying roof repairs leads to exponential damage. Here are the facts.</p>
<span className="permit-stamp permit-stamp--danger mb-6 inline-flex">&#9888; Critical Data</span>
<h2 className="text-3xl md:text-5xl font-black mb-4 mt-4">Why Risk The Wait?</h2>
<p className="text-zinc-500 dark:text-zinc-400 max-w-2xl mx-auto">Delaying roof repairs leads to exponential damage. Here are the facts.</p>
</div>
<div className="grid md:grid-cols-3 gap-8">
<div className="bg-white/5 p-8 rounded-3xl border border-white/10 hover:bg-white/10 transition-colors">
<div
ref={(el) => statRefs.current[0] = el}
data-target="40"
data-suffix="%"
className="text-4xl font-black text-red-500 mb-2"
>
0%
{/* Stat Card 1 — Insurance Denials */}
<div className="bg-white dark:bg-white/5 p-8 rounded-2xl border border-zinc-200 dark:border-white/10 hover:bg-zinc-50 dark:hover:bg-white/10 hover:-translate-y-2 hover:shadow-lg hover:shadow-black/20 transition-all duration-300 relative overflow-hidden cursor-default">
<div className="measure-ticks measure-ticks-top absolute top-0 left-4 right-4 h-3"></div>
<div className="mt-3">
<div
ref={(el) => statRefs.current[0] = el}
data-target="40"
data-suffix="%"
className="text-5xl font-mono font-bold text-red-500 mb-1 tracking-tight"
>
0%
</div>
<h3 className="font-bold text-lg mb-2">Insurance Denials</h3>
<p className="text-zinc-500 dark:text-zinc-400 text-sm leading-relaxed">Of claims are denied due to "lack of maintenance" if proof of regular inspection isn't provided.</p>
</div>
<h3 className="font-bold text-xl mb-2">Insurance Denials</h3>
<p className="text-zinc-400 text-sm">Of claims are denied due to "lack of maintenance" if proof of regular inspection isn't provided.</p>
<div className="mt-4 pt-3 border-t border-zinc-100 dark:border-white/5">
<span className="permit-stamp permit-stamp--danger">&#10005; High Risk</span>
</div>
<div className="measure-ticks absolute bottom-0 left-4 right-4 h-3"></div>
</div>
<div className="bg-white/5 p-8 rounded-3xl border border-white/10 hover:bg-white/10 transition-colors">
<div
ref={(el) => statRefs.current[1] = el}
data-target="5"
data-suffix="yrs"
className="text-4xl font-black text-yellow-500 mb-2"
>
0yrs
{/* Stat Card 2 — Life Reduced */}
<div className="bg-white dark:bg-white/5 p-8 rounded-2xl border border-zinc-200 dark:border-white/10 hover:bg-zinc-50 dark:hover:bg-white/10 hover:-translate-y-2 hover:shadow-lg hover:shadow-black/20 transition-all duration-300 relative overflow-hidden cursor-default">
<div className="measure-ticks measure-ticks-top absolute top-0 left-4 right-4 h-3"></div>
<div className="mt-3">
<div
ref={(el) => statRefs.current[1] = el}
data-target="5"
data-suffix=" yrs"
className="text-5xl font-mono font-bold text-yellow-500 mb-1 tracking-tight"
>
0 yrs
</div>
<h3 className="font-bold text-lg mb-2">Life Reduced</h3>
<p className="text-zinc-500 dark:text-zinc-400 text-sm leading-relaxed">A minor leak left for 6 months can reduce your roof's lifespan by up to 5 years.</p>
</div>
<h3 className="font-bold text-xl mb-2">Life Reduced</h3>
<p className="text-zinc-400 text-sm">A minor leak left for 6 months can reduce your roof's lifespan by up to 5 years.</p>
<div className="mt-4 pt-3 border-t border-zinc-100 dark:border-white/5">
<span className="permit-stamp permit-stamp--accent">&#9888; Caution</span>
</div>
<div className="measure-ticks absolute bottom-0 left-4 right-4 h-3"></div>
</div>
<div className="bg-white/5 p-8 rounded-3xl border border-white/10 hover:bg-white/10 transition-colors">
<div
ref={(el) => statRefs.current[2] = el}
data-target="15"
data-suffix="m"
className="text-4xl font-black text-green-500 mb-2"
>
0m
{/* Stat Card 3 — Estimate Time */}
<div className="bg-white dark:bg-white/5 p-8 rounded-2xl border border-zinc-200 dark:border-white/10 hover:bg-zinc-50 dark:hover:bg-white/10 hover:-translate-y-2 hover:shadow-lg hover:shadow-black/20 transition-all duration-300 relative overflow-hidden cursor-default">
<div className="measure-ticks measure-ticks-top absolute top-0 left-4 right-4 h-3"></div>
<div className="mt-3">
<div
ref={(el) => statRefs.current[2] = el}
data-target="15"
data-suffix=" min"
className="text-5xl font-mono font-bold text-emerald-500 mb-1 tracking-tight"
>
0 min
</div>
<h3 className="font-bold text-lg mb-2">Our Estimate Time</h3>
<p className="text-zinc-500 dark:text-zinc-400 text-sm leading-relaxed">While others take days, we give you a quote before our drone even lands.</p>
</div>
<h3 className="font-bold text-xl mb-2">Our Estimate Time</h3>
<p className="text-zinc-400 text-sm">While others take days, we give you a quote before our drone even lands.</p>
<div className="mt-4 pt-3 border-t border-zinc-100 dark:border-white/5">
<span className="permit-stamp permit-stamp--verified">&#10003; Verified</span>
</div>
<div className="measure-ticks absolute bottom-0 left-4 right-4 h-3"></div>
</div>
</div>
</div>
</div>
{/* ── Shingle Divider ── */}
<div className="shingle-divider" />
{/* INDUSTRIES THAT LOVE US */}
<div className="py-32" ref={addToRefs}>
<div className="max-w-7xl mx-auto px-6">
@@ -507,31 +627,190 @@ const Landing = () => {
</div>
</div>
{/* ── Shingle Divider ── */}
<div className="shingle-divider" />
{/* ══════════════════════════════════════════════════ */}
{/* CRANE CTA — "One crane, one card, one moment" */}
{/* Construction signature element before footer */}
{/* ══════════════════════════════════════════════════ */}
<section className="relative py-24 md:py-36 bg-zinc-100 dark:bg-[#09090b] overflow-hidden" ref={addToRefs}>
{/* Blueprint grid background */}
<div className="absolute inset-0 blueprint-grid-fine opacity-40"></div>
<div className="max-w-5xl mx-auto px-6 relative z-10 flex flex-col items-center">
{/* Tower Crane SVG — full-scale blueprint line art */}
<div className="w-full">
<svg viewBox="0 0 800 360" fill="none" className="w-full" aria-hidden="true">
{/* ===== TOWER / LATTICE MAST ===== */}
{/* Main vertical rails */}
<line x1="145" y1="55" x2="145" y2="360" stroke="var(--crane-line-strong)" strokeWidth="2" />
<line x1="195" y1="55" x2="195" y2="360" stroke="var(--crane-line-strong)" strokeWidth="2" />
{/* Horizontal rungs */}
<line x1="145" y1="93" x2="195" y2="93" stroke="var(--crane-line-medium)" strokeWidth="1" />
<line x1="145" y1="131" x2="195" y2="131" stroke="var(--crane-line-medium)" strokeWidth="1" />
<line x1="145" y1="169" x2="195" y2="169" stroke="var(--crane-line-medium)" strokeWidth="1" />
<line x1="145" y1="207" x2="195" y2="207" stroke="var(--crane-line-medium)" strokeWidth="1" />
<line x1="145" y1="245" x2="195" y2="245" stroke="var(--crane-line-medium)" strokeWidth="1" />
<line x1="145" y1="283" x2="195" y2="283" stroke="var(--crane-line-medium)" strokeWidth="1" />
<line x1="145" y1="321" x2="195" y2="321" stroke="var(--crane-line-medium)" strokeWidth="1" />
{/* X-bracing */}
<line x1="145" y1="93" x2="195" y2="131" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="195" y1="93" x2="145" y2="131" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="145" y1="169" x2="195" y2="207" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="195" y1="169" x2="145" y2="207" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="145" y1="245" x2="195" y2="283" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="195" y1="245" x2="145" y2="283" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="145" y1="321" x2="195" y2="360" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="195" y1="321" x2="145" y2="360" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
{/* ===== SLEWING PLATFORM + OPERATOR CAB ===== */}
<rect x="135" y="55" width="70" height="18" rx="2" fill="var(--crane-fill-surface)" stroke="var(--crane-line-medium)" strokeWidth="1.5" />
<rect x="135" y="40" width="34" height="17" rx="1" fill="rgba(59,130,246,0.06)" stroke="rgba(59,130,246,0.18)" strokeWidth="1" />
{/* Cab window */}
<rect x="140" y="44" width="12" height="8" rx="0.5" fill="rgba(59,130,246,0.1)" stroke="rgba(59,130,246,0.22)" strokeWidth="0.5" />
{/* ===== A-FRAME / APEX ===== */}
<line x1="170" y1="8" x2="145" y2="55" stroke="var(--crane-line-strong)" strokeWidth="1.5" />
<line x1="170" y1="8" x2="195" y2="55" stroke="var(--crane-line-strong)" strokeWidth="1.5" />
{/* Cross-tie */}
<line x1="155" y1="36" x2="185" y2="36" stroke="var(--crane-line-medium)" strokeWidth="1" />
{/* ===== SUPPORT CABLES (apex to jib tips) ===== */}
<line x1="170" y1="8" x2="30" y2="58" stroke="var(--crane-line-medium)" strokeWidth="1" />
<line x1="170" y1="8" x2="720" y2="58" stroke="var(--crane-line-medium)" strokeWidth="1" />
{/* ===== COUNTER-JIB (left of tower) ===== */}
<line x1="30" y1="58" x2="145" y2="58" stroke="var(--crane-line-medium)" strokeWidth="1.5" />
<line x1="48" y1="70" x2="145" y2="70" stroke="var(--crane-line-medium)" strokeWidth="1" />
{/* End closer */}
<line x1="30" y1="58" x2="48" y2="70" stroke="var(--crane-line-medium)" strokeWidth="1" />
{/* Diagonals */}
<line x1="48" y1="70" x2="80" y2="58" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="80" y1="70" x2="112" y2="58" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="112" y1="70" x2="145" y2="58" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
{/* Counterweight blocks */}
<rect x="28" y="58" width="26" height="18" rx="1" fill="rgba(59,130,246,0.08)" stroke="rgba(59,130,246,0.2)" strokeWidth="1" />
<rect x="58" y="60" width="18" height="14" rx="1" fill="rgba(59,130,246,0.05)" stroke="rgba(59,130,246,0.14)" strokeWidth="0.75" />
{/* ===== MAIN JIB / BOOM (right of tower) ===== */}
{/* Top chord */}
<line x1="195" y1="58" x2="720" y2="58" stroke="var(--crane-line-strong)" strokeWidth="2" />
{/* Bottom chord */}
<line x1="195" y1="70" x2="720" y2="70" stroke="var(--crane-line-medium)" strokeWidth="1.5" />
{/* End vertical */}
<line x1="720" y1="58" x2="720" y2="70" stroke="var(--crane-line-medium)" strokeWidth="1" />
{/* Diagonal bracing */}
<line x1="215" y1="70" x2="260" y2="58" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="280" y1="70" x2="325" y2="58" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="345" y1="70" x2="390" y2="58" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="410" y1="70" x2="455" y2="58" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="475" y1="70" x2="520" y2="58" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="540" y1="70" x2="585" y2="58" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="605" y1="70" x2="650" y2="58" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="670" y1="70" x2="715" y2="58" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
{/* Vertical stiffeners */}
<line x1="325" y1="58" x2="325" y2="70" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="455" y1="58" x2="455" y2="70" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
<line x1="585" y1="58" x2="585" y2="70" stroke="var(--crane-line-subtle)" strokeWidth="0.75" />
{/* ===== TROLLEY ===== */}
<rect x="390" y="70" width="20" height="10" rx="1" fill="rgba(59,130,246,0.1)" stroke="rgba(59,130,246,0.28)" strokeWidth="1" />
{/* Wheels */}
<circle cx="395" cy="70" r="2.5" fill="rgba(59,130,246,0.12)" stroke="rgba(59,130,246,0.25)" strokeWidth="0.75" />
<circle cx="405" cy="70" r="2.5" fill="rgba(59,130,246,0.12)" stroke="rgba(59,130,246,0.25)" strokeWidth="0.75" />
{/* ===== HOIST CABLE ===== */}
<line x1="400" y1="80" x2="400" y2="340" stroke="rgba(59,130,246,0.28)" strokeWidth="1.5" strokeDasharray="6 4" />
{/* ===== HOOK BLOCK ===== */}
<rect x="394" y="340" width="12" height="8" rx="2" fill="rgba(59,130,246,0.1)" stroke="rgba(59,130,246,0.3)" strokeWidth="1" />
{/* Hook */}
<line x1="400" y1="348" x2="400" y2="355" stroke="rgba(59,130,246,0.4)" strokeWidth="1.5" />
<path d="M400 355 C400 362 392 362 392 357" stroke="rgba(59,130,246,0.4)" strokeWidth="1.5" fill="none" strokeLinecap="round" />
</svg>
</div>
{/* CTA Card — "suspended" from crane, swings like a pendulum */}
<div className="crane-swing relative -mt-4 bg-white dark:bg-white/[0.03] backdrop-blur-sm rounded-2xl border border-zinc-200 dark:border-white/10 p-10 md:p-14 text-center max-w-lg w-full shadow-xl dark:shadow-[0_20px_60px_rgba(0,0,0,0.4)] cursor-default">
{/* Attachment point — visual connector to cable above */}
<div className="absolute -top-3 left-1/2 -translate-x-1/2 w-6 h-6 border border-blue-500/25 rounded-full bg-zinc-100 dark:bg-[#09090b] flex items-center justify-center">
<div className="w-2 h-2 bg-blue-500/40 rounded-full"></div>
</div>
<span className="permit-stamp permit-stamp--verified mb-6 inline-flex">&#10003; Project Ready</span>
<h3 className="text-3xl md:text-4xl font-black text-zinc-900 dark:text-white mb-3 tracking-tight">
Ready to Build?
</h3>
<p className="text-zinc-500 dark:text-zinc-400 text-sm md:text-base mb-8 max-w-sm mx-auto leading-relaxed">
Whether you protect one home or manage a thousand roofs, the future of roofing starts here.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link
to="/login"
className="group px-8 py-4 rounded-full bg-blue-600 hover:bg-blue-500 text-white font-bold text-sm transition-all flex items-center shadow-lg shadow-blue-600/20 hover:shadow-blue-500/30 hover:scale-[1.02]"
>
Schedule Demo
<ArrowRight size={16} className="ml-2 transition-transform group-hover:translate-x-0.5" />
</Link>
<Link
to="/login"
className="group px-8 py-4 rounded-full bg-zinc-100 dark:bg-white/[0.06] hover:bg-zinc-200 dark:hover:bg-white/[0.10] border border-zinc-300 dark:border-white/[0.10] hover:border-zinc-400 dark:hover:border-white/[0.15] text-zinc-700 dark:text-white/90 font-bold text-sm transition-all flex items-center hover:scale-[1.02]"
>
Start Free Trial
<ArrowRight size={16} className="ml-2 transition-transform group-hover:translate-x-0.5" />
</Link>
</div>
</div>
</div>
</section>
{/* ── Shingle Divider ── */}
<div className="shingle-divider" />
{/* FOOTER */}
<footer className="bg-zinc-950 border-t border-white/10 pt-24 pb-12 relative overflow-hidden">
<footer className="bg-zinc-100 dark:bg-zinc-950 border-t border-zinc-200 dark:border-white/10 pt-24 pb-12 relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-5 pointer-events-none mix-blend-overlay"></div>
<div className="max-w-7xl mx-auto px-6 relative z-10">
<div className="grid md:grid-cols-4 gap-12 mb-20">
<div className="col-span-1 md:col-span-2 space-y-6">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-10 lg:gap-14 mb-20">
{/* Brand */}
<div className="col-span-2 space-y-6">
<div className="flex items-center space-x-2">
<img src={Logo} alt="LynkedUp Pro" className="h-16 w-auto object-contain" />
{/* Removed: <span className="text-xl font-bold text-white tracking-tight">LynkedUp Pro</span> */}
</div>
<p className="text-sm leading-relaxed max-w-sm text-zinc-500">
Revolutionizing property management and roofing inspections with military-grade precision and AI-driven insights. Built for the modern homeowner.
</p>
<div className="flex space-x-4">
<div className="w-10 h-10 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center cursor-pointer transition-colors text-zinc-400 hover:text-pink-500"><Instagram size={18} /></div>
<div className="w-10 h-10 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center cursor-pointer transition-colors text-zinc-400 hover:text-blue-400"><Twitter size={18} /></div>
<div className="w-10 h-10 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center cursor-pointer transition-colors text-zinc-400 hover:text-red-500"><Youtube size={18} /></div>
<div className="w-10 h-10 rounded-full bg-zinc-200 dark:bg-white/5 hover:bg-zinc-300 dark:hover:bg-white/10 flex items-center justify-center cursor-pointer transition-colors text-zinc-500 dark:text-zinc-400 hover:text-pink-500"><Instagram size={18} /></div>
<div className="w-10 h-10 rounded-full bg-zinc-200 dark:bg-white/5 hover:bg-zinc-300 dark:hover:bg-white/10 flex items-center justify-center cursor-pointer transition-colors text-zinc-500 dark:text-zinc-400 hover:text-blue-400"><Twitter size={18} /></div>
<div className="w-10 h-10 rounded-full bg-zinc-200 dark:bg-white/5 hover:bg-zinc-300 dark:hover:bg-white/10 flex items-center justify-center cursor-pointer transition-colors text-zinc-500 dark:text-zinc-400 hover:text-red-500"><Youtube size={18} /></div>
</div>
</div>
{/* Platform */}
<div className="space-y-4">
<h4 className="text-white font-bold uppercase tracking-widest text-xs">Contact</h4>
<ul className="space-y-4 text-sm text-zinc-400">
<h4 className="text-zinc-900 dark:text-white font-bold uppercase tracking-widest text-xs">Platform</h4>
<ul className="space-y-2.5 text-sm text-zinc-500">
<li className="hover:text-zinc-900 dark:hover:text-white cursor-pointer transition-colors">For Homeowners</li>
<li className="hover:text-zinc-900 dark:hover:text-white cursor-pointer transition-colors">For Contractors</li>
<li className="hover:text-zinc-900 dark:hover:text-white cursor-pointer transition-colors">Intelligence Map</li>
<li className="hover:text-zinc-900 dark:hover:text-white cursor-pointer transition-colors">How It Works</li>
<li className="hover:text-zinc-900 dark:hover:text-white cursor-pointer transition-colors">Pricing</li>
</ul>
</div>
{/* Contact */}
<div className="space-y-4">
<h4 className="text-zinc-900 dark:text-white font-bold uppercase tracking-widest text-xs">Contact</h4>
<ul className="space-y-4 text-sm text-zinc-500 dark:text-zinc-400">
<li className="flex items-start">
<MapPin size={16} className="mr-3 mt-1 text-blue-500 shrink-0" />
<span>16990 Dallas Pkwy Suite 206<br />Dallas, TX 75248</span>
@@ -552,26 +831,27 @@ const Landing = () => {
</div>
</div>
{/* Legal */}
<div className="space-y-4">
<h4 className="text-white font-bold uppercase tracking-widest text-xs">Legal</h4>
<ul className="space-y-2 text-sm text-zinc-500">
<li className="hover:text-white cursor-pointer transition-colors">Privacy Policy</li>
<li className="hover:text-white cursor-pointer transition-colors">Terms of Service</li>
<li className="hover:text-white cursor-pointer transition-colors">Drone Flight Safety</li>
<li className="hover:text-white cursor-pointer transition-colors">Sitemap</li>
<h4 className="text-zinc-900 dark:text-white font-bold uppercase tracking-widest text-xs">Legal</h4>
<ul className="space-y-2.5 text-sm text-zinc-500">
<li className="hover:text-zinc-900 dark:hover:text-white cursor-pointer transition-colors">Privacy Policy</li>
<li className="hover:text-zinc-900 dark:hover:text-white cursor-pointer transition-colors">Terms of Service</li>
<li className="hover:text-zinc-900 dark:hover:text-white cursor-pointer transition-colors">Drone Flight Safety</li>
<li className="hover:text-zinc-900 dark:hover:text-white cursor-pointer transition-colors">Sitemap</li>
</ul>
</div>
</div>
<div className="border-t border-white/5 pt-12 flex flex-col md:flex-row justify-between items-center gap-6 text-xs text-zinc-600">
<div className="border-t border-zinc-200 dark:border-white/5 pt-12 flex flex-col md:flex-row justify-between items-center gap-6 text-xs text-zinc-600">
<div className="flex flex-col items-center md:items-start gap-1">
<p>&copy; 2026 LynkedUp Pro. All Rights Reserved.</p>
<p>Powered by <span className="text-zinc-400">LynkedUp Technologies</span></p>
<p>Powered by <span className="text-zinc-600 dark:text-zinc-400">LynkedUp Technologies</span></p>
</div>
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/5 hover:bg-white/10 hover:border-white/10 transition-all group cursor-default">
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-zinc-200 dark:bg-white/5 border border-zinc-300 dark:border-white/5 hover:bg-zinc-300 dark:hover:bg-white/10 hover:border-zinc-400 dark:hover:border-white/10 transition-all group cursor-default">
<span className="text-zinc-500"></span>
<span><span className="font-bold text-zinc-400 group-hover:text-white transition-colors">Satyam Rastogi</span> made this. <span className="text-zinc-500 group-hover:text-blue-400 transition-colors">Blame him for the bugs, praise him for the features.</span></span>
<span><span className="font-bold text-zinc-600 dark:text-zinc-400 group-hover:text-zinc-900 dark:group-hover:text-white transition-colors">Satyam Rastogi</span> made this. <span className="text-zinc-500 group-hover:text-blue-400 transition-colors">Blame him for the bugs, praise him for the features.</span></span>
<span className="attribution-reveal text-[8px] ml-2">5@7y@m R@570g1 | LynkedUpPro - You're reading the fine print? You'd be great at roofing contracts.</span>
</div>
</div>
+59 -1
View File
@@ -6,7 +6,65 @@ export default {
],
darkMode: 'class',
theme: {
extend: {},
extend: {
// --- Typography ---
fontFamily: {
display: ['"Instrument Serif"', 'Georgia', 'serif'],
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['"JetBrains Mono"', 'Consolas', 'monospace'],
},
// --- Color System: "Modern Trust" ---
colors: {
// Semantic surface colors
surface: {
DEFAULT: '#09090b', // zinc-950 — primary bg
secondary: '#0a0f1a', // navy-black — command center sections
card: 'rgba(255,255,255,0.05)',
'card-hover': 'rgba(255,255,255,0.08)',
},
// Semantic border
border: {
subtle: 'rgba(255,255,255,0.05)',
hover: 'rgba(255,255,255,0.10)',
},
// Accent system
accent: {
DEFAULT: '#3b82f6', // Blue-500 — primary action, trust
hover: '#2563eb', // Blue-600
data: '#06b6d4', // Cyan-500 — HUD/data readouts ONLY
home: '#10b981', // Emerald-500 — homeowner warmth/safety
urgency: '#f59e0b', // Amber-500 — weather alerts, deadlines
danger: '#ef4444', // Red-500 — critical states only
},
},
// --- Spacing & Layout ---
maxWidth: {
'content': '80rem', // 1280px — primary content width
'prose': '56rem', // 896px — text-heavy sections
},
// --- Construction-themed background patterns ---
backgroundImage: {
// Blueprint grid — engineering/architectural precision
'blueprint': [
'linear-gradient(rgba(59,130,246,0.05) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(59,130,246,0.05) 1px, transparent 1px)',
].join(', '),
// Blueprint with finer sub-grid
'blueprint-fine': [
'linear-gradient(rgba(59,130,246,0.03) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(59,130,246,0.03) 1px, transparent 1px)',
'linear-gradient(rgba(59,130,246,0.06) 1px, transparent 1px)',
'linear-gradient(90deg, rgba(59,130,246,0.06) 1px, transparent 1px)',
].join(', '),
},
backgroundSize: {
'blueprint': '48px 48px',
'blueprint-fine': '12px 12px, 12px 12px, 48px 48px, 48px 48px',
},
},
},
plugins: [],
}