diff --git a/Cargo.toml b/Cargo.toml index 66cdbb1..4075c89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2.104" wasm-bindgen-futures = "0.4.54" +js-sys = "0.3.81" [dependencies.web-sys] version = "0.3.81" @@ -20,6 +21,12 @@ features = [ "Node", "Window", "Location", + "IntersectionObserver", + "IntersectionObserverEntry", + "IntersectionObserverInit", + "DomRect", + "NodeList", + "DomTokenList", ] [profile.release] diff --git a/dist/railwayka_landing.d.ts b/dist/railwayka_landing.d.ts index 66b17eb..7accea5 100644 --- a/dist/railwayka_landing.d.ts +++ b/dist/railwayka_landing.d.ts @@ -8,6 +8,10 @@ export interface InitOutput { readonly memory: WebAssembly.Memory; readonly main: () => void; readonly __wbindgen_export_0: (a: number) => void; + 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_start: () => void; } diff --git a/dist/railwayka_landing.js b/dist/railwayka_landing.js index 9c1168b..12d6721 100644 --- a/dist/railwayka_landing.js +++ b/dist/railwayka_landing.js @@ -6,29 +6,6 @@ heap.push(undefined, null, true, false); function getObject(idx) { return heap[idx]; } -let heap_next = heap.length; - -function addHeapObject(obj) { - if (heap_next === heap.length) heap.push(heap.length + 1); - const idx = heap_next; - heap_next = heap[idx]; - - heap[idx] = obj; - return idx; -} - -function handleError(f, args) { - try { - return f.apply(this, args); - } catch (e) { - wasm.__wbindgen_export_0(addHeapObject(e)); - } -} - -function isLikeNone(x) { - return x === undefined || x === null; -} - let cachedUint8ArrayMemory0 = null; function getUint8ArrayMemory0() { @@ -59,6 +36,92 @@ function getStringFromWasm0(ptr, len) { return decodeText(ptr, len); } +let heap_next = heap.length; + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_export_0(addHeapObject(e)); + } +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +let WASM_VECTOR_LEN = 0; + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + } +} + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +let cachedDataViewMemory0 = null; + +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + function dropObject(idx) { if (idx < 132) return; heap[idx] = heap_next; @@ -71,10 +134,48 @@ function takeObject(idx) { return ret; } +const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry( +state => { + wasm.__wbindgen_export_3.get(state.dtor)(state.a, state.b); +} +); + +function makeMutClosure(arg0, arg1, dtor, f) { + const state = { a: arg0, b: arg1, cnt: 1, dtor }; + const real = (...args) => { + + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + if (--state.cnt === 0) { + wasm.__wbindgen_export_3.get(state.dtor)(a, state.b); + CLOSURE_DTORS.unregister(state); + } else { + state.a = a; + } + } + }; + real.original = state; + CLOSURE_DTORS.register(real, state, state); + return real; +} + export function main() { wasm.main(); } +function __wbg_adapter_4(arg0, arg1, arg2, arg3) { + wasm.__wbindgen_export_4(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); +} + const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']); async function __wbg_load(module, imports) { @@ -113,6 +214,9 @@ async function __wbg_load(module, imports) { function __wbg_get_imports() { const imports = {}; imports.wbg = {}; + imports.wbg.__wbg_add_4e0283c00f7ecabe = function() { return handleError(function (arg0, arg1, arg2) { + getObject(arg0).add(getStringFromWasm0(arg1, arg2)); + }, arguments) }; imports.wbg.__wbg_appendChild_87a6cc0aeb132c06 = function() { return handleError(function (arg0, arg1) { const ret = getObject(arg0).appendChild(getObject(arg1)); return addHeapObject(ret); @@ -125,6 +229,10 @@ function __wbg_get_imports() { const ret = getObject(arg0).call(getObject(arg1)); return addHeapObject(ret); }, arguments) }; + imports.wbg.__wbg_classList_61149e0de7c668c5 = function(arg0) { + const ret = getObject(arg0).classList; + return addHeapObject(ret); + }; imports.wbg.__wbg_createElement_4909dfa2011f2abe = function() { return handleError(function (arg0, arg1, arg2) { const ret = getObject(arg0).createElement(getStringFromWasm0(arg1, arg2)); return addHeapObject(ret); @@ -133,6 +241,37 @@ function __wbg_get_imports() { const ret = getObject(arg0).document; return isLikeNone(ret) ? 0 : addHeapObject(ret); }; + imports.wbg.__wbg_getAttribute_8bfaf67e99ed2ee3 = function(arg0, arg1, arg2, arg3) { + const ret = getObject(arg1).getAttribute(getStringFromWasm0(arg2, arg3)); + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export_1, wasm.__wbindgen_export_2); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }; + imports.wbg.__wbg_get_0da715ceaecea5c8 = function(arg0, arg1) { + const ret = getObject(arg0)[arg1 >>> 0]; + return addHeapObject(ret); + }; + imports.wbg.__wbg_instanceof_Element_162e4334c7d6f450 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Element; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; + imports.wbg.__wbg_instanceof_IntersectionObserverEntry_819e56422a481344 = function(arg0) { + let result; + try { + result = getObject(arg0) instanceof IntersectionObserverEntry; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }; imports.wbg.__wbg_instanceof_Window_12d20d558ef92592 = function(arg0) { let result; try { @@ -143,13 +282,44 @@ function __wbg_get_imports() { const ret = result; return ret; }; + imports.wbg.__wbg_isIntersecting_31dfa252ee048a6f = function(arg0) { + const ret = getObject(arg0).isIntersecting; + return ret; + }; + imports.wbg.__wbg_item_e5c3452334bca83f = function(arg0, arg1) { + const ret = getObject(arg0).item(arg1 >>> 0); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }; + imports.wbg.__wbg_length_186546c51cd61acd = function(arg0) { + const ret = getObject(arg0).length; + return ret; + }; + imports.wbg.__wbg_length_e7f4a6e30ea139e7 = function(arg0) { + const ret = getObject(arg0).length; + return ret; + }; imports.wbg.__wbg_log_6c7b5f4f00b8ce3f = function(arg0) { console.log(getObject(arg0)); }; + imports.wbg.__wbg_new_19c25a3f2fa63a02 = function() { + const ret = new Object(); + return addHeapObject(ret); + }; imports.wbg.__wbg_newnoargs_254190557c45b4ec = function(arg0, arg1) { const ret = new Function(getStringFromWasm0(arg0, arg1)); return addHeapObject(ret); }; + imports.wbg.__wbg_newwithoptions_f6e0820321465a5c = function() { return handleError(function (arg0, arg1) { + const ret = new IntersectionObserver(getObject(arg0), getObject(arg1)); + return addHeapObject(ret); + }, arguments) }; + imports.wbg.__wbg_observe_d5620e0d99e20a09 = function(arg0, arg1) { + getObject(arg0).observe(getObject(arg1)); + }; + imports.wbg.__wbg_querySelectorAll_71b924f0e83d096b = function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).querySelectorAll(getStringFromWasm0(arg1, arg2)); + return addHeapObject(ret); + }, arguments) }; imports.wbg.__wbg_setAttribute_d1baf9023ad5696f = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); }, arguments) }; @@ -162,6 +332,9 @@ function __wbg_get_imports() { imports.wbg.__wbg_settextContent_b55fe2f5f1399466 = function(arg0, arg1, arg2) { getObject(arg0).textContent = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2); }; + imports.wbg.__wbg_setthreshold_7daca4126268ea47 = function(arg0, arg1) { + getObject(arg0).threshold = getObject(arg1); + }; imports.wbg.__wbg_static_accessor_GLOBAL_8921f820c2ce3f12 = function() { const ret = typeof global === 'undefined' ? null : global; return isLikeNone(ret) ? 0 : addHeapObject(ret); @@ -178,6 +351,19 @@ function __wbg_get_imports() { const ret = typeof window === 'undefined' ? null : window; return isLikeNone(ret) ? 0 : addHeapObject(ret); }; + imports.wbg.__wbg_target_161cb00cc3daf872 = function(arg0) { + const ret = getObject(arg0).target; + return addHeapObject(ret); + }; + imports.wbg.__wbg_wbindgencbdrop_eb10308566512b88 = function(arg0) { + const obj = getObject(arg0).original; + if (obj.cnt-- == 1) { + obj.a = 0; + return true; + } + const ret = false; + return ret; + }; imports.wbg.__wbg_wbindgenisundefined_c4b71d073b92f3c5 = function(arg0) { const ret = getObject(arg0) === undefined; return ret; @@ -193,6 +379,16 @@ function __wbg_get_imports() { const ret = getStringFromWasm0(arg0, arg1); return addHeapObject(ret); }; + imports.wbg.__wbindgen_cast_d6cd19b81560fd6e = function(arg0) { + // Cast intrinsic for `F64 -> Externref`. + const ret = arg0; + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_cast_fe9164a03cdb0b6b = function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { dtor_idx: 1, function: Function { arguments: [NamedExternref("Array"), NamedExternref("IntersectionObserver")], shim_idx: 2, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, 1, __wbg_adapter_4); + return addHeapObject(ret); + }; imports.wbg.__wbindgen_object_clone_ref = function(arg0) { const ret = getObject(arg0); return addHeapObject(ret); @@ -211,6 +407,7 @@ function __wbg_init_memory(imports, memory) { function __wbg_finalize_init(instance, module) { wasm = instance.exports; __wbg_init.__wbindgen_wasm_module = module; + cachedDataViewMemory0 = null; cachedUint8ArrayMemory0 = null; diff --git a/dist/railwayka_landing_bg.wasm b/dist/railwayka_landing_bg.wasm index 37de7d2..238df8f 100644 Binary files a/dist/railwayka_landing_bg.wasm and b/dist/railwayka_landing_bg.wasm differ diff --git a/dist/railwayka_landing_bg.wasm.d.ts b/dist/railwayka_landing_bg.wasm.d.ts index 13cde68..396db70 100644 --- a/dist/railwayka_landing_bg.wasm.d.ts +++ b/dist/railwayka_landing_bg.wasm.d.ts @@ -3,4 +3,8 @@ export const memory: WebAssembly.Memory; export const main: () => void; 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_start: () => void; diff --git a/dist/style.css b/dist/style.css index 0f8af89..3ac623f 100644 --- a/dist/style.css +++ b/dist/style.css @@ -159,6 +159,8 @@ section { text-decoration: none; color: inherit; display: block; + overflow: hidden; + word-wrap: break-word; } .service-card:hover { @@ -173,12 +175,15 @@ section { justify-content: space-between; align-items: flex-start; margin-bottom: 0.75rem; + gap: 0.75rem; } .service-card h3 { font-size: 1.25rem; color: var(--text-primary); flex: 1; + word-break: break-word; + min-width: 0; } .status-badge { @@ -191,6 +196,8 @@ section { color: var(--text-secondary); font-size: 0.95rem; line-height: 1.6; + word-break: break-word; + overflow-wrap: break-word; } /* Tech Section */ @@ -297,3 +304,140 @@ html { background: var(--accent); color: white; } + +/* Scroll-triggered animations */ +[data-animate] { + opacity: 0; + transform: translateY(30px); + 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; + } +} diff --git a/src/lib.rs b/src/lib.rs index 9658e58..fceac10 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ use wasm_bindgen::prelude::*; -use web_sys::{console, Document, Element, HtmlElement, Window}; +use wasm_bindgen::JsCast; +use web_sys::{console, Document, Element, HtmlElement, Window, IntersectionObserver, IntersectionObserverEntry, IntersectionObserverInit}; #[wasm_bindgen(start)] pub fn main() -> Result<(), JsValue> { @@ -12,6 +13,7 @@ pub fn main() -> Result<(), JsValue> { if let Some(body) = document.body() { body.set_inner_html(""); build_page(&document, &body)?; + setup_scroll_animations(&document)?; } console::log_1(&"✅ Landing page ready!".into()); @@ -83,12 +85,12 @@ fn create_services_section(document: &Document) -> Result { // Live services let services_live = vec![ ("Git Repository", "Gitea - Self-hosted Git server with web UI", "🔴", "https://git.dc.railwayka.ru"), - ("CI/CD", "Woodpecker CI - Automated builds and deployments", "🔴", "https://cicd.dc.railwayka.ru"), - ("Container Management", "Portainer - Docker management interface", "🔴", "https://port.dc.railwayka.ru"), ]; - for (name, desc, status, url) in services_live { + for (i, (name, desc, status, url)) in services_live.iter().enumerate() { let card = create_service_card(document, name, desc, status, Some(url))?; + card.set_attribute("data-animate", "fade-up")?; + card.set_attribute("data-delay", &format!("{}", i * 100))?; grid.append_child(&card)?; } @@ -101,8 +103,10 @@ fn create_services_section(document: &Document) -> Result { ("Monitoring", "Grafana + Prometheus - Infrastructure monitoring", "🔵"), ]; - for (name, desc, status) in services_planned { + for (i, (name, desc, status)) in services_planned.iter().enumerate() { let card = create_service_card(document, name, desc, status, None)?; + card.set_attribute("data-animate", "fade-up")?; + card.set_attribute("data-delay", &format!("{}", (i + services_live.len()) * 100))?; grid.append_child(&card)?; } @@ -177,8 +181,10 @@ fn create_tech_section(document: &Document) -> Result { ("Rust + WASM", "High-performance web applications"), ]; - for (name, desc) in technologies { + for (i, (name, desc)) in technologies.iter().enumerate() { let item = create_tech_item(document, name, desc)?; + item.set_attribute("data-animate", "fade-up")?; + item.set_attribute("data-delay", &format!("{}", i * 80))?; grid.append_child(&item)?; } @@ -238,3 +244,47 @@ fn create_footer(document: &Document) -> Result { Ok(footer) } + +fn setup_scroll_animations(document: &Document) -> Result<(), JsValue> { + // Create intersection observer callback + let callback = Closure::wrap(Box::new(move |entries: js_sys::Array, _observer: IntersectionObserver| { + for entry in entries.iter() { + if let Ok(entry) = entry.dyn_into::() { + if entry.is_intersecting() { + if let Some(target) = entry.target().dyn_ref::() { + let delay = target.get_attribute("data-delay") + .and_then(|d| d.parse::().ok()) + .unwrap_or(0); + + target.class_list().add_1("animate-in").ok(); + target.set_attribute("style", &format!("animation-delay: {}ms", delay)).ok(); + } + } + } + } + }) as Box); + + // Create observer with options + let mut options = IntersectionObserverInit::new(); + options.set_threshold(&JsValue::from_f64(0.1)); + + let observer = IntersectionObserver::new_with_options( + callback.as_ref().unchecked_ref(), + &options, + )?; + + // Observe all elements with data-animate attribute + let elements = document.query_selector_all("[data-animate]")?; + for i in 0..elements.length() { + if let Some(node) = elements.item(i) { + if let Some(element) = node.dyn_ref::() { + observer.observe(element); + } + } + } + + // Prevent callback from being dropped + callback.forget(); + + Ok(()) +} diff --git a/style.css b/style.css index 0f8af89..3ac623f 100644 --- a/style.css +++ b/style.css @@ -159,6 +159,8 @@ section { text-decoration: none; color: inherit; display: block; + overflow: hidden; + word-wrap: break-word; } .service-card:hover { @@ -173,12 +175,15 @@ section { justify-content: space-between; align-items: flex-start; margin-bottom: 0.75rem; + gap: 0.75rem; } .service-card h3 { font-size: 1.25rem; color: var(--text-primary); flex: 1; + word-break: break-word; + min-width: 0; } .status-badge { @@ -191,6 +196,8 @@ section { color: var(--text-secondary); font-size: 0.95rem; line-height: 1.6; + word-break: break-word; + overflow-wrap: break-word; } /* Tech Section */ @@ -297,3 +304,140 @@ html { background: var(--accent); color: white; } + +/* Scroll-triggered animations */ +[data-animate] { + opacity: 0; + transform: translateY(30px); + 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; + } +}