Carl 22 ore fa
parent
commit
c96305b7da
7 ha cambiato i file con 469 aggiunte e 12 eliminazioni
  1. 72 0
      DEPLOY.md
  2. 3 0
      index.html
  3. 59 0
      install-certbot.sh
  4. 75 0
      nginx.conf.domain
  5. 47 0
      nginx.conf.example
  6. 126 12
      src/components/AiorzWebARLanding.tsx
  7. 87 0
      src/index.css

+ 72 - 0
DEPLOY.md

@@ -0,0 +1,72 @@
+# 部署指南
+scp -r dist/* [email protected]:/usr/share/nginx/aiorz-webar/
+## 构建静态资源
+
+项目已经构建完成,静态文件位于 `dist/` 目录。
+
+## Nginx 部署步骤
+
+### 1. 复制文件到服务器
+
+将 `dist/` 目录下的所有文件复制到服务器的目标目录,例如:
+
+```bash
+# 在服务器上创建目录
+sudo mkdir -p /var/www/aiorz-webar
+
+# 复制文件(从本地执行)
+scp -r dist/* user@your-server:/var/www/aiorz-webar/
+```
+
+### 2. 配置 Nginx
+
+编辑或创建 Nginx 配置文件:
+
+```bash
+sudo nano /etc/nginx/sites-available/aiorz-webar
+```
+
+使用项目根目录下的 `nginx.conf.example` 作为参考,修改以下内容:
+- `server_name`: 您的域名或 IP 地址
+- `root`: dist 目录的实际路径(例如 `/var/www/aiorz-webar`)
+
+### 3. 启用站点
+
+```bash
+# 创建符号链接
+sudo ln -s /etc/nginx/sites-available/aiorz-webar /etc/nginx/sites-enabled/
+
+# 测试配置
+sudo nginx -t
+
+# 重启 Nginx
+sudo systemctl restart nginx
+```
+
+### 4. 设置权限
+
+确保 Nginx 可以读取文件:
+
+```bash
+sudo chown -R www-data:www-data /var/www/aiorz-webar
+sudo chmod -R 755 /var/www/aiorz-webar
+```
+
+## HTTPS 配置(可选但推荐)
+
+使用 Let's Encrypt 配置 HTTPS:
+
+```bash
+sudo apt install certbot python3-certbot-nginx
+sudo certbot --nginx -d your-domain.com
+```
+
+## 验证部署
+
+访问您的域名或 IP 地址,应该能看到 AIORZ WebAR 页面正常运行。
+
+## 注意事项
+
+1. **外部资源**: 项目使用了 CDN 加载 Three.js 和 MediaPipe,确保服务器可以访问这些外部资源
+2. **摄像头权限**: 如果使用摄像头功能,需要 HTTPS 连接(localhost 除外)
+3. **防火墙**: 确保防火墙允许 80 和 443 端口

+ 3 - 0
index.html

@@ -5,6 +5,9 @@
     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>AIORZ WebAR</title>
+    <!-- 预连接到 CDN,提前建立连接以加速资源加载 -->
+    <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
+    <link rel="dns-prefetch" href="https://cdn.jsdelivr.net" />
   </head>
   <body>
     <div id="root"></div>

+ 59 - 0
install-certbot.sh

@@ -0,0 +1,59 @@
+#!/bin/bash
+
+# Certbot 自动安装脚本
+# 支持 Debian/Ubuntu, CentOS/RHEL, Fedora
+
+echo "=== 检测系统类型 ==="
+if [ -f /etc/os-release ]; then
+    . /etc/os-release
+    echo "系统: $NAME"
+    echo "版本: $VERSION"
+else
+    echo "无法检测系统类型"
+    exit 1
+fi
+
+echo ""
+echo "=== 开始安装 Certbot ==="
+
+# 检测包管理器并安装
+if command -v apt &> /dev/null; then
+    echo "检测到 apt (Debian/Ubuntu)"
+    sudo apt update
+    sudo apt install certbot python3-certbot-nginx -y
+    
+elif command -v yum &> /dev/null; then
+    echo "检测到 yum (CentOS/RHEL)"
+    # 安装 EPEL 仓库
+    sudo yum install epel-release -y
+    sudo yum install certbot python3-certbot-nginx -y
+    
+elif command -v dnf &> /dev/null; then
+    echo "检测到 dnf (Fedora/CentOS 8+)"
+    sudo dnf install epel-release -y
+    sudo dnf install certbot python3-certbot-nginx -y
+    
+else
+    echo "未检测到支持的包管理器"
+    echo "尝试使用 pip 安装..."
+    if command -v pip3 &> /dev/null; then
+        sudo pip3 install certbot certbot-nginx
+    else
+        echo "错误: 无法安装 certbot"
+        exit 1
+    fi
+fi
+
+echo ""
+echo "=== 验证安装 ==="
+if command -v certbot &> /dev/null; then
+    certbot --version
+    echo ""
+    echo "✅ Certbot 安装成功!"
+    echo ""
+    echo "下一步: 运行以下命令获取 SSL 证书"
+    echo "sudo certbot --nginx -d aiorz.com -d www.aiorz.com"
+else
+    echo "❌ Certbot 安装失败"
+    exit 1
+fi

+ 75 - 0
nginx.conf.domain

@@ -0,0 +1,75 @@
+# AIORZ WebAR - 域名版本配置 (aiorz.com)
+# 使用 Let's Encrypt SSL 证书
+
+# HTTPS 服务器配置
+server {
+    listen 443 ssl http2;
+    server_name aiorz.com www.aiorz.com;
+    
+    # SSL 证书配置(Let's Encrypt)
+    ssl_certificate /etc/letsencrypt/live/aiorz.com/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/live/aiorz.com/privkey.pem;
+    
+    # SSL 优化配置
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK;
+    ssl_prefer_server_ciphers on;
+    ssl_session_cache shared:SSL:10m;
+    ssl_session_timeout 10m;
+    ssl_stapling on;
+    ssl_stapling_verify on;
+    
+    # 网站根目录
+    root /usr/share/nginx/aiorz-webar/;
+    index index.html;
+
+    # Gzip 压缩
+    gzip on;
+    gzip_vary on;
+    gzip_min_length 1024;
+    gzip_comp_level 6;
+    gzip_types text/plain text/css text/xml text/javascript 
+               application/x-javascript application/xml+rss 
+               application/javascript application/json 
+               application/xml application/rss+xml 
+               image/svg+xml;
+
+    # 静态资源缓存
+    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot|webp)$ {
+        expires 1y;
+        add_header Cache-Control "public, immutable";
+        access_log off;
+    }
+
+    # SPA 路由支持 - 所有路由都返回 index.html
+    location / {
+        try_files $uri $uri/ /index.html;
+    }
+
+    # 安全头
+    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+    add_header X-Frame-Options "SAMEORIGIN" always;
+    add_header X-Content-Type-Options "nosniff" always;
+    add_header X-XSS-Protection "1; mode=block" always;
+    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+
+    # 日志
+    access_log /var/log/nginx/aiorz-webar-access.log;
+    error_log /var/log/nginx/aiorz-webar-error.log;
+}
+
+# HTTP 重定向到 HTTPS
+server {
+    listen 80;
+    server_name aiorz.com www.aiorz.com;
+    
+    # Let's Encrypt 验证路径(Certbot 会自动配置)
+    location /.well-known/acme-challenge/ {
+        root /var/www/certbot;
+    }
+    
+    # 其他所有请求重定向到 HTTPS
+    location / {
+        return 301 https://$server_name$request_uri;
+    }
+}

+ 47 - 0
nginx.conf.example

@@ -0,0 +1,47 @@
+# HTTPS 服务器配置(使用域名)
+server {
+    listen 443 ssl http2;
+    server_name aiorz.com www.aiorz.com;
+    
+    # SSL 证书配置(Let's Encrypt)
+    ssl_certificate /etc/letsencrypt/live/aiorz.com/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/live/aiorz.com/privkey.pem;
+    
+    # SSL 优化配置
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers HIGH:!aNULL:!MD5;
+    ssl_prefer_server_ciphers on;
+    ssl_session_cache shared:SSL:10m;
+    ssl_session_timeout 10m;
+    
+    root /usr/share/nginx/aiorz-webar/;
+    index index.html;
+
+    gzip on;
+    gzip_vary on;
+    gzip_min_length 1024;
+    gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json;
+
+    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
+        expires 1y;
+        add_header Cache-Control "public, immutable";
+    }
+
+    location / {
+        try_files $uri $uri/ /index.html;
+    }
+
+    add_header X-Frame-Options "SAMEORIGIN" always;
+    add_header X-Content-Type-Options "nosniff" always;
+    add_header X-XSS-Protection "1; mode=block" always;
+
+    access_log /var/log/nginx/aiorz-webar-access.log;
+    error_log /var/log/nginx/aiorz-webar-error.log;
+}
+
+# HTTP 重定向到 HTTPS
+server {
+    listen 80;
+    server_name aiorz.com www.aiorz.com;
+    return 301 https://$server_name$request_uri;
+}

+ 126 - 12
src/components/AiorzWebARLanding.tsx

@@ -23,6 +23,7 @@ const AiorzWebARLanding = () => {
   const [cameraActive, setCameraActive] = useState(false);
   const [systemStatus, setSystemStatus] = useState("SYSTEM_INIT");
   const [gestureMode, setGestureMode] = useState("NEUTRAL");
+  const [latency, setLatency] = useState<number>(0);
   
   // Refs for WebGL and MediaPipe
   const videoRef = useRef<HTMLVideoElement>(null);
@@ -35,6 +36,8 @@ const AiorzWebARLanding = () => {
   const particlesRef = useRef<any>(null);
   const handsRef = useRef<any>(null);
   const cameraUtilsRef = useRef<any>(null);
+  const frameTimestampRef = useRef<number>(0);
+  const latencySamplesRef = useRef<number[]>([]); // 存储延迟样本用于计算平均值
   
   // Physics State Refs
   const particlesDataRef = useRef<{
@@ -114,6 +117,38 @@ const AiorzWebARLanding = () => {
 
   // ==================== 1. Initialization ====================
   useEffect(() => {
+    // 提前预加载 MediaPipe 资源文件(在脚本加载前就开始)
+    const preloadMediaPipeResources = async () => {
+      const baseUrl = 'https://cdn.jsdelivr.net/npm/@mediapipe/hands/';
+      const resources = [
+        'hands_solution_packed_assets.data',
+        'hands_solution_simd_wasm_bin.wasm'
+      ];
+      
+      try {
+        // 使用 fetch 预加载资源文件
+        const preloadPromises = resources.map(resource => {
+          return fetch(`${baseUrl}${resource}`, { 
+            method: 'HEAD',
+            cache: 'force-cache'
+          }).catch(() => {
+            // 如果 HEAD 请求失败,尝试 GET 请求
+            return fetch(`${baseUrl}${resource}`, { 
+              cache: 'force-cache'
+            });
+          });
+        });
+        
+        await Promise.all(preloadPromises);
+        console.log('[MediaPipe] 资源文件预加载完成');
+      } catch (err) {
+        console.warn('[MediaPipe] 资源文件预加载失败:', err);
+      }
+    };
+    
+    // 立即开始预加载资源(不等待脚本加载)
+    preloadMediaPipeResources();
+    
     const initSystem = async () => {
       try {
         setSystemStatus("LOADING_MODULES");
@@ -124,7 +159,7 @@ const AiorzWebARLanding = () => {
         await loadScript('https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js');
 
         initThreeJS();
-        initMediaPipe();
+        await initMediaPipe();
         
         setLoading(false);
         setSystemStatus("READY_TO_LINK");
@@ -143,6 +178,33 @@ const AiorzWebARLanding = () => {
     };
   }, []);
 
+  // ==================== 延迟平均值计算 ====================
+  useEffect(() => {
+    if (!cameraActive) {
+      // 摄像头未激活时,清空样本数组并重置延迟显示
+      latencySamplesRef.current = [];
+      setLatency(0);
+      return;
+    }
+
+    // 每秒计算一次平均值并更新延迟显示
+    const interval = setInterval(() => {
+      const samples = latencySamplesRef.current;
+      if (samples.length > 0) {
+        // 计算平均值
+        const sum = samples.reduce((acc, val) => acc + val, 0);
+        const average = Math.round(sum / samples.length);
+        setLatency(average);
+        // 清空样本数组,准备下一秒的数据收集
+        latencySamplesRef.current = [];
+      }
+    }, 1000); // 每秒更新一次
+
+    return () => {
+      clearInterval(interval);
+    };
+  }, [cameraActive]);
+
   // ==================== 2. Three.js Setup ====================
   const initThreeJS = () => {
     const THREE = (window as any).THREE;
@@ -655,7 +717,9 @@ const AiorzWebARLanding = () => {
       }
     }
 
-    setGestureMode(key.toUpperCase());
+    // 当手势是 'text' 时,显示 'AI ORZ' 而不是 'TEXT'
+    const displayText = key === 'text' ? 'AI ORZ' : key.toUpperCase();
+    setGestureMode(displayText);
   };
 
   // ==================== 4. Physics Loop ====================
@@ -899,8 +963,9 @@ const AiorzWebARLanding = () => {
   };
 
   // ==================== 5. MediaPipe Logic ====================
-  const initMediaPipe = () => {
+  const initMediaPipe = async () => {
     const Hands = (window as any).Hands;
+    
     const hands = new Hands({locateFile: (file: string) => {
       return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`;
     }});
@@ -914,10 +979,48 @@ const AiorzWebARLanding = () => {
 
     hands.onResults(onResults);
     handsRef.current = hands;
+
+    // 通过发送测试图像来强制 MediaPipe 初始化并加载资源
+    // 由于资源文件已经在初始化阶段预加载,这里主要是触发 MediaPipe 的初始化
+    try {
+      const preloadCanvas = document.createElement('canvas');
+      preloadCanvas.width = 640;
+      preloadCanvas.height = 480;
+      const ctx = preloadCanvas.getContext('2d');
+      if (ctx) {
+        ctx.fillStyle = 'black';
+        ctx.fillRect(0, 0, 640, 480);
+        
+        // 发送一个测试图像来触发 MediaPipe 初始化
+        // 这会强制 MediaPipe 加载并初始化所有必要的资源
+        // 由于资源文件已经预加载,这里应该会更快完成
+        await hands.send({ image: preloadCanvas as any });
+        console.log('[MediaPipe] MediaPipe 实例初始化完成,资源已就绪');
+      }
+    } catch (err) {
+      console.warn('[MediaPipe] MediaPipe 初始化失败,将在首次使用时加载:', err);
+    }
   };
 
   const startCameraSystem = async () => {
     if (cameraActive) return;
+    
+    // 检查是否支持 getUserMedia
+    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
+      const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
+      const isSecure = window.location.protocol === 'https:';
+      
+      if (!isLocalhost && !isSecure) {
+        setSystemStatus("HTTPS_REQUIRED");
+        alert('Camera access requires HTTPS connection. Please use https:// to access this page, or configure an SSL certificate.');
+        return;
+      }
+      
+      setSystemStatus("CAMERA_NOT_SUPPORTED");
+      alert('Your browser does not support camera access functionality.');
+      return;
+    }
+    
     setSystemStatus("INITIALIZING_VISION_CORE");
     
     const Camera = (window as any).Camera;
@@ -925,6 +1028,8 @@ const AiorzWebARLanding = () => {
       onFrame: async () => {
         if (handsRef.current && videoRef.current && videoRef.current.readyState >= 2) {
           try {
+            // 记录发送帧的时间戳
+            frameTimestampRef.current = performance.now();
             await handsRef.current.send({image: videoRef.current});
           } catch (e) {}
         }
@@ -945,6 +1050,15 @@ const AiorzWebARLanding = () => {
   };
 
   const onResults = (results: any) => {
+    // 计算延迟:从发送帧到收到结果的耗时
+    if (frameTimestampRef.current > 0) {
+      const currentTime = performance.now();
+      const frameLatency = currentTime - frameTimestampRef.current;
+      // 将延迟值添加到样本数组中,而不是直接更新 state
+      latencySamplesRef.current.push(Math.round(frameLatency));
+      frameTimestampRef.current = 0; // 重置时间戳
+    }
+    
     if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
       const lm = results.multiHandLandmarks[0];
       handStateRef.current.landmarks = lm;
@@ -1004,7 +1118,7 @@ const AiorzWebARLanding = () => {
           <div className="w-8 h-8 bg-emerald-500/10 border border-emerald-500/50 rounded flex items-center justify-center group-hover:bg-emerald-500/20 transition-all duration-300">
             <Cpu size={18} className="text-emerald-400" />
           </div>
-          <span className="text-xl font-bold tracking-wider text-white group-hover:text-emerald-400 transition-colors">AIORZ</span>
+          <span className="text-xl font-bold tracking-wider text-white group-hover:text-emerald-400 transition-colors text-glow">AIORZ</span>
         </div>
         <div className="hidden md:flex gap-6 text-sm items-center">
           <div className="flex items-center gap-2 text-emerald-500/80">
@@ -1025,21 +1139,21 @@ const AiorzWebARLanding = () => {
       <main className="relative z-20 max-w-7xl mx-auto px-6 pt-16 h-[calc(100vh-100px)]">
         {/* 左侧主要内容区域 */}
         <div className="max-w-2xl space-y-8">
