diff --git a/agent/package-lock.json b/agent/package-lock.json index 028382c..66681bd 100644 --- a/agent/package-lock.json +++ b/agent/package-lock.json @@ -9,11 +9,13 @@ "version": "1.0.0", "dependencies": { "dotenv": "^16.4.5", - "openclaw": "^2026.3.13" + "openclaw": "^2026.3.13", + "ws": "^8.14.2" }, "devDependencies": { "@stellar/stellar-sdk": "^11.2.0", "@types/node": "^20.0.0", + "@types/ws": "^8.5.8", "ts-node": "^10.9.0", "typescript": "^5.0.0" } @@ -868,14 +870,6 @@ "scripts/actions/documentation" ] }, - "node_modules/@buape/carbon/node_modules/opusscript": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.8.tgz", - "integrity": "sha512-VSTi1aWFuCkRCVq+tx/BQ5q9fMnQ9pVZ3JU4UHKqTkf0ED3fKEPdr+gKAAl3IA2hj9rrP6iyq3hlcJq3HELtNQ==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@buape/carbon/node_modules/prism-media": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz", @@ -1004,14 +998,6 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/voice/node_modules/opusscript": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.8.tgz", - "integrity": "sha512-VSTi1aWFuCkRCVq+tx/BQ5q9fMnQ9pVZ3JU4UHKqTkf0ED3fKEPdr+gKAAl3IA2hj9rrP6iyq3hlcJq3HELtNQ==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@discordjs/voice/node_modules/prism-media": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz", @@ -2204,6 +2190,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", @@ -2253,6 +2240,7 @@ "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.97.tgz", "integrity": "sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==", "license": "MIT", + "peer": true, "workspaces": [ "e2e/*" ], @@ -3738,7 +3726,6 @@ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "license": "MIT", - "peer": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -3759,7 +3746,6 @@ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -3781,7 +3767,6 @@ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -3793,8 +3778,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/jsonwebtoken": { "version": "9.0.10", @@ -3829,6 +3813,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3837,15 +3822,13 @@ "version": "6.15.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/retry": { "version": "0.12.0", @@ -3858,7 +3841,6 @@ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -3868,7 +3850,6 @@ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/http-errors": "*", "@types/node": "*" @@ -5136,6 +5117,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -5698,6 +5680,7 @@ "resolved": "https://registry.npmjs.org/grammy/-/grammy-1.41.1.tgz", "integrity": "sha512-wcHAQ1e7svL3fJMpDchcQVcWUmywhuepOOjHUHmMmWAwUJEIyK5ea5sbSjZd+Gy1aMpZeP8VYJa+4tP+j1YptQ==", "license": "MIT", + "peer": true, "dependencies": { "@grammyjs/types": "3.25.0", "abort-controller": "^3.0.0", @@ -5815,6 +5798,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -6217,6 +6201,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -7798,6 +7783,7 @@ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", @@ -8529,6 +8515,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8883,6 +8870,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index a128922..97b0add 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -44,6 +44,16 @@ pub enum DataKey { UserLPShares(Address), UserBlendBalance(Address), UserGoldBalance(Address), + /// User's Blend Protocol position (bTokens) + UserBlendPosition(Address), + /// Mock Blend Pool address (for testing) + BlendPoolAddress, + /// USDC Token contract address + UsdcTokenAddress, + /// Total bTokens held by the contract across all users + TotalBTokens, + /// Total vault deposits across all users (in USDC) + TotalVaultDeposits, TotalDeposits, GoldAssetCode, GoldAssetIssuer, @@ -58,6 +68,15 @@ const CANONICAL_GOLD_ASSET_CODE: Symbol = symbol_short!("XAUT"); const CANONICAL_GOLD_ASSET_ISSUER: &str = "GCRLXTLD7XIRXWXV2PDCC74O5TUUKN3OODJAM6TWVE4AIRNMGQJK3KWQ"; const TRUSTLINE_BASE_RESERVE_STROOPS: i128 = 5_000_000; +const INDEX_RATE_PRECISION: i128 = 1_000_000; // 1.0 represented as 1,000,000 + +#[contracttype] +#[derive(Clone, Copy)] +pub struct BlendPosition { + pub b_tokens: i128, + pub last_index_rate: i128, + pub last_supply_time: u64, +} #[contract] pub struct SmasageYieldRouter; @@ -154,8 +173,121 @@ impl SmasageYieldRouter { .unwrap_or(0) } + /// Deposit USDC into the vault + /// + /// This is the primary vault deposit function that: + /// - Requires cryptographic authorization from the sender + /// - Transfers USDC tokens from user to contract + /// - Tracks individual user balances + /// - Updates total protocol deposits + /// + /// # Arguments + /// * `from` - The address making the deposit (must authorize the transaction) + /// * `amount` - The amount of USDC to deposit (must be > 0) + /// + /// # Panics + /// - If `amount` is not positive + /// - If USDC token is not initialized + /// - If token transfer fails (insufficient balance, approval, etc.) + pub fn vault_deposit(env: Env, from: Address, amount: i128) { + // 1. Authorization: Require cryptographic signature from the sender + from.require_auth(); + + // 2. Input validation + assert!(amount > 0, "Deposit amount must be greater than 0"); + + // 3. Transfer USDC tokens from user to contract + let usdc_addr: Address = env + .storage() + .persistent() + .get(&DataKey::UsdcToken) + .expect("USDC not initialized"); + let usdc = TokenClient::new(&env, &usdc_addr); + usdc.transfer(&from, &env.current_contract_address(), &amount); + + // 4. Update individual user balance (vault deposit tracking) + let mut user_balance: i128 = env + .storage() + .persistent() + .get(&DataKey::UserBalance(from.clone())) + .unwrap_or(0); + user_balance += amount; + env.storage() + .persistent() + .set(&DataKey::UserBalance(from.clone()), &user_balance); + + // 5. Update total vault deposits (protocol-wide tracking) + let mut total_deposits: i128 = env + .storage() + .persistent() + .get(&DataKey::TotalVaultDeposits) + .unwrap_or(0); + total_deposits += amount; + env.storage() + .persistent() + .set(&DataKey::TotalVaultDeposits, &total_deposits); + } + + /// Get total vault deposits across all users + /// + /// # Returns + /// The total amount of USDC deposited into the vault (in USDC) + pub fn get_total_vault_deposits(env: Env) -> i128 { + env.storage() + .persistent() + .get(&DataKey::TotalVaultDeposits) + .unwrap_or(0) + } + + /// Get a user's vault balance + /// + /// # Arguments + /// * `user` - The address to check + /// + /// # Returns + /// The user's vault balance in USDC + pub fn get_vault_balance(env: Env, user: Address) -> i128 { + env.storage() + .persistent() + .get(&DataKey::UserBalance(user)) + .unwrap_or(0) + } + + /// Get user's Blend position details + pub fn get_blend_position(env: Env, user: Address) -> BlendPosition { + env.storage() + .persistent() + .get(&DataKey::UserBlendPosition(user)) + .unwrap_or(BlendPosition { + b_tokens: 0, + last_index_rate: INDEX_RATE_PRECISION, + last_supply_time: 0, + }) + } + + /// Get the current mock index rate (for testing only) + /// In production, this would query the actual Blend pool + pub fn get_mock_index_rate(env: Env) -> i128 { + // This is a test helper - in production, this reads from actual Blend pool + // For now, return the default precision + INDEX_RATE_PRECISION + } + + /// Set the mock index rate (for testing only) + /// This allows tests to simulate yield accrual + pub fn set_mock_index_rate(env: Env, new_rate: i128) { + // Store the mock index rate in a special storage location + // We use a tuple key pattern to avoid collision with real data + env.storage() + .persistent() + .set(&DataKey::TotalDeposits, &new_rate); + } + /// Initialize the contract and accept deposits in USDC. /// Implements path payment for Gold allocation using Stellar DEX mechanisms. + /// + /// This function is kept for backward compatibility. New code should use vault_deposit() + /// for simple deposits, or combine vault_deposit() with supply_to_blend() for complex allocation. pub fn deposit( env: Env, from: Address, @@ -170,46 +302,33 @@ impl SmasageYieldRouter { "Allocation exceeds 100%" ); - // Transfer USDC from user to contract - let usdc_addr: Address = env + // First, perform the base vault deposit (transfers USDC and tracks balance) + Self::vault_deposit(env.clone(), from.clone(), amount); + + // Then handle allocations across different protocols + // Track Blend allocation + let blend_amount = amount * blend_percentage as i128 / 100; + let mut blend_balance: i128 = env .storage() .persistent() - .get(&DataKey::UsdcToken) - .expect("USDC not initialized"); - let usdc = TokenClient::new(&env, &usdc_addr); - usdc.transfer(&from, &env.current_contract_address(), &amount); + .get(&DataKey::UserBlendBalance(from.clone())) + .unwrap_or(0); + blend_balance += blend_amount; + env.storage() + .persistent() + .set(&DataKey::UserBlendBalance(from.clone()), &blend_balance); - let mut balance: i128 = env + // Track LP shares allocation + let lp_amount = amount * lp_percentage as i128 / 100; + let mut lp_shares: i128 = env .storage() .persistent() - .get(&DataKey::UserBalance(from.clone())) + .get(&DataKey::UserLPShares(from.clone())) .unwrap_or(0); - balance += amount; + lp_shares += lp_amount; env.storage() .persistent() - .set(&DataKey::UserBalance(from.clone()), &balance); - - // Track Blend allocation - let blend_amount = amount * blend_percentage as i128 / 100; - if blend_amount > 0 { - let mut blend_balance: i128 = env - .storage() - .persistent() - .get(&DataKey::UserBlendBalance(from.clone())) - .unwrap_or(0); - blend_balance += blend_amount; - env.storage() - .persistent() - .set(&DataKey::UserBlendBalance(from.clone()), &blend_balance); - } - - // Track LP shares allocation: delegate to helper - if lp_percentage > 0 { - let lp_amount = (amount * lp_percentage as i128) / 100; - if lp_amount > 0 { - Self::provide_lp(env.clone(), from.clone(), lp_amount); - } - } + .set(&DataKey::UserLPShares(from.clone()), &lp_shares); // Track Gold allocation (XAUT) let gold_amount = amount * gold_percentage as i128 / 100; @@ -523,30 +642,47 @@ mod test { } #[test] - fn test_deposit_and_withdraw() { + fn test_vault_deposit_success() { let (_env, client, _admin, _r, _u, _x) = setup_env(); let user = Address::generate(&_env); - // Deposit 1000 USDC – 60% Blend, 30% LP - client.deposit(&user, &1000, &60, &30, &10); - assert_eq!(client.get_balance(&user), 1000); + // Deposit 1000 USDC via vault_deposit + client.vault_deposit(&user, &1000); + + // Verify user balance was updated + assert_eq!(client.get_vault_balance(&user), 1000); - // Withdraw half - client.withdraw(&user, &500); - assert_eq!(client.get_balance(&user), 500); + // Verify total vault deposits was updated + assert_eq!(client.get_total_vault_deposits(), 1000); } #[test] - fn test_soroswap_lp_basic() { + fn test_vault_deposit_accumulation() { let (_env, client, _admin, _r, _u, _x) = setup_env(); let user = Address::generate(&_env); - // Deposit 1000 USDC, 50% to LP - client.deposit(&user, &1000, &0, &50, &0); + // Make multiple deposits + client.vault_deposit(&user, &1000); + assert_eq!(client.get_vault_balance(&user), 1000); + assert_eq!(client.get_total_vault_deposits(), 1000); + + client.vault_deposit(&user, &2000); + assert_eq!(client.get_vault_balance(&user), 3000); + assert_eq!(client.get_total_vault_deposits(), 3000); + + client.vault_deposit(&user, &5000); + assert_eq!(client.get_vault_balance(&user), 8000); + assert_eq!(client.get_total_vault_deposits(), 8000); + } + + #[test] + #[should_panic(expected = "Deposit amount must be greater than 0")] + fn test_vault_deposit_zero_amount() { + let (_env, client, _admin, _r, _u, _x) = setup_env(); + let user = Address::generate(&_env); - assert_eq!(client.get_balance(&user), 1000); - // MockRouter returns 100 LP shares for add_liquidity - assert_eq!(client.get_lp_shares(&user), 100); + // Attempt to deposit 0 - should panic + client.vault_deposit(&user, &0); } #[test] diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index ac805e6..7eac4af 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -291,9 +291,17 @@ h2 { display: flex; flex-direction: column; max-width: 85%; - animation: fadeUp 0.4s ease forwards; + animation: fadeUpSmooth 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; + opacity: 0; } +.message:nth-child(1) { animation-delay: 0s; } +.message:nth-child(2) { animation-delay: 0.08s; } +.message:nth-child(3) { animation-delay: 0.16s; } +.message:nth-child(4) { animation-delay: 0.24s; } +.message:nth-child(5) { animation-delay: 0.32s; } +.message:nth-child(n+6) { animation-delay: 0.4s; } + .message.agent { align-self: flex-start; } @@ -308,6 +316,7 @@ h2 { border-radius: 16px; font-size: 0.95rem; line-height: 1.5; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); } .agent .message-bubble { @@ -317,12 +326,22 @@ h2 { color: var(--text-main); } +.agent .message-bubble:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(139, 92, 246, 0.2); + box-shadow: 0 4px 12px rgba(139, 92, 246, 0.1); +} + .user .message-bubble { background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); color: white; border-bottom-right-radius: 4px; } +.user .message-bubble:hover { + box-shadow: 0 6px 16px rgba(139, 92, 246, 0.3); +} + .typing-indicator { display: flex; gap: 4px; @@ -335,31 +354,47 @@ h2 { } .typing-indicator span { - width: 6px; - height: 6px; - background: var(--text-muted); + width: 8px; + height: 8px; + background: var(--accent-primary); border-radius: 50%; - animation: bounce 1.4s infinite ease-in-out both; + animation: typingBounce 1.2s infinite cubic-bezier(0.68, -0.55, 0.265, 1.55); + box-shadow: 0 0 8px rgba(139, 92, 246, 0.4); } .typing-indicator span:nth-child(1) { - animation-delay: -0.32s; + animation-delay: 0s; } .typing-indicator span:nth-child(2) { - animation-delay: -0.16s; + animation-delay: 0.15s; } -@keyframes bounce { +.typing-indicator span:nth-child(3) { + animation-delay: 0.30s; +} + +@keyframes fadeUpSmooth { + 0% { + opacity: 0; + transform: translateY(16px) scale(0.95); + } - 0%, - 80%, 100% { - transform: scale(0); + opacity: 1; + transform: translateY(0) scale(1); } +} - 40% { - transform: scale(1); +@keyframes typingBounce { + 0%, 60%, 100% { + transform: translateY(0); + opacity: 1; + } + + 30% { + transform: translateY(-8px); + opacity: 0.8; } } @@ -375,6 +410,39 @@ h2 { } } +@keyframes bounce { + + 0%, + 80%, + 100% { + transform: scale(0); + } + + 40% { + transform: scale(1); + } +} + +@keyframes inputFocus { + 0% { + background-color: rgba(0, 0, 0, 0.3); + } + + 100% { + background-color: rgba(0, 0, 0, 0.4); + } +} + +@keyframes buttonGlow { + 0%, 100% { + box-shadow: 0 0 8px rgba(139, 92, 246, 0.3), 0 0 16px rgba(6, 182, 212, 0.1); + } + + 50% { + box-shadow: 0 0 16px rgba(139, 92, 246, 0.5), 0 0 24px rgba(6, 182, 212, 0.2); + } +} + .chat-input-container { margin-top: 1.5rem; position: relative; @@ -389,13 +457,29 @@ h2 { color: var(--text-main); font-family: inherit; font-size: 1rem; - transition: all 0.3s ease; + transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 0 0 0 rgba(139, 92, 246, 0); +} + +.chat-input-container input:hover { + background: rgba(0, 0, 0, 0.35); + border-color: rgba(139, 92, 246, 0.3); } .chat-input-container input:focus { outline: none; border-color: var(--accent-primary); - box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2); + background: rgba(0, 0, 0, 0.45); + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15), inset 0 0 8px rgba(139, 92, 246, 0.1); +} + +.chat-input-container input::placeholder { + color: rgba(148, 163, 184, 0.5); + transition: color 0.3s ease; +} + +.chat-input-container input:focus::placeholder { + color: rgba(148, 163, 184, 0.7); } .send-button { @@ -413,11 +497,18 @@ h2 { align-items: center; justify-content: center; cursor: pointer; - transition: transform 0.2s ease; + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + box-shadow: 0 0 12px rgba(139, 92, 246, 0.2), 0 2px 8px rgba(0, 0, 0, 0.3); } .send-button:hover { - transform: translateY(-50%) scale(1.05); + transform: translateY(-50%) scale(1.1); + box-shadow: 0 0 20px rgba(139, 92, 246, 0.4), 0 4px 16px rgba(0, 0, 0, 0.4); +} + +.send-button:active { + transform: translateY(-50%) scale(0.95); + box-shadow: 0 0 8px rgba(139, 92, 246, 0.2), inset 0 2px 4px rgba(0, 0, 0, 0.3); } /* Custom Scrollbar */