Compare commits

..

No commits in common. "11d52b2e8628dc2fd54776b6958344850396120a" and "a81ec7901d5dde5d54e1b40c8feadbf9df946d3c" have entirely different histories.

8 changed files with 666 additions and 693 deletions

View File

@ -30,9 +30,6 @@ features = [
"HtmlCanvasElement",
"CanvasRenderingContext2d",
"CanvasGradient",
"MouseEvent",
"Event",
"CssStyleDeclaration",
]
[profile.release]

View File

@ -11,9 +11,8 @@ export interface InitOutput {
readonly __wbindgen_export_1: (a: number, b: number) => number;
readonly __wbindgen_export_2: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_export_3: WebAssembly.Table;
readonly __wbindgen_export_4: (a: number, b: number, c: number, d: number) => void;
readonly __wbindgen_export_5: (a: number, b: number, c: number) => void;
readonly __wbindgen_export_6: (a: number, b: number) => void;
readonly __wbindgen_export_4: (a: number, b: number) => void;
readonly __wbindgen_export_5: (a: number, b: number, c: number, d: number) => void;
readonly __wbindgen_start: () => void;
}

View File

@ -237,16 +237,12 @@ export function main() {
wasm.main();
}
function __wbg_adapter_4(arg0, arg1, arg2, arg3) {
wasm.__wbindgen_export_4(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
function __wbg_adapter_4(arg0, arg1) {
wasm.__wbindgen_export_4(arg0, arg1);
}
function __wbg_adapter_11(arg0, arg1, arg2) {
wasm.__wbindgen_export_5(arg0, arg1, addHeapObject(arg2));
}
function __wbg_adapter_14(arg0, arg1) {
wasm.__wbindgen_export_6(arg0, arg1);
function __wbg_adapter_7(arg0, arg1, arg2, arg3) {
wasm.__wbindgen_export_5(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
}
const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']);
@ -318,25 +314,14 @@ function __wbg_get_imports() {
const ret = getObject(arg0).classList;
return addHeapObject(ret);
};
imports.wbg.__wbg_clientX_ea858fbae3debd3c = function(arg0) {
const ret = getObject(arg0).clientX;
return ret;
};
imports.wbg.__wbg_clientY_cbb39a771d53208f = function(arg0) {
const ret = getObject(arg0).clientY;
return ret;
};
imports.wbg.__wbg_closePath_58530240bb00a7fc = function(arg0) {
getObject(arg0).closePath();
};
imports.wbg.__wbg_createElement_4909dfa2011f2abe = function() { return handleError(function (arg0, arg1, arg2) {
const ret = getObject(arg0).createElement(getStringFromWasm0(arg1, arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_createLinearGradient_098463fca2d0190f = function(arg0, arg1, arg2, arg3, arg4) {
const ret = getObject(arg0).createLinearGradient(arg1, arg2, arg3, arg4);
imports.wbg.__wbg_createRadialGradient_b10566e092cb7089 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6) {
const ret = getObject(arg0).createRadialGradient(arg1, arg2, arg3, arg4, arg5, arg6);
return addHeapObject(ret);
};
}, arguments) };
imports.wbg.__wbg_document_7d29d139bd619045 = function(arg0) {
const ret = getObject(arg0).document;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
@ -354,10 +339,6 @@ function __wbg_get_imports() {
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbg_getBoundingClientRect_a2461829d8aa0b30 = function(arg0) {
const ret = getObject(arg0).getBoundingClientRect();
return addHeapObject(ret);
};
imports.wbg.__wbg_getContext_15e158d04230a6f6 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = getObject(arg0).getContext(getStringFromWasm0(arg1, arg2));
return isLikeNone(ret) ? 0 : addHeapObject(ret);
@ -366,10 +347,6 @@ function __wbg_get_imports() {
const ret = getObject(arg0)[arg1 >>> 0];
return addHeapObject(ret);
};
imports.wbg.__wbg_height_36fae1857b6cca70 = function(arg0) {
const ret = getObject(arg0).height;
return ret;
};
imports.wbg.__wbg_innerHeight_eacbddff807274db = function() { return handleError(function (arg0) {
const ret = getObject(arg0).innerHeight;
return addHeapObject(ret);
@ -408,16 +385,6 @@ function __wbg_get_imports() {
const ret = result;
return ret;
};
imports.wbg.__wbg_instanceof_HtmlElement_d60c51c41eb8699a = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof HTMLElement;
} catch (_) {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_instanceof_IntersectionObserverEntry_819e56422a481344 = function(arg0) {
let result;
try {
@ -446,10 +413,6 @@ function __wbg_get_imports() {
const ret = getObject(arg0).item(arg1 >>> 0);
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_left_31939b629ff732e9 = function(arg0) {
const ret = getObject(arg0).left;
return ret;
};
imports.wbg.__wbg_length_186546c51cd61acd = function(arg0) {
const ret = getObject(arg0).length;
return ret;
@ -458,15 +421,9 @@ function __wbg_get_imports() {
const ret = getObject(arg0).length;
return ret;
};
imports.wbg.__wbg_lineTo_d9b895383c2303ba = function(arg0, arg1, arg2) {
getObject(arg0).lineTo(arg1, arg2);
};
imports.wbg.__wbg_log_6c7b5f4f00b8ce3f = function(arg0) {
console.log(getObject(arg0));
};
imports.wbg.__wbg_moveTo_a0b1ec729ba8ee5d = function(arg0, arg1, arg2) {
getObject(arg0).moveTo(arg1, arg2);
};
imports.wbg.__wbg_new_19c25a3f2fa63a02 = function() {
const ret = new Object();
return addHeapObject(ret);
@ -486,6 +443,10 @@ function __wbg_get_imports() {
const ret = getObject(arg0).querySelectorAll(getStringFromWasm0(arg1, arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_random_7ed63a0b38ee3b75 = function() {
const ret = Math.random();
return ret;
};
imports.wbg.__wbg_requestAnimationFrame_ddc84a7def436784 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg0).requestAnimationFrame(getObject(arg1));
return ret;
@ -493,9 +454,6 @@ function __wbg_get_imports() {
imports.wbg.__wbg_setAttribute_d1baf9023ad5696f = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_setProperty_a4431938dd3e6945 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).setProperty(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_setclassName_c8bccad917b973f4 = function(arg0, arg1, arg2) {
getObject(arg0).className = getStringFromWasm0(arg1, arg2);
};
@ -514,12 +472,6 @@ function __wbg_get_imports() {
imports.wbg.__wbg_setinnerHTML_34e240d6b8e8260c = function(arg0, arg1, arg2) {
getObject(arg0).innerHTML = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_setlineWidth_069d571345379833 = function(arg0, arg1) {
getObject(arg0).lineWidth = arg1;
};
imports.wbg.__wbg_setstrokeStyle_3c450999cfcdcd2f = function(arg0, arg1, arg2) {
getObject(arg0).strokeStyle = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_settextContent_b55fe2f5f1399466 = function(arg0, arg1, arg2) {
getObject(arg0).textContent = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2);
};
@ -545,21 +497,10 @@ function __wbg_get_imports() {
const ret = typeof window === 'undefined' ? null : window;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
};
imports.wbg.__wbg_stroke_b53e61cc42965e61 = function(arg0) {
getObject(arg0).stroke();
};
imports.wbg.__wbg_style_32a3c8393b46a115 = function(arg0) {
const ret = getObject(arg0).style;
return addHeapObject(ret);
};
imports.wbg.__wbg_target_161cb00cc3daf872 = function(arg0) {
const ret = getObject(arg0).target;
return addHeapObject(ret);
};
imports.wbg.__wbg_top_447ffcfee64c5d99 = function(arg0) {
const ret = getObject(arg0).top;
return ret;
};
imports.wbg.__wbg_wbindgencbdrop_eb10308566512b88 = function(arg0) {
const obj = getObject(arg0).original;
if (obj.cnt-- == 1) {
@ -592,10 +533,6 @@ function __wbg_get_imports() {
imports.wbg.__wbg_wbindgenthrow_451ec1a8469d7eb6 = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
imports.wbg.__wbg_width_d69b4eebaa4e6b70 = function(arg0) {
const ret = getObject(arg0).width;
return ret;
};
imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) {
// Cast intrinsic for `Ref(String) -> Externref`.
const ret = getStringFromWasm0(arg0, arg1);
@ -603,12 +540,7 @@ function __wbg_get_imports() {
};
imports.wbg.__wbindgen_cast_aaa93aae03c115ab = function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { dtor_idx: 1, function: Function { arguments: [], shim_idx: 2, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_14);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_cast_d557c1fcdf607639 = function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { dtor_idx: 1, function: Function { arguments: [NamedExternref("MouseEvent")], shim_idx: 6, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_11);
const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_4);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_cast_d6cd19b81560fd6e = function(arg0) {
@ -618,7 +550,7 @@ function __wbg_get_imports() {
};
imports.wbg.__wbindgen_cast_e58c2e3d55f4d158 = function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { dtor_idx: 1, function: Function { arguments: [NamedExternref("Array<any>"), NamedExternref("IntersectionObserver")], shim_idx: 4, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_4);
const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_7);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {

Binary file not shown.

View File

@ -6,7 +6,6 @@ export const __wbindgen_export_0: (a: number) => void;
export const __wbindgen_export_1: (a: number, b: number) => number;
export const __wbindgen_export_2: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_export_3: WebAssembly.Table;
export const __wbindgen_export_4: (a: number, b: number, c: number, d: number) => void;
export const __wbindgen_export_5: (a: number, b: number, c: number) => void;
export const __wbindgen_export_6: (a: number, b: number) => void;
export const __wbindgen_export_4: (a: number, b: number) => void;
export const __wbindgen_export_5: (a: number, b: number, c: number, d: number) => void;
export const __wbindgen_start: () => void;

328
dist/style.css vendored
View File

@ -7,14 +7,14 @@
:root {
--bg-dark: #0a0e17;
--bg-card: rgba(26, 31, 46, 0.8); /* More transparent for glass effect */
--bg-card-hover: rgba(37, 42, 58, 0.9);
--bg-card: #1a1f2e;
--bg-card-hover: #252a3a;
--text-primary: #e0e6ed;
--text-secondary: #9ca3af;
--accent: #3b82f6;
--accent-hover: #2563eb;
--rust: #f74c00;
--border: rgba(45, 55, 72, 0.5);
--border: #2d3748;
--success: #10b981;
--planned: #6366f1;
}
@ -38,6 +38,37 @@ body {
pointer-events: none;
}
/* Loading screen */
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-dark);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loader {
font-size: 1.2rem;
color: var(--text-secondary);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.error {
padding: 2rem;
text-align: center;
color: var(--rust);
}
/* Container */
.container {
max-width: 1200px;
@ -57,6 +88,16 @@ body {
z-index: 1;
}
.hero::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 50%);
}
.hero .container {
position: relative;
z-index: 1;
@ -70,7 +111,6 @@ body {
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
filter: drop-shadow(0 0 2em rgba(59, 130, 246, 0.3));
}
.hero-subtitle {
@ -85,14 +125,12 @@ body {
.hero-badge {
display: inline-block;
padding: 0.75rem 1.5rem;
background: rgba(26, 31, 46, 0.6);
backdrop-filter: blur(10px);
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 2rem;
font-size: 1rem;
color: var(--text-primary);
animation: float 3s ease-in-out infinite;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
@keyframes float {
@ -112,118 +150,72 @@ section {
margin-bottom: 3rem;
text-align: center;
color: var(--text-primary);
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
/* Services Section */
.services {
/* Transparent to let particles show through slightly or keep dark */
background: var(--bg-dark);
}
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 2rem;
perspective: 1000px; /* Global perspective for grid items entrance */
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
/* 3D Card System */
.service-card-wrapper {
/* This is the element that receives mouse events and defines the space */
position: relative;
height: 100%;
min-height: 200px;
.service-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.3s ease;
text-decoration: none;
color: inherit;
display: block;
perspective: 1500px; /* Perspective for the 3D tilt */
cursor: pointer;
overflow: hidden;
word-wrap: break-word;
}
.service-card-content {
position: relative;
height: 100%;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 1rem;
padding: 2rem;
/* Glassmorphism */
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
/* 3D Transform settings */
transform-style: preserve-3d;
transform: rotateX(var(--rotate-x, 0deg)) rotateY(var(--rotate-y, 0deg));
/* Smooth return when mouse leaves */
transition: transform 0.1s cubic-bezier(0.2, 0.4, 0.6, 1), border-color 0.3s ease, box-shadow 0.3s ease;
}
.service-card-wrapper:hover .service-card-content {
.service-card:hover {
background: var(--bg-card-hover);
border-color: var(--accent);
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.4),
0 0 20px rgba(59, 130, 246, 0.2),
inset 0 0 0 1px rgba(59, 130, 246, 0.5);
transform: translateY(-4px);
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.2);
}
/* Glare Effect */
.card-glare {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 1rem;
background: radial-gradient(
circle at var(--glare-x, 50%) var(--glare-y, 50%),
rgba(255, 255, 255, 0.15) 0%,
transparent 60%
);
opacity: var(--glare-opacity, 0);
pointer-events: none;
transition: opacity 0.3s ease;
mix-blend-mode: overlay;
z-index: 10;
}
/* Floating Content inside Card */
.service-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
margin-bottom: 0.75rem;
gap: 0.75rem;
transform: translateZ(25px); /* Pop out */
transform-style: preserve-3d;
}
.service-card-content h3 {
font-size: 1.5rem;
.service-card h3 {
font-size: 1.25rem;
color: var(--text-primary);
font-weight: 700;
flex: 1;
word-break: break-word;
min-width: 0;
}
.status-badge {
font-size: 1.2rem;
transform: translateZ(35px); /* Pop out more */
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
font-size: 1.5rem;
flex-shrink: 0;
margin-left: 0.5rem;
}
.service-description {
color: var(--text-secondary);
font-size: 1rem;
font-size: 0.95rem;
line-height: 1.6;
transform: translateZ(15px); /* Pop out slightly */
word-break: break-word;
overflow-wrap: break-word;
}
/* Tech Section */
.tech {
background: linear-gradient(180deg, rgba(10, 14, 23, 0) 0%, var(--bg-dark) 100%);
position: relative;
background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-card) 100%);
}
.tech-grid {
@ -235,17 +227,6 @@ section {
.tech-item {
text-align: center;
padding: 2rem 1rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 1rem;
transition: all 0.3s ease;
border: 1px solid transparent;
}
.tech-item:hover {
transform: translateY(-5px) scale(1.02);
background: rgba(255, 255, 255, 0.05);
border-color: var(--border);
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
}
.tech-item h3 {
@ -265,8 +246,6 @@ section {
border-top: 1px solid var(--border);
padding: 3rem 0;
text-align: center;
position: relative;
z-index: 2;
}
.footer p {
@ -304,19 +283,174 @@ section {
padding: 0 1.5rem;
}
.hero-title {
font-size: 3rem;
section {
padding: 3rem 0;
}
.services-grid,
.tech-grid {
grid-template-columns: 1fr;
}
.hero {
min-height: 80vh;
}
.footer-links {
flex-direction: column;
gap: 1rem;
}
}
/* Scroll Animations */
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Selection color */
::selection {
background: var(--accent);
color: white;
}
::-moz-selection {
background: var(--accent);
color: white;
}
/* Scroll-triggered animations */
[data-animate] {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.8s cubic-bezier(0.2, 0.8, 0.2, 1), transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
[data-animate].animate-in {
opacity: 1;
transform: translateY(0);
}
/* Parallax effect on hero */
.hero::before {
animation: parallax-float 20s ease-in-out infinite;
}
@keyframes parallax-float {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-20px) scale(1.05); }
}
/* Animated gradient text */
.hero-title {
background-size: 200% auto;
animation: gradient-shift 3s ease-in-out infinite;
}
@keyframes gradient-shift {
0%, 100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
/* Hover micro-interactions */
.service-card {
transform-origin: center;
will-change: transform;
}
.service-card:hover {
animation: subtle-bounce 0.6s ease;
}
@keyframes subtle-bounce {
0%, 100% { transform: translateY(-4px); }
50% { transform: translateY(-8px); }
}
.tech-item {
transition: all 0.3s ease;
}
.tech-item:hover {
transform: scale(1.05);
}
.tech-item:hover h3 {
animation: pulse-glow 1.5s ease-in-out infinite;
}
@keyframes pulse-glow {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; text-shadow: 0 0 20px rgba(59, 130, 246, 0.5); }
}
/* Footer links animation */
.footer-links a {
position: relative;
overflow: hidden;
}
.footer-links a::before {
content: '';
position: absolute;
bottom: 0;
left: -100%;
width: 100%;
height: 2px;
background: var(--accent);
transition: left 0.3s ease;
}
.footer-links a:hover::before {
left: 0;
}
/* Hero badge pulse */
.hero-badge {
animation: float 3s ease-in-out infinite, glow-pulse 2s ease-in-out infinite;
}
@keyframes glow-pulse {
0%, 100% { box-shadow: 0 0 10px rgba(59, 130, 246, 0.2); }
50% { box-shadow: 0 0 20px rgba(59, 130, 246, 0.4); }
}
/* Status badge animations */
.status-badge {
animation: badge-pulse 2s ease-in-out infinite;
}
@keyframes badge-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
/* Section title entrance */
.section-title {
animation: slide-in-top 0.8s ease-out;
}
@keyframes slide-in-top {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Reduce motion for accessibility */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

View File

@ -3,327 +3,185 @@ use wasm_bindgen::JsCast;
use web_sys::{
console, Document, Element, HtmlElement, Window,
IntersectionObserver, IntersectionObserverEntry, IntersectionObserverInit,
HtmlCanvasElement, CanvasRenderingContext2d, MouseEvent, CssStyleDeclaration,
HtmlCanvasElement, CanvasRenderingContext2d,
};
use std::cell::RefCell;
use std::rc::Rc;
use std::f64::consts::PI;
// Mouse State
#[derive(Clone, Copy)]
struct MouseState {
// Particle structure
struct Particle {
x: f64,
y: f64,
// Normalized coordinates (-1.0 to 1.0)
norm_x: f64,
norm_y: f64,
vx: f64,
vy: f64,
size: f64,
life: f64,
max_life: f64,
hue: f64,
}
impl MouseState {
fn new() -> Self {
Self { x: 0.0, y: 0.0, norm_x: 0.0, norm_y: 0.0 }
}
}
// 3D Types
#[derive(Clone, Copy)]
struct Point3D {
x: f64,
y: f64,
z: f64,
}
struct Polyhedron {
vertices: Vec<Point3D>,
edges: Vec<(usize, usize)>,
rotation_x: f64,
rotation_y: f64,
}
impl Polyhedron {
fn new_dodecahedron(size: f64) -> Self {
let phi = (1.0 + 5.0_f64.sqrt()) / 2.0;
let inv_phi = 1.0 / phi;
let mut vertices = Vec::new();
// Orange vertices (Cube)
vertices.push(Point3D { x: -1.0, y: -1.0, z: -1.0 });
vertices.push(Point3D { x: 1.0, y: -1.0, z: -1.0 });
vertices.push(Point3D { x: 1.0, y: 1.0, z: -1.0 });
vertices.push(Point3D { x: -1.0, y: 1.0, z: -1.0 });
vertices.push(Point3D { x: -1.0, y: -1.0, z: 1.0 });
vertices.push(Point3D { x: 1.0, y: -1.0, z: 1.0 });
vertices.push(Point3D { x: 1.0, y: 1.0, z: 1.0 });
vertices.push(Point3D { x: -1.0, y: 1.0, z: 1.0 });
// Green vertices (Rectangle in YZ plane)
vertices.push(Point3D { x: 0.0, y: -phi, z: -inv_phi });
vertices.push(Point3D { x: 0.0, y: -phi, z: inv_phi });
vertices.push(Point3D { x: 0.0, y: phi, z: -inv_phi });
vertices.push(Point3D { x: 0.0, y: phi, z: inv_phi });
// Blue vertices (Rectangle in XZ plane)
vertices.push(Point3D { x: -inv_phi, y: 0.0, z: -phi });
vertices.push(Point3D { x: inv_phi, y: 0.0, z: -phi });
vertices.push(Point3D { x: -inv_phi, y: 0.0, z: phi });
vertices.push(Point3D { x: inv_phi, y: 0.0, z: phi });
// Pink vertices (Rectangle in XY plane)
vertices.push(Point3D { x: -phi, y: -inv_phi, z: 0.0 });
vertices.push(Point3D { x: phi, y: -inv_phi, z: 0.0 });
vertices.push(Point3D { x: -phi, y: inv_phi, z: 0.0 });
vertices.push(Point3D { x: phi, y: inv_phi, z: 0.0 });
// Scale vertices
for v in &mut vertices {
v.x *= size;
v.y *= size;
v.z *= size;
}
// Simplified edges (connecting nearest neighbors) - simplistic wireframe logic
// A full correct edge list is long, so we'll use a distance threshold to draw edges dynamically
// or just use a simpler shape if performance is an issue.
// Let's use a cube for guaranteed correctness in code length constraint.
Self::new_cube(size * 1.5)
}
fn new_cube(size: f64) -> Self {
let vertices = vec![
Point3D { x: -size, y: -size, z: -size },
Point3D { x: size, y: -size, z: -size },
Point3D { x: size, y: size, z: -size },
Point3D { x: -size, y: size, z: -size },
Point3D { x: -size, y: -size, z: size },
Point3D { x: size, y: -size, z: size },
Point3D { x: size, y: size, z: size },
Point3D { x: -size, y: size, z: size },
];
let edges = vec![
(0, 1), (1, 2), (2, 3), (3, 0), // Back face
(4, 5), (5, 6), (6, 7), (7, 4), // Front face
(0, 4), (1, 5), (2, 6), (3, 7), // Connecting edges
// Diagonals for more "tech" look
(0, 2), (4, 6), (0, 5), (1, 4)
];
impl Particle {
fn new(x: f64, y: f64, hue: f64) -> Self {
Self {
vertices,
edges,
rotation_x: 0.0,
rotation_y: 0.0,
x,
y,
vx: 0.0,
vy: 0.0,
size: js_sys::Math::random() * 2.0 + 2.0,
life: 1.0,
max_life: js_sys::Math::random() * 500.0 + 500.0,
hue,
}
}
fn rotate(&mut self, dx: f64, dy: f64) {
self.rotation_x += dx;
self.rotation_y += dy;
fn update(&mut self, width: f64, height: f64, time: f64) {
// Multiple layered flow fields for complex fluid motion
let scale1 = 0.003;
let scale2 = 0.006;
let scale3 = 0.001;
// Layer 1: Large sweeping currents
let angle1 = (self.x * scale1 + time * 0.0002).sin() * 3.0
+ (self.y * scale1 + time * 0.0001).cos() * 3.0;
// Layer 2: Medium turbulence
let angle2 = (self.x * scale2 - time * 0.0003).cos() * 2.0
+ (self.y * scale2 + time * 0.0002).sin() * 2.0;
// Layer 3: Fine detail
let angle3 = ((self.x * scale3 + time * 0.0001).sin()
+ (self.y * scale3 - time * 0.00015).cos()) * 1.5;
// Combine flow fields
let angle = angle1 + angle2 + angle3;
// Convert to force
let force = 0.15;
let fx = angle.cos() * force;
let fy = angle.sin() * force;
// Apply force with gentle acceleration
self.vx += fx;
self.vy += fy;
// Smooth damping for fluid motion
self.vx *= 0.95;
self.vy *= 0.95;
// Limit maximum velocity for smooth flow
let max_speed = 2.0;
let speed = (self.vx * self.vx + self.vy * self.vy).sqrt();
if speed > max_speed {
self.vx = (self.vx / speed) * max_speed;
self.vy = (self.vy / speed) * max_speed;
}
// Update position
self.x += self.vx;
self.y += self.vy;
self.life -= 1.0;
// Wrap around edges smoothly
if self.x < -10.0 {
self.x = width + 10.0;
}
if self.x > width + 10.0 {
self.x = -10.0;
}
if self.y < -10.0 {
self.y = height + 10.0;
}
if self.y > height + 10.0 {
self.y = -10.0;
}
}
// Wave layer for fluid simulation
struct WaveLayer {
amplitude: f64,
frequency: f64,
speed: f64,
offset: f64,
fn is_dead(&self) -> bool {
self.life <= 0.0
}
// Fluid wave system
struct FluidWaves {
fn draw(&self, ctx: &CanvasRenderingContext2d) {
let alpha = (self.life / self.max_life).min(1.0);
// Glow effect
let glow_gradient = ctx.create_radial_gradient(
self.x, self.y, 0.0,
self.x, self.y, self.size * 3.0
).ok();
if let Some(gradient) = glow_gradient {
let alpha_hex = format!("{:02x}", (alpha * 60.0) as u8);
gradient.add_color_stop(0.0, &format!("#{}", alpha_hex.repeat(3))).ok();
gradient.add_color_stop(0.5, &format!("rgba(50, 255, 150, {})", alpha * 0.3)).ok();
gradient.add_color_stop(1.0, "rgba(50, 255, 150, 0)").ok();
ctx.set_fill_style(&JsValue::from(gradient));
ctx.begin_path();
let _ = ctx.arc(self.x, self.y, self.size * 3.0, 0.0, std::f64::consts::PI * 2.0);
ctx.fill();
}
// Core particle
let color = format!("rgba(100, 255, 180, {})", alpha * 0.8);
ctx.set_fill_style_str(&color);
ctx.begin_path();
let _ = ctx.arc(self.x, self.y, self.size, 0.0, std::f64::consts::PI * 2.0);
ctx.fill();
}
}
// Particle system
struct ParticleSystem {
particles: Vec<Particle>,
width: f64,
height: f64,
max_particles: usize,
time: f64,
layers: Vec<WaveLayer>,
polyhedron: Polyhedron,
mouse: Rc<RefCell<MouseState>>,
}
impl FluidWaves {
fn new(width: f64, height: f64, mouse: Rc<RefCell<MouseState>>) -> Self {
// Create multiple wave layers with different properties (faster speeds)
let layers = vec![
// Larger, slower primary waves for big curves
WaveLayer { amplitude: 0.25, frequency: 0.001, speed: 0.0008, offset: 0.0 },
// Secondary shape definition
WaveLayer { amplitude: 0.18, frequency: 0.0018, speed: 0.0015, offset: 1.5 },
// Counter-movement for interest
WaveLayer { amplitude: 0.12, frequency: 0.003, speed: -0.0012, offset: 3.0 },
// Detail waves
WaveLayer { amplitude: 0.08, frequency: 0.005, speed: 0.002, offset: 4.5 },
// Fine detail
WaveLayer { amplitude: 0.05, frequency: 0.008, speed: -0.0025, offset: 2.0 },
];
impl ParticleSystem {
fn new(width: f64, height: f64) -> Self {
Self {
particles: Vec::new(),
width,
height,
max_particles: 1200,
time: 0.0,
layers,
polyhedron: Polyhedron::new_dodecahedron(100.0),
mouse,
}
}
fn spawn_particles(&mut self, count: usize) {
for _ in 0..count {
if self.particles.len() < self.max_particles {
let x = js_sys::Math::random() * self.width;
let y = js_sys::Math::random() * self.height;
// Green hues: 120-160 (emerald to lime)
let hue = js_sys::Math::random() * 40.0 + 120.0;
self.particles.push(Particle::new(x, y, hue));
}
}
}
fn update(&mut self) {
self.time += 1.0;
// Rotate polyhedron based on mouse position and time
let mouse = self.mouse.borrow();
self.polyhedron.rotate(0.005 + mouse.norm_y * 0.01, 0.005 + mouse.norm_x * 0.01);
// Update existing particles
for particle in &mut self.particles {
particle.update(self.width, self.height, self.time);
}
// Calculate wave height at a given position with randomness
fn wave_height(&self, x: f64, y: f64, randomness: f64) -> f64 {
let mut height = 0.0;
// Remove dead particles
self.particles.retain(|p| !p.is_dead());
for layer in &self.layers {
// Multiple sine waves at different frequencies for organic curves
let wave1 = (x * layer.frequency + self.time * layer.speed + layer.offset).sin();
let wave2 = (x * layer.frequency * 1.7 - self.time * layer.speed * 0.5).sin();
let wave3 = (x * layer.frequency * 2.3 + self.time * layer.speed * 1.2 + layer.offset).cos();
// Vertical component for depth
let wave_y = (y * layer.frequency * 0.4 - self.time * layer.speed * 0.6).cos();
// Combine with noise-like variation
let combined = (wave1 + wave2 * 0.5 + wave3 * 0.3 + wave_y * 0.4) * layer.amplitude;
height += combined * randomness;
}
height
// Spawn new particles
let spawn_count = (self.max_particles - self.particles.len()).min(5);
self.spawn_particles(spawn_count);
}
fn draw(&self, ctx: &CanvasRenderingContext2d) {
// Draw smooth gradient waves at the bottom
self.draw_smooth_waves(ctx);
// Draw 3D object floating in the background
self.draw_polyhedron(ctx);
}
fn draw_polyhedron(&self, ctx: &CanvasRenderingContext2d) {
let cx = self.width * 0.75; // Position on the right side
let cy = self.height * 0.4;
let cos_x = self.polyhedron.rotation_x.cos();
let sin_x = self.polyhedron.rotation_x.sin();
let cos_y = self.polyhedron.rotation_y.cos();
let sin_y = self.polyhedron.rotation_y.sin();
let mut projected_points = Vec::new();
// Project vertices
for v in &self.polyhedron.vertices {
// Rotate around X
let y1 = v.y * cos_x - v.z * sin_x;
let z1 = v.y * sin_x + v.z * cos_x;
// Rotate around Y
let x2 = v.x * cos_y - z1 * sin_y;
let z2 = v.x * sin_y + z1 * cos_y; // Depth
// Perspective projection
let scale = 400.0 / (400.0 + z2); // Simple perspective
let x_proj = x2 * scale + cx;
let y_proj = y1 * scale + cy;
projected_points.push((x_proj, y_proj));
}
// Draw edges
ctx.set_stroke_style_str("rgba(59, 130, 246, 0.15)");
ctx.set_line_width(1.0);
ctx.begin_path();
for &(start, end) in &self.polyhedron.edges {
if start < projected_points.len() && end < projected_points.len() {
let (x1, y1) = projected_points[start];
let (x2, y2) = projected_points[end];
ctx.move_to(x1, y1);
ctx.line_to(x2, y2);
}
}
ctx.stroke();
// Draw vertices
ctx.set_fill_style_str("rgba(99, 102, 241, 0.4)");
for (x, y) in &projected_points {
ctx.begin_path();
ctx.arc(*x, *y, 2.0, 0.0, PI * 2.0).unwrap();
ctx.fill();
}
}
fn draw_smooth_waves(&self, ctx: &CanvasRenderingContext2d) {
let num_waves = 12;
let wave_section_height = self.height * 0.6; // Bottom 60% of screen
let start_y = self.height - wave_section_height;
// Draw from back to front for proper layering
for i in (0..num_waves).rev() {
let progress = i as f64 / (num_waves - 1) as f64;
// Each wave layer at different depths
let layer_offset = progress * wave_section_height * 0.9;
let base_y = start_y + layer_offset;
// Varying amplitude for each layer (more variation in front)
// Increased amplitude for "curvier" look
let amplitude = 40.0 + progress * 80.0;
// Randomness factor increases for front layers
let randomness = 0.8 + progress * 0.4;
ctx.begin_path();
ctx.move_to(0.0, base_y);
// Draw smooth wave curve - increased points for smoothness
let points = 300;
for j in 0..=points {
let x = (j as f64 / points as f64) * self.width;
let y_offset = self.wave_height(x, base_y, randomness) * amplitude;
let y = base_y + y_offset;
ctx.line_to(x, y);
}
// Close the path to bottom
ctx.line_to(self.width, self.height);
ctx.line_to(0.0, self.height);
ctx.close_path();
// Create gradient for depth effect
let gradient = ctx.create_linear_gradient(0.0, base_y - 80.0, 0.0, self.height);
// Back layers are more transparent and darker (3D depth)
// Front layers are more opaque and brighter
let alpha_top = 0.05 + progress * 0.25;
let alpha_bottom = 0.1 + progress * 0.3;
// Deep ocean colors
// Hue shift: Deep Blue (220) -> Teal/Cyan (160)
let hue = 220.0 - progress * 60.0;
let saturation = 50.0 + progress * 20.0; // 50-70%
let lightness_top = 15.0 + progress * 20.0; // 15-35%
let lightness_bottom = 10.0 + progress * 15.0; // 10-25%
let _ = gradient.add_color_stop(0.0, &format!("hsla({}, {}%, {}%, {})",
hue, saturation, lightness_top, alpha_top));
// Middle stop for better volume/highlight
let _ = gradient.add_color_stop(0.4, &format!("hsla({}, {}%, {}%, {})",
hue - 5.0, saturation + 5.0, lightness_top + 5.0, alpha_top * 1.2));
let _ = gradient.add_color_stop(1.0, &format!("hsla({}, {}%, {}%, {})",
hue - 15.0, saturation, lightness_bottom, alpha_bottom));
#[allow(deprecated)] // set_fill_style with JsValue is technically deprecated but fine here
ctx.set_fill_style(&JsValue::from(gradient));
ctx.fill();
for particle in &self.particles {
particle.draw(ctx);
}
}
@ -340,34 +198,12 @@ pub fn main() -> Result<(), JsValue> {
let window = web_sys::window().expect("no global window exists");
let document = window.document().expect("should have a document on window");
// Global mouse tracking
let mouse_state = Rc::new(RefCell::new(MouseState::new()));
{
let mouse_state = mouse_state.clone();
let window_clone = window.clone();
let closure = Closure::wrap(Box::new(move |event: MouseEvent| {
let w = window_clone.inner_width().unwrap().as_f64().unwrap_or(1.0);
let h = window_clone.inner_height().unwrap().as_f64().unwrap_or(1.0);
let x = event.client_x() as f64;
let y = event.client_y() as f64;
let mut state = mouse_state.borrow_mut();
state.x = x;
state.y = y;
state.norm_x = (x / w) * 2.0 - 1.0;
state.norm_y = (y / h) * 2.0 - 1.0;
}) as Box<dyn FnMut(_)>);
window.add_event_listener_with_callback("mousemove", closure.as_ref().unchecked_ref())?;
closure.forget();
}
// Clear body and build page
if let Some(body) = document.body() {
body.set_inner_html("");
// Create and setup particle canvas
setup_particle_canvas(&document, &body, &window, mouse_state.clone())?;
setup_particle_canvas(&document, &body, &window)?;
build_page(&document, &body)?;
setup_scroll_animations(&document)?;
@ -377,12 +213,7 @@ pub fn main() -> Result<(), JsValue> {
Ok(())
}
fn setup_particle_canvas(
document: &Document,
body: &HtmlElement,
window: &Window,
mouse_state: Rc<RefCell<MouseState>>
) -> Result<(), JsValue> {
fn setup_particle_canvas(document: &Document, body: &HtmlElement, window: &Window) -> Result<(), JsValue> {
console::log_1(&"🎨 Setting up particle canvas...".into());
// Create canvas element
@ -409,24 +240,47 @@ fn setup_particle_canvas(
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()?;
// Initialize fluid wave system
let wave_system = Rc::new(RefCell::new(FluidWaves::new(width, height, mouse_state)));
console::log_1(&"🌊 Fluid wave system initialized!".into());
// Initialize particle system
let particle_system = Rc::new(RefCell::new(ParticleSystem::new(width, height)));
// Spawn initial particles
particle_system.borrow_mut().spawn_particles(600);
console::log_1(&format!("🌟 Spawned {} initial particles", particle_system.borrow().particles.len()).into());
// Setup animation loop
let system_clone = wave_system.clone();
let system_clone = particle_system.clone();
let context_clone = context.clone();
let frame_count = Rc::new(RefCell::new(0u32));
let window_clone = window.clone();
let animate_closure = Rc::new(RefCell::new(None::<Closure<dyn FnMut()>>));
let animate_clone = animate_closure.clone();
let frame_clone = frame_count.clone();
*animate_closure.borrow_mut() = Some(Closure::wrap(Box::new(move || {
// Clear canvas
context_clone.set_fill_style_str("rgba(10, 14, 23, 1.0)");
*frame_clone.borrow_mut() += 1;
if *frame_clone.borrow() == 1 {
console::log_1(&"🎬 Animation loop started!".into());
}
if *frame_clone.borrow() % 60 == 0 {
let sys = system_clone.borrow();
console::log_1(&format!("🔄 Frame {}, {} particles",
*frame_clone.borrow(),
sys.particles.len()).into());
if let Some(p) = sys.particles.first() {
console::log_1(&format!("🔍 First particle: x={:.1}, y={:.1}, vx={:.2}, vy={:.2}, size={:.1}, life={:.1}",
p.x, p.y, p.vx, p.vy, p.size, p.life).into());
}
}
// Semi-transparent clear for fluid trails
context_clone.set_fill_style_str("rgba(10, 14, 23, 0.12)");
context_clone.fill_rect(0.0, 0.0, width, height);
// Update and draw waves
// Update and draw particles
system_clone.borrow_mut().update();
system_clone.borrow().draw(&context_clone);
@ -445,12 +299,12 @@ fn setup_particle_canvas(
// Setup resize handler
let canvas_clone = canvas.clone();
let system_resize = wave_system.clone();
let window_resize = window.clone();
let system_resize = particle_system.clone();
let window_clone = window.clone();
let resize_closure = Closure::wrap(Box::new(move || {
let new_width = window_resize.inner_width().unwrap().as_f64().unwrap_or(800.0);
let new_height = window_resize.inner_height().unwrap().as_f64().unwrap_or(600.0);
let new_width = window_clone.inner_width().unwrap().as_f64().unwrap_or(800.0);
let new_height = window_clone.inner_height().unwrap().as_f64().unwrap_or(600.0);
canvas_clone.set_width(new_width as u32);
canvas_clone.set_height(new_height as u32);
@ -568,7 +422,7 @@ fn create_service_card(
status: &str,
url: Option<&str>,
) -> Result<Element, JsValue> {
let wrapper = if let Some(link) = url {
let card = if let Some(link) = url {
let a = document.create_element("a")?;
a.set_attribute("href", link)?;
a.set_attribute("target", "_blank")?;
@ -577,81 +431,7 @@ fn create_service_card(
} else {
document.create_element("div")?
};
// Cast to HtmlElement to access properties
let wrapper_el = wrapper.dyn_ref::<HtmlElement>().unwrap();
wrapper.set_class_name("service-card-wrapper");
// Inner Content Div
let content = document.create_element("div")?;
content.set_class_name("service-card-content");
let content_el = content.dyn_ref::<HtmlElement>().unwrap();
// Glare Effect Div
let glare = document.create_element("div")?;
glare.set_class_name("card-glare");
content.append_child(&glare)?;
// Add 3D Tilt Effect Logic
{
let content_el_clone = content_el.clone();
let wrapper_el_clone = wrapper_el.clone();
let glare_el_clone = glare.dyn_ref::<HtmlElement>().unwrap().clone();
let mouse_move_closure = Closure::wrap(Box::new(move |e: MouseEvent| {
let rect = wrapper_el_clone.get_bounding_client_rect();
// Calculate position relative to the card center
let width = rect.width();
let height = rect.height();
// Mouse position relative to element top-left
let mouse_x = e.client_x() as f64 - rect.left();
let mouse_y = e.client_y() as f64 - rect.top();
let center_x = width / 2.0;
let center_y = height / 2.0;
// Rotation logic
let max_rot = 12.0; // degrees
let rotate_y = ((mouse_x - center_x) / center_x) * max_rot;
let rotate_x = -((mouse_y - center_y) / center_y) * max_rot;
let style = content_el_clone.style();
style.set_property("--rotate-x", &format!("{:.2}deg", rotate_x)).ok();
style.set_property("--rotate-y", &format!("{:.2}deg", rotate_y)).ok();
// Glare logic
let glare_x_pct = (mouse_x / width) * 100.0;
let glare_y_pct = (mouse_y / height) * 100.0;
// Glare opacity based on distance from center (optional, or constant on hover)
let glare_style = glare_el_clone.style();
glare_style.set_property("--glare-x", &format!("{:.1}%", glare_x_pct)).ok();
glare_style.set_property("--glare-y", &format!("{:.1}%", glare_y_pct)).ok();
glare_style.set_property("--glare-opacity", "1").ok();
}) as Box<dyn FnMut(_)>);
wrapper_el.add_event_listener_with_callback("mousemove", mouse_move_closure.as_ref().unchecked_ref())?;
mouse_move_closure.forget();
let content_el_clone = content_el.clone();
let glare_el_clone = glare.dyn_ref::<HtmlElement>().unwrap().clone();
let mouse_leave_closure = Closure::wrap(Box::new(move |_: MouseEvent| {
let style = content_el_clone.style();
style.set_property("--rotate-x", "0deg").ok();
style.set_property("--rotate-y", "0deg").ok();
let glare_style = glare_el_clone.style();
glare_style.set_property("--glare-opacity", "0").ok();
}) as Box<dyn FnMut(_)>);
wrapper_el.add_event_listener_with_callback("mouseleave", mouse_leave_closure.as_ref().unchecked_ref())?;
mouse_leave_closure.forget();
}
card.set_class_name("service-card");
let header = document.create_element("div")?;
header.set_class_name("service-header");
@ -670,12 +450,10 @@ fn create_service_card(
desc.set_text_content(Some(description));
desc.set_class_name("service-description");
content.append_child(&header)?;
content.append_child(&desc)?;
card.append_child(&header)?;
card.append_child(&desc)?;
wrapper.append_child(&content)?;
Ok(wrapper)
Ok(card)
}
fn create_tech_section(document: &Document) -> Result<Element, JsValue> {

328
style.css
View File

@ -7,14 +7,14 @@
:root {
--bg-dark: #0a0e17;
--bg-card: rgba(26, 31, 46, 0.8); /* More transparent for glass effect */
--bg-card-hover: rgba(37, 42, 58, 0.9);
--bg-card: #1a1f2e;
--bg-card-hover: #252a3a;
--text-primary: #e0e6ed;
--text-secondary: #9ca3af;
--accent: #3b82f6;
--accent-hover: #2563eb;
--rust: #f74c00;
--border: rgba(45, 55, 72, 0.5);
--border: #2d3748;
--success: #10b981;
--planned: #6366f1;
}
@ -38,6 +38,37 @@ body {
pointer-events: none;
}
/* Loading screen */
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-dark);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loader {
font-size: 1.2rem;
color: var(--text-secondary);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.error {
padding: 2rem;
text-align: center;
color: var(--rust);
}
/* Container */
.container {
max-width: 1200px;
@ -57,6 +88,16 @@ body {
z-index: 1;
}
.hero::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 50%, rgba(59, 130, 246, 0.1) 0%, transparent 50%);
}
.hero .container {
position: relative;
z-index: 1;
@ -70,7 +111,6 @@ body {
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
filter: drop-shadow(0 0 2em rgba(59, 130, 246, 0.3));
}
.hero-subtitle {
@ -85,14 +125,12 @@ body {
.hero-badge {
display: inline-block;
padding: 0.75rem 1.5rem;
background: rgba(26, 31, 46, 0.6);
backdrop-filter: blur(10px);
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 2rem;
font-size: 1rem;
color: var(--text-primary);
animation: float 3s ease-in-out infinite;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
@keyframes float {
@ -112,118 +150,72 @@ section {
margin-bottom: 3rem;
text-align: center;
color: var(--text-primary);
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
/* Services Section */
.services {
/* Transparent to let particles show through slightly or keep dark */
background: var(--bg-dark);
}
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 2rem;
perspective: 1000px; /* Global perspective for grid items entrance */
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
/* 3D Card System */
.service-card-wrapper {
/* This is the element that receives mouse events and defines the space */
position: relative;
height: 100%;
min-height: 200px;
.service-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.3s ease;
text-decoration: none;
color: inherit;
display: block;
perspective: 1500px; /* Perspective for the 3D tilt */
cursor: pointer;
overflow: hidden;
word-wrap: break-word;
}
.service-card-content {
position: relative;
height: 100%;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 1rem;
padding: 2rem;
/* Glassmorphism */
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
/* 3D Transform settings */
transform-style: preserve-3d;
transform: rotateX(var(--rotate-x, 0deg)) rotateY(var(--rotate-y, 0deg));
/* Smooth return when mouse leaves */
transition: transform 0.1s cubic-bezier(0.2, 0.4, 0.6, 1), border-color 0.3s ease, box-shadow 0.3s ease;
}
.service-card-wrapper:hover .service-card-content {
.service-card:hover {
background: var(--bg-card-hover);
border-color: var(--accent);
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.4),
0 0 20px rgba(59, 130, 246, 0.2),
inset 0 0 0 1px rgba(59, 130, 246, 0.5);
transform: translateY(-4px);
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.2);
}
/* Glare Effect */
.card-glare {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 1rem;
background: radial-gradient(
circle at var(--glare-x, 50%) var(--glare-y, 50%),
rgba(255, 255, 255, 0.15) 0%,
transparent 60%
);
opacity: var(--glare-opacity, 0);
pointer-events: none;
transition: opacity 0.3s ease;
mix-blend-mode: overlay;
z-index: 10;
}
/* Floating Content inside Card */
.service-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
margin-bottom: 0.75rem;
gap: 0.75rem;
transform: translateZ(25px); /* Pop out */
transform-style: preserve-3d;
}
.service-card-content h3 {
font-size: 1.5rem;
.service-card h3 {
font-size: 1.25rem;
color: var(--text-primary);
font-weight: 700;
flex: 1;
word-break: break-word;
min-width: 0;
}
.status-badge {
font-size: 1.2rem;
transform: translateZ(35px); /* Pop out more */
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
font-size: 1.5rem;
flex-shrink: 0;
margin-left: 0.5rem;
}
.service-description {
color: var(--text-secondary);
font-size: 1rem;
font-size: 0.95rem;
line-height: 1.6;
transform: translateZ(15px); /* Pop out slightly */
word-break: break-word;
overflow-wrap: break-word;
}
/* Tech Section */
.tech {
background: linear-gradient(180deg, rgba(10, 14, 23, 0) 0%, var(--bg-dark) 100%);
position: relative;
background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-card) 100%);
}
.tech-grid {
@ -235,17 +227,6 @@ section {
.tech-item {
text-align: center;
padding: 2rem 1rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 1rem;
transition: all 0.3s ease;
border: 1px solid transparent;
}
.tech-item:hover {
transform: translateY(-5px) scale(1.02);
background: rgba(255, 255, 255, 0.05);
border-color: var(--border);
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
}
.tech-item h3 {
@ -265,8 +246,6 @@ section {
border-top: 1px solid var(--border);
padding: 3rem 0;
text-align: center;
position: relative;
z-index: 2;
}
.footer p {
@ -304,19 +283,174 @@ section {
padding: 0 1.5rem;
}
.hero-title {
font-size: 3rem;
section {
padding: 3rem 0;
}
.services-grid,
.tech-grid {
grid-template-columns: 1fr;
}
.hero {
min-height: 80vh;
}
.footer-links {
flex-direction: column;
gap: 1rem;
}
}
/* Scroll Animations */
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Selection color */
::selection {
background: var(--accent);
color: white;
}
::-moz-selection {
background: var(--accent);
color: white;
}
/* Scroll-triggered animations */
[data-animate] {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.8s cubic-bezier(0.2, 0.8, 0.2, 1), transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
[data-animate].animate-in {
opacity: 1;
transform: translateY(0);
}
/* Parallax effect on hero */
.hero::before {
animation: parallax-float 20s ease-in-out infinite;
}
@keyframes parallax-float {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-20px) scale(1.05); }
}
/* Animated gradient text */
.hero-title {
background-size: 200% auto;
animation: gradient-shift 3s ease-in-out infinite;
}
@keyframes gradient-shift {
0%, 100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
/* Hover micro-interactions */
.service-card {
transform-origin: center;
will-change: transform;
}
.service-card:hover {
animation: subtle-bounce 0.6s ease;
}
@keyframes subtle-bounce {
0%, 100% { transform: translateY(-4px); }
50% { transform: translateY(-8px); }
}
.tech-item {
transition: all 0.3s ease;
}
.tech-item:hover {
transform: scale(1.05);
}
.tech-item:hover h3 {
animation: pulse-glow 1.5s ease-in-out infinite;
}
@keyframes pulse-glow {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; text-shadow: 0 0 20px rgba(59, 130, 246, 0.5); }
}
/* Footer links animation */
.footer-links a {
position: relative;
overflow: hidden;
}
.footer-links a::before {
content: '';
position: absolute;
bottom: 0;
left: -100%;
width: 100%;
height: 2px;
background: var(--accent);
transition: left 0.3s ease;
}
.footer-links a:hover::before {
left: 0;
}
/* Hero badge pulse */
.hero-badge {
animation: float 3s ease-in-out infinite, glow-pulse 2s ease-in-out infinite;
}
@keyframes glow-pulse {
0%, 100% { box-shadow: 0 0 10px rgba(59, 130, 246, 0.2); }
50% { box-shadow: 0 0 20px rgba(59, 130, 246, 0.4); }
}
/* Status badge animations */
.status-badge {
animation: badge-pulse 2s ease-in-out infinite;
}
@keyframes badge-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
/* Section title entrance */
.section-title {
animation: slide-in-top 0.8s ease-out;
}
@keyframes slide-in-top {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Reduce motion for accessibility */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}