-          <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-emerald-900/30 border border-emerald-500/30 text-emerald-400 text-xs font-bold uppercase tracking-widest">
+          <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-emerald-900/30 border border-emerald-500/30 text-emerald-400 text-xs font-bold uppercase tracking-widest text-pulse-glow">
             <span className={`w-2 h-2 rounded-full ${cameraActive ? 'bg-emerald-400 animate-ping' : 'bg-gray-500'}`}></span>
             {cameraActive ? 'Neural Link Active' : 'System Standby'}
           </div>
           
           <h1 className="text-5xl md:text-7xl font-black text-white leading-tight">
-            Don't Kneel <br />
-            <span className="text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-cyan-500">
+            <span className="text-glow">Don't Kneel</span> <br />
+            <span className="gradient-animated">
               To Complexity.
             </span>
           </h1>
           
           <p className="text-lg text-gray-400 max-w-lg leading-relaxed border-l-2 border-emerald-500/50 pl-6">
-            The next-gen AI agent for cross-border commerce. <br/>
-            Unleash the power of <span className="text-emerald-400 font-bold">Spatial Computing</span>.
+            <span className="text-pulse-glow">We are on the verge of creating a great AI era.</span> <br/>
+            Unleash the power of <span className="text-shimmer font-bold">Spatial Computing</span>.
           </p>
 
           <div className="pt-4">
@@ -1058,7 +1172,7 @@ const AiorzWebARLanding = () => {
               <div className="flex gap-4">
                 <div className="p-4 bg-emerald-950/50 border border-emerald-500/30 rounded-lg backdrop-blur-sm">
                    <div className="text-xs text-gray-400 mb-1">CURRENT GESTURE</div>
-                   <div className="text-xl font-bold text-white tracking-widest">{gestureMode}</div>
+                   <div className="text-xl font-bold text-white tracking-widest text-glow">{gestureMode}</div>
                 </div>
               </div>
             )}
@@ -1092,7 +1206,7 @@ const AiorzWebARLanding = () => {
                 </div>
                 <div className="flex justify-between items-center">
                   <span className="text-gray-500">Latency:</span>
-                  <span className="text-cyan-400">16ms</span>
+                  <span className="text-cyan-400">{latency}ms</span>
                 </div>
               </div>
             </div>
@@ -1200,7 +1314,7 @@ const AiorzWebARLanding = () => {
                  </div>
                  <div className="mt-3 pt-2 border-t border-gray-700/50 text-[10px] text-gray-500 leading-tight">
                     &gt; tracking: {cameraActive ? <span className="text-green-500">ON</span> : <span className="text-red-500">OFF</span>} <br/>
-                    &gt; latency: 16ms
+                    &gt; latency: {latency}ms
                  </div>
               </div>
            </div>

+ 87 - 0
src/index.css

@@ -15,3 +15,90 @@ body {
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
 }
+
+/* 科幻文字特效 */
+@keyframes text-glow {
+  0%, 100% {
+    text-shadow: 
+      0 0 10px rgba(16, 185, 129, 0.5),
+      0 0 20px rgba(16, 185, 129, 0.3),
+      0 0 30px rgba(16, 185, 129, 0.2),
+      0 0 40px rgba(6, 182, 212, 0.1);
+  }
+  50% {
+    text-shadow: 
+      0 0 20px rgba(16, 185, 129, 0.8),
+      0 0 30px rgba(16, 185, 129, 0.6),
+      0 0 40px rgba(16, 185, 129, 0.4),
+      0 0 50px rgba(6, 182, 212, 0.3);
+  }
+}
+
+@keyframes shimmer {
+  0% {
+    background-position: -200% center;
+  }
+  100% {
+    background-position: 200% center;
+  }
+}
+
+@keyframes pulse-glow {
+  0%, 100% {
+    opacity: 1;
+    filter: brightness(1) drop-shadow(0 0 10px rgba(16, 185, 129, 0.5));
+  }
+  50% {
+    opacity: 0.9;
+    filter: brightness(1.2) drop-shadow(0 0 20px rgba(16, 185, 129, 0.8));
+  }
+}
+
+@keyframes gradient-shift {
+  0% {
+    background-position: 0% 50%;
+  }
+  50% {
+    background-position: 100% 50%;
+  }
+  100% {
+    background-position: 0% 50%;
+  }
+}
+
+.text-glow {
+  animation: text-glow 3s ease-in-out infinite;
+}
+
+.text-shimmer {
+  background: linear-gradient(
+    90deg,
+    rgba(16, 185, 129, 0.8) 0%,
+    rgba(6, 182, 212, 1) 50%,
+    rgba(16, 185, 129, 0.8) 100%
+  );
+  background-size: 200% auto;
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+  animation: shimmer 3s linear infinite;
+}
+
+.text-pulse-glow {
+  animation: pulse-glow 2s ease-in-out infinite;
+}
+
+.gradient-animated {
+  background: linear-gradient(
+    -45deg,
+    #10b981,
+    #06b6d4,
+    #10b981,
+    #34d399
+  );
+  background-size: 300% 300%;
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+  animation: gradient-shift 4s ease infinite;
+}