commit 6683b5963e1f559b74a7874040da4ccbf07733cd Author: ImpulsiveFPS Date: Sun Feb 1 16:15:02 2026 +0100 Initial commit: LemonLink landing page diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b9c4acf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +# Git +.git +.gitignore +.gitattributes + +# Documentation +README.md +*.md + +# IDE +.vscode +.idea +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Docker +Dockerfile +docker-compose.yml +.dockerignore + +# CI/CD +.github +.gitlab-ci.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89aa2c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +dist/ +build/ +.out/ + +# Environment files +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db +desktop.ini + +# Logs +logs/ +*.log + +# Testing +coverage/ +.nyc_output/ + +# Temporary files +*.tmp +*.temp +.cache/ + +# Don't ignore these for deployment +!docker-compose.yml +!nginx.conf +!Dockerfile diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f7cc6cb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,41 @@ +# Contributing to LemonLink + +Thank you for your interest in contributing! This is a personal homelab landing page project. + +## πŸš€ Quick Start + +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/my-feature` +3. Commit changes: `git commit -m 'Add my feature'` +4. Push to branch: `git push origin feature/my-feature` +5. Open a Pull Request + +## πŸ“ Guidelines + +- Keep changes minimal and focused +- Test on mobile and desktop +- Follow existing code style +- Update documentation if needed + +## πŸ› Bug Reports + +Open an issue with: +- Clear description +- Steps to reproduce +- Screenshots if applicable +- Browser/environment info + +## πŸ’‘ Feature Requests + +Open an issue describing: +- What you'd like to see +- Why it would be useful +- Any implementation ideas + +## 🎨 Design Changes + +For visual changes, please include before/after screenshots. + +--- + +Built with πŸ’› by the LemonLink team diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..310e51f --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,419 @@ +# πŸš€ LemonLink Deployment Guide + +Deploy your stunning landing page using Docker in Portainer or Proxmox! + +--- + +## πŸ“‹ Prerequisites + +- Docker VM running in Proxmox +- Portainer (CE or EE) installed and accessible +- (Optional) Reverse proxy (Traefik/Nginx Proxy Manager) for HTTPS + +--- + +## 🎯 Option 1: Portainer Stack (Recommended - Easiest) + +### Step 1: Upload Files to Your VM + +SSH into your Docker VM and create the project directory: + +```bash +ssh root@your-docker-vm-ip +mkdir -p /opt/lemonlink +cd /opt/lemonlink +``` + +Upload these 5 files to `/opt/lemonlink/`: +- `index.html` +- `styles.css` +- `script.js` +- `docker-compose.yml` +- `nginx.conf` + +You can use SCP, SFTP, or Portainer's file browser (if available). + +### Step 2: Deploy via Portainer + +1. Open Portainer in your browser (`http://your-vm-ip:9000`) +2. Click **Stacks** in the left sidebar +3. Click **+ Add Stack** +4. Configure: + - **Name**: `lemonlink` + - **Build method**: Select "Web editor" + - Paste the contents of `docker-compose.yml`: + +```yaml +version: '3.8' + +services: + lemonlink: + image: nginx:alpine + container_name: lemonlink-landing + restart: unless-stopped + volumes: + - /opt/lemonlink/index.html:/usr/share/nginx/html/index.html:ro + - /opt/lemonlink/styles.css:/usr/share/nginx/html/styles.css:ro + - /opt/lemonlink/script.js:/usr/share/nginx/html/script.js:ro + - /opt/lemonlink/nginx.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - lemonlink-network + ports: + - "8080:80" + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + labels: + - "com.lemonlink.description=LemonLink Landing Page" + +networks: + lemonlink-network: + driver: bridge +``` + +5. Click **Deploy the stack** +6. Wait for deployment (green indicator) + +### Step 3: Access Your Site + +Visit: `http://your-docker-vm-ip:8080` + +--- + +## 🐳 Option 2: Direct Docker (No Portainer) + +If you prefer command line: + +```bash +# SSH to your Docker VM +ssh root@your-docker-vm-ip + +# Create directory +mkdir -p /opt/lemonlink +cd /opt/lemonlink + +# Upload files (from your local machine) +scp index.html styles.css script.js docker-compose.yml nginx.conf root@your-docker-vm-ip:/opt/lemonlink/ + +# Run the container +docker-compose up -d + +# Check status +docker-compose ps +docker-compose logs -f +``` + +--- + +## πŸ”’ Option 3: With HTTPS (Traefik Reverse Proxy) + +If you have Traefik running in Portainer: + +### Step 1: Create Network + +In Portainer: +1. Go to **Networks** +2. Click **+ Add Network** +3. Name: `traefik-public` +4. Driver: `bridge` +5. Click **Create the network** + +### Step 2: Deploy with Traefik Labels + +Update the `docker-compose.yml` in Portainer: + +```yaml +version: '3.8' + +services: + lemonlink: + image: nginx:alpine + container_name: lemonlink-landing + restart: unless-stopped + volumes: + - /opt/lemonlink/index.html:/usr/share/nginx/html/index.html:ro + - /opt/lemonlink/styles.css:/usr/share/nginx/html/styles.css:ro + - /opt/lemonlink/script.js:/usr/share/nginx/html/script.js:ro + - /opt/lemonlink/nginx.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - traefik-public # Connect to Traefik network + # NO PORTS EXPOSED - Traefik handles routing + labels: + # Enable Traefik + - "traefik.enable=true" + + # Router configuration + - "traefik.http.routers.lemonlink.rule=Host(`lemonlink.eu`)" + - "traefik.http.routers.lemonlink.entrypoints=websecure" + - "traefik.http.routers.lemonlink.tls.certresolver=letsencrypt" + + # Service configuration + - "traefik.http.services.lemonlink.loadbalancer.server.port=80" + + # Middleware (optional - for security headers) + - "traefik.http.routers.lemonlink.middlewares=security-headers" + +networks: + traefik-public: + external: true +``` + +### Step 3: Update DNS + +Point your domain `lemonlink.eu` to your Docker VM's public IP. + +### Step 4: Access + +Visit: `https://lemonlink.eu` πŸŽ‰ + +--- + +## πŸ”„ Option 4: Using Nginx Proxy Manager + +If you use Nginx Proxy Manager: + +### Deploy without exposed ports: + +```yaml +version: '3.8' + +services: + lemonlink: + image: nginx:alpine + container_name: lemonlink-landing + restart: unless-stopped + volumes: + - /opt/lemonlink/index.html:/usr/share/nginx/html/index.html:ro + - /opt/lemonlink/styles.css:/usr/share/nginx/html/styles.css:ro + - /opt/lemonlink/script.js:/usr/share/nginx/html/script.js:ro + - /opt/lemonlink/nginx.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - npm-network # Your NPM network + # NO PORTS - NPM will route to it + +networks: + npm-network: + external: true +``` + +### In Nginx Proxy Manager: + +1. Go to **Proxy Hosts** +2. Click **Add Proxy Host** +3. Configure: + - **Domain Names**: `lemonlink.eu` + - **Scheme**: `http` + - **Forward Hostname/IP**: `lemonlink-landing` + - **Forward Port**: `80` +4. Enable **Block Common Exploits** +5. Go to **SSL** tab: + - Request a new SSL certificate + - Enable **Force SSL** + - Enable **HTTP/2 Support** +6. Click **Save** + +--- + +## πŸ–₯️ Option 5: Proxmox LXC Container (Alternative) + +If you prefer an LXC container instead of Docker: + +### Step 1: Create LXC Container + +In Proxmox: +1. Click **Create CT** +2. Template: `debian-12-standard` +3. Resources: 1 CPU, 512MB RAM, 8GB Disk (minimum) +4. Network: DHCP or static IP +5. Start container after creation + +### Step 2: Install Nginx + +```bash +# Enter the container (or SSH into it) +pct exec -- bash + +# Update and install nginx +apt update && apt install -y nginx + +# Remove default site +rm /var/www/html/index.nginx-debian.html +``` + +### Step 3: Upload Files + +From your computer: +```bash +scp index.html styles.css script.js root@lxc-container-ip:/var/www/html/ +``` + +### Step 4: Configure Nginx + +```bash +# Edit nginx config +cat > /etc/nginx/sites-available/default << 'EOF' +server { + listen 80; + listen [::]:80; + root /var/www/html; + index index.html; + server_name lemonlink.eu; + + location / { + try_files $uri $uri/ =404; + } + + # Gzip + gzip on; + gzip_types text/css application/javascript; +} +EOF + +# Test and reload +nginx -t +systemctl reload nginx +``` + +--- + +## πŸ“ File Structure on Server + +After deployment, your server should have: + +``` +/opt/lemonlink/ +β”œβ”€β”€ index.html # Main website +β”œβ”€β”€ styles.css # Styling +β”œβ”€β”€ script.js # JavaScript +β”œβ”€β”€ docker-compose.yml # Docker configuration +β”œβ”€β”€ nginx.conf # Nginx configuration +β”œβ”€β”€ DEPLOY.md # This guide +└── README.md # Website documentation +``` + +--- + +## πŸ”„ Updating Your Website + +### Method 1: Direct File Edit + +Edit files directly on the server: +```bash +ssh root@your-docker-vm-ip +nano /opt/lemonlink/index.html +# Edit, save, and changes reflect immediately! +``` + +### Method 2: Re-upload Files + +```bash +scp index.html styles.css script.js root@your-docker-vm-ip:/opt/lemonlink/ +``` + +No restart needed - changes are live instantly! + +### Method 3: Using Portainer + +1. Go to **Containers** +2. Find `lemonlink-landing` +3. Click **Console** β†’ **/bin/sh** +4. Edit files in `/usr/share/nginx/html/` + +--- + +## πŸ› οΈ Troubleshooting + +### Container won't start + +```bash +# Check logs +docker logs lemonlink-landing + +# Verify files exist +ls -la /opt/lemonlink/ + +# Restart container +docker restart lemonlink-landing +``` + +### Permission denied + +```bash +# Fix permissions +chmod 644 /opt/lemonlink/*.{html,css,js,conf} +``` + +### Can't access the site + +```bash +# Check if container is running +docker ps | grep lemonlink + +# Check ports +docker port lemonlink-landing + +# Test from VM +curl http://localhost:8080 + +# Check firewall +ufw status +iptables -L +``` + +### SSL/HTTPS issues + +Make sure: +1. DNS A record points to your server IP +2. Port 443 is open in firewall +3. Traefik/NPM is properly configured + +--- + +## 🌐 Domain & DNS Setup + +### For External Access: + +1. **Get your public IP**: + ```bash + curl ifconfig.me + ``` + +2. **Create A Record** at your DNS provider: + - Type: `A` + - Name: `@` (or `www`) + - Value: `YOUR_PUBLIC_IP` + - TTL: `3600` + +3. **Port Forward** on your router: + - External 80 β†’ Internal `your-vm-ip:8080` + - External 443 β†’ Internal `your-vm-ip:443` (if using HTTPS) + +### For Internal Only: + +Add to your local DNS (Pi-hole, AdGuard, or `/etc/hosts`): +``` +your-docker-vm-ip lemonlink.eu +``` + +--- + +## πŸŽ‰ Success! + +Your jaw-dropping landing page should now be live! πŸ‹ + +- **Without HTTPS**: `http://your-vm-ip:8080` +- **With HTTPS**: `https://lemonlink.eu` + +--- + +## πŸ’‘ Pro Tips + +1. **Auto-updates**: Set up a Git repository for version control +2. **Backups**: Use Proxmox backup for the entire VM +3. **Monitoring**: Add the site to Uptime Kuma or your monitoring stack +4. **Analytics**: Add Plausible or Umami for privacy-focused analytics + +Need help? Check the logs or ask! πŸš€ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..42f7aa8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# LemonLink Landing Page - Production Dockerfile +FROM nginx:alpine + +# Copy custom nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy website files to nginx html directory +COPY index.html /usr/share/nginx/html/ +COPY styles.css /usr/share/nginx/html/ +COPY script.js /usr/share/nginx/html/ + +# Create non-root user for security +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + chown -R nginx:nginx /var/cache/nginx && \ + chown -R nginx:nginx /var/log/nginx && \ + chown -R nginx:nginx /etc/nginx/conf.d + +# Add health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1 + +# Expose port 80 +EXPOSE 80 + +# Run nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/GITEA_SETUP.md b/GITEA_SETUP.md new file mode 100644 index 0000000..faf1e5e --- /dev/null +++ b/GITEA_SETUP.md @@ -0,0 +1,269 @@ +# 🦊 Gitea Setup Guide + +Complete guide to push your LemonLink project to your Gitea instance. + +--- + +## πŸ“‹ What I Need From You + +To help you set up the repository, please provide: + +1. **Gitea URL** - e.g., `https://git.yourdomain.com` or `https://gitea.lemonlink.eu` +2. **Your username** - e.g., `lemonadmin` +3. **Repository name** - e.g., `lemonlink-website` or `landing-page` +4. **Access method**: SSH key or HTTPS with token? + +--- + +## πŸš€ Method 1: Create Repo via Web UI (Easiest) + +### Step 1: Create Repository in Gitea + +1. Log into your Gitea: `https://your-gitea.com` +2. Click **+** β†’ **New Repository** +3. Fill in: + - **Owner**: Your username/organization + - **Repository Name**: `lemonlink` (or your preference) + - **Description**: "Jaw-dropping landing page for my homelab" + - **Visibility**: β˜‘οΈ Make it private (or public if you want) + - **Initialize**: ☐ Uncheck "Initialize Repository" (we'll push existing files) +4. Click **Create Repository** + +### Step 2: Push Your Code + +Open terminal/command prompt in your project folder and run: + +```bash +# Initialize Git (if not already done) +git init + +# Add all files +git add . + +# First commit +git commit -m "Initial commit: Jaw-dropping landing page πŸ‹" + +# Add Gitea remote (replace with your info) +git remote add origin https://your-gitea.com/username/lemonlink.git + +# Push to Gitea +git push -u origin main +``` + +--- + +## πŸ” Method 2: Using SSH Keys + +### Step 1: Generate SSH Key (if you don't have one) + +```bash +# Generate key +ssh-keygen -t ed25519 -C "your-email@example.com" + +# Copy public key +cat ~/.ssh/id_ed25519.pub +``` + +### Step 2: Add Key to Gitea + +1. Gitea β†’ User Settings β†’ SSH / GPG Keys +2. Click **Add SSH Key** +3. Paste your public key +4. Click **Add Key** + +### Step 3: Push with SSH + +```bash +# Use SSH URL +git remote add origin git@your-gitea.com:username/lemonlink.git +git push -u origin main +``` + +--- + +## πŸ”‘ Method 3: Using HTTPS with Access Token + +### Step 1: Create Access Token + +1. Gitea β†’ User Settings β†’ Applications +2. Generate New Token +3. Name: "LemonLink Development" +4. Permissions: β˜‘οΈ repo (full access) +5. Click **Generate Token** +6. **COPY THE TOKEN** (you can't see it again!) + +### Step 2: Push with Token + +```bash +# Use token in URL (replace YOUR_TOKEN) +git remote add origin https://YOUR_TOKEN@your-gitea.com/username/lemonlink.git + +git push -u origin main +``` + +Or use credential helper: +```bash +git config --global credential.helper cache +git push -u origin main +# Enter username and token as password +``` + +--- + +## πŸ”„ Keeping Gitea in Sync + +After making changes: + +```bash +# Stage changes +git add . + +# Commit +git commit -m "Description of changes" + +# Push to Gitea +git push origin main +``` + +--- + +## 🏷️ Tagging Releases + +```bash +# Create version tag +git tag -a v1.0.0 -m "First stable release" + +# Push tags +git push origin --tags +``` + +--- + +## 🎨 Gitea Features to Enable + +In your Gitea repository settings: + +### Issues +- Enable for bug reports and feature requests + +### Wiki +- Enable for extended documentation + +### Projects +- Enable for Kanban-style project management + +### Actions (CI/CD) +If your Gitea has Actions enabled, you can add `.gitea/workflows/deploy.yml`: + +```yaml +name: Deploy to Server +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Deploy to server + run: | + # Add your deployment commands here + echo "Deployed!" +``` + +--- + +## πŸ“± Cloning on Other Machines + +Once in Gitea, clone anywhere: + +```bash +# HTTPS +git clone https://your-gitea.com/username/lemonlink.git + +# SSH +git clone git@your-gitea.com:username/lemonlink.git +``` + +--- + +## 🌐 Gitea + Docker Integration + +If your Gitea and Docker are on the same VM, you can automate deployment: + +### Option 1: Gitea Webhook + +1. Gitea β†’ Repository Settings β†’ Webhooks +2. Add Gitea Webhook +3. Target URL: `http://localhost:9000/hooks/deploy` (your deployment endpoint) +4. Trigger on: Push events + +### Option 2: Git Pull in Container + +```bash +# SSH into Docker VM +cd /opt/lemonlink +git pull origin main +# Files update instantly! +``` + +### Option 3: Watchtower (Auto-update) + +If using Docker image from Gitea Registry: + +```yaml +version: '3.8' +services: + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: --interval 30 --cleanup +``` + +--- + +## πŸ› οΈ Troubleshooting + +### "Permission denied" +```bash +# Check remote URL +git remote -v + +# Fix permissions on SSH key +chmod 600 ~/.ssh/id_ed25519 +``` + +### "Repository not found" +- Verify repository exists in Gitea +- Check username/repo name spelling +- Ensure you have access permissions + +### "Failed to push" +```bash +# Force push (careful!) +git push -f origin main + +# Or pull first +git pull origin main +git push origin main +``` + +--- + +## ✨ Quick Reference + +| Command | Description | +|---------|-------------| +| `git status` | Check current state | +| `git add .` | Stage all changes | +| `git commit -m "msg"` | Commit changes | +| `git push` | Push to Gitea | +| `git pull` | Get latest changes | +| `git log` | View commit history | + +--- + +**Ready?** Give me your Gitea details and I'll generate the exact commands for you! πŸš€ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a0c8938 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 LemonLink + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea66206 --- /dev/null +++ b/README.md @@ -0,0 +1,212 @@ +# πŸ‹ LemonLink - Jaw-Dropping Landing Page + +A stunning, modern landing page for lemonlink.eu that showcases your homelab, services, projects, and network of sub-domains. Built with pure HTML, CSS, and JavaScript - no frameworks required! + +![LemonLink Preview](preview.png) + +## ✨ Features + +### 🎨 Visual Design +- **Glassmorphism UI** - Modern frosted glass effects with backdrop blur +- **Animated Gradient Background** - Floating blobs with smooth animations +- **Responsive Design** - Looks great on desktop, tablet, and mobile +- **Dark Theme** - Easy on the eyes with vibrant lemon/gold accents +- **Smooth Animations** - Scroll reveals, hover effects, and micro-interactions + +### 🏠 Sections +1. **Hero** - Eye-catching intro with animated server rack visualization +2. **Homelab** - Showcase your infrastructure with animated server units +3. **Services** - Grid of self-hosted services with status indicators +4. **Projects** - Portfolio cards with code window visualization +5. **Network** - Visual map of all your sub-domains +6. **Contact** - Terminal-style contact section + +### πŸš€ Interactive Features +- **Mouse-following glow** on service cards +- **Animated counters** for statistics +- **Parallax background** on mouse move +- **Typing animation** in terminal +- **Konami code easter egg** (↑↑↓↓←→←→BA) +- **Smooth scroll** navigation + +## πŸ“ File Structure + +``` +lemonlink/ +β”œβ”€β”€ index.html # Main HTML file +β”œβ”€β”€ styles.css # All styles (32KB) +β”œβ”€β”€ script.js # All interactivity (15KB) +└── README.md # This file +``` + +## πŸš€ Quick Start + +### Option 1: Simple Hosting (Recommended) +Just upload the three files (`index.html`, `styles.css`, `script.js`) to your web server: + +```bash +# Using SCP +scp index.html styles.css script.js user@yourserver:/var/www/lemonlink.eu/ + +# Or using FTP, SFTP, or your hosting provider's file manager +``` + +### Option 2: Docker Deployment + +Create a `Dockerfile`: + +```dockerfile +FROM nginx:alpine +COPY . /usr/share/nginx/html +EXPOSE 80 +``` + +Build and run: + +```bash +docker build -t lemonlink . +docker run -d -p 80:80 --name lemonlink lemonlink +``` + +### Option 3: Nginx Configuration + +```nginx +server { + listen 80; + server_name lemonlink.eu www.lemonlink.eu; + root /var/www/lemonlink.eu; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + # Enable gzip compression + gzip on; + gzip_types text/css application/javascript; +} +``` + +## πŸ”§ Customization Guide + +### 1. Update Service Links + +Find the services section in `index.html` and update the URLs: + +```html + + + +``` + +### 2. Update Sub-domains + +In the Network section, update the domain nodes: + +```html + + service + Description + +``` + +### 3. Update Project Information + +Find the Projects section and customize: + +```html +
+

Your Project Name

+

Your project description...

+ +
+``` + +### 4. Update Homelab Specs + +In the Homelab section, update your hardware specs: + +```html +
+ Your Cores + Your RAM + Your Storage +
+``` + +### 5. Update Contact Information + +```html + + your@email.com + +``` + +### 6. Update Social Links + +Find the social links section and update the hrefs: + +```html + + + +
+
+
+ + Systems Online +
+

+ Welcome to + LemonLink +

+

+ A cutting-edge homelab ecosystem hosting innovative services, + powerful applications, and experimental projects. + Built with passion, powered by curiosity. +

+
+
+ 0 + % + Uptime +
+
+ 0 + /7 + Monitoring +
+
+ 0 + + + Services +
+
+ +
+
+
+
+
+ + + +
+
+
+
+
+ + + +
+
+
+
+
+ + + +
+
+
+
+
+ + + +
+
+
+
+
+
+ 🐳 + Docker +
+
+ ☸️ + K8s +
+
+ πŸ”’ + Secure +
+
+
+
+ + +
+
+
+ 🏠 Infrastructure +

The Homelab

+

A powerful self-hosted ecosystem running on enterprise-grade hardware

+
+ +
+ + +
+
πŸ–§
+
+

Network Stack

+

UniFi ecosystem with 10G backbone and advanced VLAN segmentation

+
+ 10 Gbps + UniFi + VLAN +
+
+
+ +
+
πŸ’Ύ
+
+

Storage Array

+

TrueNAS SCALE with ZFS pools for reliable data storage

+
+ 48 TB + ZFS + RAID-Z2 +
+
+
+ +
+
πŸ€–
+
+

Automation

+

Terraform, Ansible & GitOps for infrastructure as code

+
+ IaC + GitOps + CI/CD +
+
+
+
+ +
+

Powered By

+
+
+ Proxmox + Docker + Kubernetes + TrueNAS + UniFi + Nginx + Traefik + Prometheus + Grafana + Portainer + Proxmox + Docker + Kubernetes + TrueNAS + UniFi + Nginx + Traefik + Prometheus + Grafana + Portainer +
+
+
+
+
+ + +
+ +
+ + +
+
+
+ πŸš€ Creations +

Projects

+

Things I've built and contributed to

+
+ +
+
+
+
+
+
+ + + +
+
+
const project = {
+
name: "Awesome App",
+
status: "Production",
+
stars: 1337
+
};
+
+
+
+
+
+
+ React + TypeScript + Node.js +
+

Featured Project

+

Your flagship project description goes here. This could be your most impressive work, a popular open-source contribution, or a commercial application you've built.

+ +
+
+ +
+
+
+ Python + Automation +
+

Home Automation

+

Smart home integration with Home Assistant and custom sensors

+ + + +
+ +
+
+ Go + CLI +
+

Dev Tool

+

Command-line utility for developers to boost productivity

+ + + +
+ +
+
+ Rust + Systems +
+

System Monitor

+

High-performance system resource monitoring daemon

+ + + +
+
+
+
+
+ + +
+
+
+ 🌐 Sub-Domains +

Network

+

Explore the LemonLink ecosystem

+
+ + +
+
+ + +
+
+
+
+ πŸ‘‹ Let's Connect +

Have a project in mind?

+

Whether you want to collaborate, need infrastructure advice, or just want to chat about homelabs β€” I'm always open to interesting conversations.

+ + +
+
+
+
+ + + +
+
+
+ ➜ + ~ + whoami +
+
lemon_admin
+
+ ➜ + ~ + uptime +
+
99.9% availability
+
+ ➜ + ~ + | +
+
+
+
+
+
+
+ + + + + + + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..ec22828 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,53 @@ +# LemonLink Nginx Configuration +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/xml+rss + application/json; + + # Security headers + 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; + + # Cache static assets + location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # Main location + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + # Deny access to hidden files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + # Error pages + error_page 404 /index.html; + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/portainer-stack.yml b/portainer-stack.yml new file mode 100644 index 0000000..5ea5e38 --- /dev/null +++ b/portainer-stack.yml @@ -0,0 +1,70 @@ +# πŸ‹ LemonLink - Portainer Stack Template +# Copy this entire content and paste into Portainer Stack Web Editor + +version: '3.8' + +services: + lemonlink: + image: nginx:alpine + container_name: lemonlink-landing + restart: unless-stopped + + # Mount website files from host + volumes: + - /opt/lemonlink/index.html:/usr/share/nginx/html/index.html:ro + - /opt/lemonlink/styles.css:/usr/share/nginx/html/styles.css:ro + - /opt/lemonlink/script.js:/usr/share/nginx/html/script.js:ro + - /opt/lemonlink/nginx.conf:/etc/nginx/conf.d/default.conf:ro + + # Network + networks: + - lemonlink-network + + # Port mapping - access at http://your-vm-ip:8080 + ports: + - "8080:80" + + # Health check + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + + # Labels for Traefik (uncomment if using Traefik) + # labels: + # - "traefik.enable=true" + # - "traefik.http.routers.lemonlink.rule=Host(`lemonlink.eu`)" + # - "traefik.http.routers.lemonlink.entrypoints=websecure" + # - "traefik.http.routers.lemonlink.tls.certresolver=letsencrypt" + # - "traefik.http.services.lemonlink.loadbalancer.server.port=80" + +networks: + lemonlink-network: + driver: bridge + name: lemonlink-network + +# πŸ”§ SETUP INSTRUCTIONS: +# +# 1. SSH into your Docker VM: +# ssh root@your-docker-vm-ip +# +# 2. Create directory and upload files: +# mkdir -p /opt/lemonlink +# # Upload index.html, styles.css, script.js, nginx.conf to /opt/lemonlink/ +# +# 3. In Portainer: +# - Go to Stacks β†’ + Add Stack +# - Name: lemonlink +# - Copy this entire file into Web Editor +# - Click Deploy +# +# 4. Access your site: +# http://your-docker-vm-ip:8080 +# +# πŸ“ For HTTPS with Traefik: +# - Uncomment the labels section above +# - Remove the "ports" section +# - Ensure Traefik is running on the same network +# - Point your domain DNS to this server diff --git a/script.js b/script.js new file mode 100644 index 0000000..fbd1126 --- /dev/null +++ b/script.js @@ -0,0 +1,467 @@ +/** + * LemonLink - Interactive Landing Page + * Modern animations and interactions + */ + +document.addEventListener('DOMContentLoaded', () => { + // Initialize all modules + initNavbar(); + initSmoothScroll(); + initCounters(); + initServiceCards(); + initScrollReveal(); + initMobileMenu(); + initParallax(); +}); + +/** + * Navbar scroll effect + */ +function initNavbar() { + const navbar = document.querySelector('.navbar'); + let lastScroll = 0; + + window.addEventListener('scroll', () => { + const currentScroll = window.pageYOffset; + + // Add/remove scrolled class + if (currentScroll > 50) { + navbar.classList.add('scrolled'); + } else { + navbar.classList.remove('scrolled'); + } + + // Hide/show on scroll direction + if (currentScroll > lastScroll && currentScroll > 100) { + navbar.style.transform = 'translateY(-100%)'; + } else { + navbar.style.transform = 'translateY(0)'; + } + + lastScroll = currentScroll; + }); + + // Smooth transition for navbar + navbar.style.transition = 'transform 0.3s ease, background 0.3s ease'; +} + +/** + * Smooth scroll for anchor links + */ +function initSmoothScroll() { + document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function(e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute('href')); + if (target) { + target.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + } + }); + }); +} + +/** + * Animated counters for stats + */ +function initCounters() { + const counters = document.querySelectorAll('.stat-number'); + + const animateCounter = (counter) => { + const target = parseFloat(counter.getAttribute('data-target')); + const duration = 2000; + const step = target / (duration / 16); + let current = 0; + + const updateCounter = () => { + current += step; + if (current < target) { + counter.textContent = current.toFixed(1); + requestAnimationFrame(updateCounter); + } else { + counter.textContent = target; + } + }; + + updateCounter(); + }; + + // Use Intersection Observer to trigger animation + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + animateCounter(entry.target); + observer.unobserve(entry.target); + } + }); + }, { threshold: 0.5 }); + + counters.forEach(counter => observer.observe(counter)); +} + +/** + * Service cards mouse follow glow effect + */ +function initServiceCards() { + const cards = document.querySelectorAll('.service-card'); + + cards.forEach(card => { + const glow = card.querySelector('.service-glow'); + + card.addEventListener('mousemove', (e) => { + const rect = card.getBoundingClientRect(); + const x = ((e.clientX - rect.left) / rect.width) * 100; + const y = ((e.clientY - rect.top) / rect.height) * 100; + + card.style.setProperty('--mouse-x', `${x}%`); + card.style.setProperty('--mouse-y', `${y}%`); + }); + }); +} + +/** + * Scroll reveal animations + */ +function initScrollReveal() { + const reveals = document.querySelectorAll('.section, .service-card, .infra-card, .project-card, .domain-node'); + + const revealOnScroll = (entries, observer) => { + entries.forEach((entry, index) => { + if (entry.isIntersecting) { + // Add stagger delay for cards + if (entry.target.classList.contains('service-card') || + entry.target.classList.contains('domain-node')) { + entry.target.style.animationDelay = `${index * 0.1}s`; + } + + entry.target.style.opacity = '1'; + entry.target.style.transform = 'translateY(0)'; + observer.unobserve(entry.target); + } + }); + }; + + const observer = new IntersectionObserver(revealOnScroll, { + threshold: 0.1, + rootMargin: '0px 0px -50px 0px' + }); + + reveals.forEach(el => { + el.style.opacity = '0'; + el.style.transform = 'translateY(30px)'; + el.style.transition = 'opacity 0.6s ease, transform 0.6s ease'; + observer.observe(el); + }); +} + +/** + * Mobile menu toggle + */ +function initMobileMenu() { + const menuBtn = document.querySelector('.mobile-menu-btn'); + const navLinks = document.querySelector('.nav-links'); + + if (!menuBtn) return; + + menuBtn.addEventListener('click', () => { + menuBtn.classList.toggle('active'); + navLinks.classList.toggle('active'); + + // Animate hamburger + const spans = menuBtn.querySelectorAll('span'); + if (menuBtn.classList.contains('active')) { + spans[0].style.transform = 'rotate(45deg) translate(5px, 5px)'; + spans[1].style.opacity = '0'; + spans[2].style.transform = 'rotate(-45deg) translate(5px, -5px)'; + + // Show mobile menu + navLinks.style.display = 'flex'; + navLinks.style.flexDirection = 'column'; + navLinks.style.position = 'absolute'; + navLinks.style.top = '100%'; + navLinks.style.left = '0'; + navLinks.style.right = '0'; + navLinks.style.background = 'rgba(10, 10, 15, 0.98)'; + navLinks.style.padding = '2rem'; + navLinks.style.backdropFilter = 'blur(20px)'; + navLinks.style.borderBottom = '1px solid rgba(255,255,255,0.1)'; + } else { + spans[0].style.transform = 'none'; + spans[1].style.opacity = '1'; + spans[2].style.transform = 'none'; + navLinks.style.display = ''; + } + }); + + // Close menu on link click + navLinks.querySelectorAll('a').forEach(link => { + link.addEventListener('click', () => { + menuBtn.classList.remove('active'); + navLinks.classList.remove('active'); + const spans = menuBtn.querySelectorAll('span'); + spans[0].style.transform = 'none'; + spans[1].style.opacity = '1'; + spans[2].style.transform = 'none'; + navLinks.style.display = ''; + }); + }); +} + +/** + * Parallax effect for hero section + */ +function initParallax() { + const hero = document.querySelector('.hero'); + const blobs = document.querySelectorAll('.bg-blob'); + + if (!hero) return; + + // Check for touch device + const isTouchDevice = window.matchMedia('(pointer: coarse)').matches; + if (isTouchDevice) return; + + let ticking = false; + + document.addEventListener('mousemove', (e) => { + if (!ticking) { + requestAnimationFrame(() => { + const x = (e.clientX / window.innerWidth - 0.5) * 2; + const y = (e.clientY / window.innerHeight - 0.5) * 2; + + blobs.forEach((blob, index) => { + const speed = (index + 1) * 20; + blob.style.transform = `translate(${x * speed}px, ${y * speed}px)`; + }); + + ticking = false; + }); + ticking = true; + } + }); +} + +/** + * Typing effect for terminal cursor + */ +function initTerminalTyping() { + const terminal = document.querySelector('.terminal-body'); + if (!terminal) return; + + const commands = [ + { text: 'docker ps', output: 'CONTAINER ID IMAGE STATUS\nabc123 nginx Up 3 days' }, + { text: 'kubectl get pods', output: 'NAME READY STATUS\nlemonlink-app-7d9f4 1/1 Running' }, + { text: 'systemctl status lemonlink', output: '● lemonlink.service - LemonLink Platform\n Active: active (running)' } + ]; + + let currentCommand = 0; + let isTyping = false; + + const typeCommand = () => { + if (isTyping) return; + isTyping = true; + + const cmd = commands[currentCommand]; + const lines = terminal.querySelectorAll('.terminal-line'); + const lastLine = lines[lines.length - 1]; + + // Clear cursor from previous line + const cursor = terminal.querySelector('.cursor'); + if (cursor) cursor.remove(); + + // Create new command line + const newLine = document.createElement('div'); + newLine.className = 'terminal-line'; + newLine.innerHTML = ` + ➜ + ~ + + | + `; + terminal.appendChild(newLine); + + const commandSpan = newLine.querySelector('.command'); + let charIndex = 0; + + const typeChar = () => { + if (charIndex < cmd.text.length) { + commandSpan.textContent += cmd.text[charIndex]; + charIndex++; + setTimeout(typeChar, 50 + Math.random() * 50); + } else { + // Show output after typing + setTimeout(() => { + const outputDiv = document.createElement('div'); + outputDiv.className = 'terminal-output'; + outputDiv.innerHTML = cmd.output.replace(/\n/g, '
'); + terminal.appendChild(outputDiv); + + // Move cursor to new line + const nextLine = document.createElement('div'); + nextLine.className = 'terminal-line'; + nextLine.innerHTML = ` + ➜ + ~ + | + `; + terminal.appendChild(nextLine); + + // Scroll to bottom + terminal.scrollTop = terminal.scrollHeight; + + isTyping = false; + currentCommand = (currentCommand + 1) % commands.length; + }, 500); + } + }; + + typeChar(); + }; + + // Start typing effect every 8 seconds + setInterval(typeCommand, 8000); +} + +// Initialize terminal typing when contact section is visible +const terminalObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + initTerminalTyping(); + terminalObserver.unobserve(entry.target); + } + }); +}, { threshold: 0.5 }); + +const contactSection = document.querySelector('.contact-section'); +if (contactSection) { + terminalObserver.observe(contactSection); +} + +/** + * Network connection lines animation + */ +function drawConnectionLines() { + const hub = document.querySelector('.hub-center'); + const nodes = document.querySelectorAll('.domain-node'); + const svg = document.querySelector('.connections-svg'); + + if (!hub || !svg || nodes.length === 0) return; + + const hubRect = hub.getBoundingClientRect(); + const hubX = hubRect.left + hubRect.width / 2; + const hubY = hubRect.top + hubRect.height / 2; + + // Clear existing lines + svg.innerHTML = svg.querySelector('defs').outerHTML; + + nodes.forEach(node => { + const nodeRect = node.getBoundingClientRect(); + const nodeX = nodeRect.left + nodeRect.width / 2; + const nodeY = nodeRect.top + nodeRect.height / 2; + + const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + line.setAttribute('x1', hubX); + line.setAttribute('y1', hubY); + line.setAttribute('x2', nodeX); + line.setAttribute('y2', nodeY); + line.setAttribute('stroke', 'url(#line-gradient)'); + line.setAttribute('stroke-width', '1'); + line.setAttribute('opacity', '0.3'); + + svg.appendChild(line); + }); +} + +// Draw lines on load and resize +window.addEventListener('load', drawConnectionLines); +window.addEventListener('resize', drawConnectionLines); + +/** + * Easter egg: Konami code + */ +let konamiCode = []; +const konamiSequence = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a']; + +document.addEventListener('keydown', (e) => { + konamiCode.push(e.key); + konamiCode = konamiCode.slice(-10); + + if (konamiCode.join(',') === konamiSequence.join(',')) { + // Activate lemon mode! + document.body.style.filter = 'hue-rotate(180deg)'; + setTimeout(() => { + document.body.style.filter = ''; + }, 3000); + + // Create floating lemons + for (let i = 0; i < 20; i++) { + createFloatingLemon(); + } + } +}); + +function createFloatingLemon() { + const lemon = document.createElement('div'); + lemon.textContent = 'πŸ‹'; + lemon.style.cssText = ` + position: fixed; + font-size: 2rem; + pointer-events: none; + z-index: 9999; + left: ${Math.random() * 100}vw; + top: -50px; + animation: fall ${3 + Math.random() * 2}s linear forwards; + `; + + document.body.appendChild(lemon); + + setTimeout(() => lemon.remove(), 5000); +} + +// Add falling animation +const style = document.createElement('style'); +style.textContent = ` + @keyframes fall { + to { + transform: translateY(110vh) rotate(${Math.random() * 360}deg); + } + } +`; +document.head.appendChild(style); + +/** + * Service status check simulation + */ +function checkServiceStatus() { + const statusIndicators = document.querySelectorAll('.service-status'); + + statusIndicators.forEach(indicator => { + // Random status update for demo purposes + const isOnline = Math.random() > 0.1; // 90% online + const dot = indicator.querySelector('.status-dot'); + + if (isOnline) { + indicator.className = 'service-status online'; + indicator.querySelector('span:last-child').textContent = 'Online'; + dot.style.background = '#22c55e'; + } else { + indicator.className = 'service-status maintenance'; + indicator.querySelector('span:last-child').textContent = 'Maintenance'; + dot.style.background = '#f59e0b'; + } + }); +} + +// Simulate status check every 30 seconds +setInterval(checkServiceStatus, 30000); + +/** + * Prefers reduced motion + */ +if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + // Disable animations + document.documentElement.style.setProperty('--transition-fast', '0s'); + document.documentElement.style.setProperty('--transition-normal', '0s'); + document.documentElement.style.setProperty('--transition-slow', '0s'); +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..67dc8c8 --- /dev/null +++ b/styles.css @@ -0,0 +1,1514 @@ +/* ======================================== + LemonLink - Jaw-Dropping Landing Page + Modern Glassmorphism Design + ======================================== */ + +/* CSS Variables */ +:root { + --color-bg: #0a0a0f; + --color-bg-secondary: #12121a; + --color-bg-card: rgba(20, 20, 30, 0.6); + --color-border: rgba(255, 255, 255, 0.08); + --color-text: #f8fafc; + --color-text-muted: #94a3b8; + --color-primary: #eab308; + --color-primary-light: #facc15; + --color-primary-dark: #ca8a04; + --color-secondary: #6366f1; + --color-accent: #22d3ee; + --color-success: #22c55e; + --gradient-primary: linear-gradient(135deg, #eab308 0%, #f97316 50%, #ef4444 100%); + --gradient-cool: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #d946ef 100%); + --gradient-dark: linear-gradient(180deg, rgba(10,10,15,0) 0%, rgba(10,10,15,1) 100%); + --shadow-glow: 0 0 40px rgba(234, 179, 8, 0.3); + --shadow-card: 0 8px 32px rgba(0, 0, 0, 0.4); + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 20px; + --radius-xl: 28px; + --transition-fast: 0.2s ease; + --transition-normal: 0.3s ease; + --transition-slow: 0.5s ease; +} + +/* Reset & Base */ +*, *::before, *::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + scroll-padding-top: 80px; +} + +body { + font-family: 'Outfit', -apple-system, BlinkMacSystemFont, sans-serif; + background: var(--color-bg); + color: var(--color-text); + line-height: 1.6; + overflow-x: hidden; +} + +/* ======================================== + Animated Background + ======================================== */ +.bg-animation { + position: fixed; + inset: 0; + z-index: -1; + overflow: hidden; +} + +.bg-blob { + position: absolute; + border-radius: 50%; + filter: blur(80px); + opacity: 0.5; + animation: blob-float 20s infinite ease-in-out; +} + +.blob-1 { + width: 600px; + height: 600px; + background: radial-gradient(circle, rgba(234, 179, 8, 0.4) 0%, transparent 70%); + top: -200px; + right: -100px; + animation-delay: 0s; +} + +.blob-2 { + width: 500px; + height: 500px; + background: radial-gradient(circle, rgba(99, 102, 241, 0.3) 0%, transparent 70%); + bottom: -150px; + left: -100px; + animation-delay: -7s; +} + +.blob-3 { + width: 400px; + height: 400px; + background: radial-gradient(circle, rgba(34, 211, 238, 0.2) 0%, transparent 70%); + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + animation-delay: -14s; +} + +@keyframes blob-float { + 0%, 100% { transform: translate(0, 0) scale(1); } + 25% { transform: translate(50px, -50px) scale(1.1); } + 50% { transform: translate(-30px, 30px) scale(0.9); } + 75% { transform: translate(-50px, -30px) scale(1.05); } +} + +.bg-grid { + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px); + background-size: 50px 50px; + mask-image: radial-gradient(ellipse at center, black 0%, transparent 70%); +} + +/* ======================================== + Navigation + ======================================== */ +.navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + padding: 1rem 2rem; + transition: var(--transition-normal); +} + +.navbar.scrolled { + background: rgba(10, 10, 15, 0.8); + backdrop-filter: blur(20px); + border-bottom: 1px solid var(--color-border); +} + +.nav-container { + max-width: 1400px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; +} + +.logo { + display: flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; + color: var(--color-text); + font-family: 'Space Grotesk', sans-serif; + font-weight: 700; + font-size: 1.5rem; +} + +.logo-icon { + font-size: 1.75rem; + animation: lemon-bounce 2s infinite ease-in-out; +} + +@keyframes lemon-bounce { + 0%, 100% { transform: rotate(-5deg) scale(1); } + 50% { transform: rotate(5deg) scale(1.1); } +} + +.logo-highlight { + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.nav-links { + display: flex; + align-items: center; + gap: 2rem; + list-style: none; +} + +.nav-link { + text-decoration: none; + color: var(--color-text-muted); + font-weight: 500; + transition: var(--transition-fast); + position: relative; +} + +.nav-link::after { + content: ''; + position: absolute; + bottom: -4px; + left: 0; + width: 0; + height: 2px; + background: var(--gradient-primary); + transition: var(--transition-fast); +} + +.nav-link:hover { + color: var(--color-text); +} + +.nav-link:hover::after { + width: 100%; +} + +.nav-cta { + padding: 0.6rem 1.2rem; + background: var(--gradient-primary); + color: var(--color-bg) !important; + border-radius: var(--radius-md); + font-weight: 600; +} + +.nav-cta::after { + display: none; +} + +.nav-cta:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-glow); +} + +.mobile-menu-btn { + display: none; + flex-direction: column; + gap: 5px; + background: none; + border: none; + cursor: pointer; + padding: 5px; +} + +.mobile-menu-btn span { + width: 25px; + height: 2px; + background: var(--color-text); + transition: var(--transition-fast); +} + +/* ======================================== + Hero Section + ======================================== */ +.hero { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 8rem 2rem 4rem; + position: relative; + overflow: hidden; +} + +.hero-content { + max-width: 700px; + text-align: center; + z-index: 1; +} + +.hero-badge { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: rgba(34, 197, 94, 0.1); + border: 1px solid rgba(34, 197, 94, 0.3); + border-radius: 100px; + font-size: 0.875rem; + color: #22c55e; + margin-bottom: 1.5rem; +} + +.badge-dot { + width: 8px; + height: 8px; + background: #22c55e; + border-radius: 50%; + animation: pulse-dot 2s infinite; +} + +@keyframes pulse-dot { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(1.2); } +} + +.hero-title { + font-family: 'Space Grotesk', sans-serif; + font-size: clamp(3rem, 8vw, 5rem); + font-weight: 800; + line-height: 1.1; + margin-bottom: 1.5rem; +} + +.title-line { + display: block; +} + +.title-gradient { + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + position: relative; +} + +.title-gradient::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 8px; + background: var(--gradient-primary); + opacity: 0.3; + filter: blur(10px); +} + +.hero-subtitle { + font-size: 1.25rem; + color: var(--color-text-muted); + max-width: 600px; + margin: 0 auto 2.5rem; +} + +.hero-stats { + display: flex; + justify-content: center; + gap: 3rem; + margin-bottom: 2.5rem; +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; +} + +.stat-number { + font-family: 'Space Grotesk', sans-serif; + font-size: 2.5rem; + font-weight: 700; + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stat-suffix { + font-family: 'Space Grotesk', sans-serif; + font-size: 1.5rem; + font-weight: 600; + color: var(--color-primary); + margin-left: 2px; +} + +.stat-label { + font-size: 0.875rem; + color: var(--color-text-muted); +} + +.hero-cta { + display: flex; + justify-content: center; + gap: 1rem; + flex-wrap: wrap; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 1rem 2rem; + font-family: 'Outfit', sans-serif; + font-size: 1rem; + font-weight: 600; + text-decoration: none; + border-radius: var(--radius-md); + transition: var(--transition-normal); + border: none; + cursor: pointer; +} + +.btn-primary { + background: var(--gradient-primary); + color: var(--color-bg); + box-shadow: 0 4px 20px rgba(234, 179, 8, 0.3); +} + +.btn-primary:hover { + transform: translateY(-3px); + box-shadow: 0 8px 30px rgba(234, 179, 8, 0.4); +} + +.btn-secondary { + background: transparent; + color: var(--color-text); + border: 2px solid var(--color-border); +} + +.btn-secondary:hover { + border-color: var(--color-primary); + background: rgba(234, 179, 8, 0.1); +} + +.btn-icon { + width: 20px; + height: 20px; + transition: var(--transition-fast); +} + +.btn:hover .btn-icon { + transform: translateX(4px); +} + +/* Hero Visual */ +.hero-visual { + position: absolute; + right: 5%; + top: 50%; + transform: translateY(-50%); + opacity: 0.6; + pointer-events: none; +} + +.server-rack { + display: flex; + flex-direction: column; + gap: 8px; + transform: perspective(1000px) rotateY(-15deg) rotateX(5deg); +} + +.server-unit { + width: 180px; + height: 50px; + background: linear-gradient(90deg, #1a1a2e 0%, #252542 100%); + border-radius: 6px; + border: 1px solid rgba(255,255,255,0.1); + display: flex; + align-items: center; + padding: 0 15px; + gap: 10px; +} + +.server-lights { + display: flex; + gap: 4px; +} + +.light { + width: 4px; + height: 4px; + border-radius: 50%; + animation: server-blink 1.5s infinite; +} + +.light.green { background: #22c55e; animation-delay: 0s; } +.light.blue { background: #3b82f6; animation-delay: 0.3s; } +.light.amber { background: #f59e0b; animation-delay: 0.6s; } + +@keyframes server-blink { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 1; } +} + +.server-vents { + flex: 1; + height: 30px; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 3px, + rgba(0,0,0,0.3) 3px, + rgba(0,0,0,0.3) 4px + ); + border-radius: 2px; +} + +.floating-cards { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.float-card { + position: absolute; + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + background: rgba(20, 20, 35, 0.8); + backdrop-filter: blur(10px); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + font-size: 0.875rem; + font-weight: 500; + animation: float-card 6s infinite ease-in-out; +} + +.card-1 { top: -20px; left: -60px; animation-delay: 0s; } +.card-2 { top: 50%; right: -80px; animation-delay: -2s; } +.card-3 { bottom: -10px; left: -40px; animation-delay: -4s; } + +.card-icon { + font-size: 1.25rem; +} + +@keyframes float-card { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-15px); } +} + +/* ======================================== + Section Styles + ======================================== */ +.section { + padding: 6rem 2rem; + position: relative; +} + +.section-container { + max-width: 1200px; + margin: 0 auto; +} + +.section-header { + text-align: center; + margin-bottom: 4rem; +} + +.section-badge { + display: inline-block; + padding: 0.5rem 1rem; + background: rgba(234, 179, 8, 0.1); + border: 1px solid rgba(234, 179, 8, 0.3); + border-radius: 100px; + font-size: 0.875rem; + color: var(--color-primary); + margin-bottom: 1rem; +} + +.section-title { + font-family: 'Space Grotesk', sans-serif; + font-size: clamp(2rem, 5vw, 3rem); + font-weight: 700; + margin-bottom: 1rem; +} + +.section-desc { + font-size: 1.125rem; + color: var(--color-text-muted); + max-width: 600px; + margin: 0 auto; +} + +/* ======================================== + Homelab Section + ======================================== */ +.homelab-section { + background: linear-gradient(180deg, transparent 0%, rgba(99, 102, 241, 0.05) 50%, transparent 100%); +} + +.homelab-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; + margin-bottom: 4rem; +} + +.infra-card { + background: var(--color-bg-card); + backdrop-filter: blur(20px); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 2rem; + transition: var(--transition-normal); + position: relative; + overflow: hidden; +} + +.infra-card::before { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle at top right, rgba(234, 179, 8, 0.1) 0%, transparent 50%); + opacity: 0; + transition: var(--transition-normal); +} + +.infra-card:hover { + transform: translateY(-5px); + border-color: rgba(234, 179, 8, 0.3); + box-shadow: var(--shadow-card); +} + +.infra-card:hover::before { + opacity: 1; +} + +.infra-card.featured { + grid-column: span 2; + display: grid; + grid-template-columns: 1fr 2fr; + gap: 2rem; + align-items: center; +} + +.infra-visual { + display: flex; + justify-content: center; + align-items: center; +} + +.rack-animation { + display: flex; + flex-direction: column; + gap: 6px; + padding: 1rem; + background: rgba(0,0,0,0.3); + border-radius: var(--radius-md); +} + +.rack-unit { + width: 120px; + height: 35px; + background: linear-gradient(90deg, #1a1a2e 0%, #252542 100%); + border-radius: 4px; + display: flex; + align-items: center; + padding: 0 10px; +} + +.rack-unit.active .unit-leds span { + animation: led-blink 1s infinite; +} + +.unit-leds { + display: flex; + gap: 3px; +} + +.unit-leds span { + width: 3px; + height: 3px; + background: #22c55e; + border-radius: 50%; +} + +.unit-leds span:nth-child(2) { animation-delay: 0.2s; background: #3b82f6; } +.unit-leds span:nth-child(3) { animation-delay: 0.4s; background: #eab308; } +.unit-leds span:nth-child(4) { animation-delay: 0.6s; background: #ef4444; } + +@keyframes led-blink { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 1; } +} + +.infra-icon { + font-size: 3rem; + margin-bottom: 1rem; +} + +.infra-content { + position: relative; + z-index: 1; +} + +.infra-content h3 { + font-family: 'Space Grotesk', sans-serif; + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.infra-content p { + color: var(--color-text-muted); + margin-bottom: 1rem; +} + +.infra-specs { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.spec { + padding: 0.35rem 0.75rem; + background: rgba(234, 179, 8, 0.1); + border: 1px solid rgba(234, 179, 8, 0.2); + border-radius: 100px; + font-size: 0.875rem; + font-weight: 500; + color: var(--color-primary); +} + +/* Tech Stack Marquee */ +.tech-stack { + text-align: center; +} + +.stack-title { + font-family: 'Space Grotesk', sans-serif; + font-size: 1rem; + font-weight: 600; + color: var(--color-text-muted); + margin-bottom: 1.5rem; + text-transform: uppercase; + letter-spacing: 2px; +} + +.stack-marquee { + overflow: hidden; + mask-image: linear-gradient(90deg, transparent 0%, black 10%, black 90%, transparent 100%); +} + +.stack-track { + display: flex; + gap: 3rem; + animation: marquee 30s linear infinite; +} + +.tech-item { + font-family: 'Space Grotesk', sans-serif; + font-size: 1.25rem; + font-weight: 600; + color: var(--color-text-muted); + white-space: nowrap; + opacity: 0.6; + transition: var(--transition-fast); +} + +.tech-item:hover { + color: var(--color-primary); + opacity: 1; +} + +@keyframes marquee { + 0% { transform: translateX(0); } + 100% { transform: translateX(-50%); } +} + +/* ======================================== + Services Section + ======================================== */ +.services-section { + background: linear-gradient(180deg, transparent 0%, rgba(34, 211, 238, 0.03) 50%, transparent 100%); +} + +.services-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.service-card { + position: relative; + background: var(--color-bg-card); + backdrop-filter: blur(20px); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 2rem; + text-decoration: none; + color: inherit; + transition: var(--transition-normal); + overflow: hidden; +} + +.service-glow { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(234, 179, 8, 0.15) 0%, transparent 50%); + opacity: 0; + transition: opacity 0.3s; + pointer-events: none; +} + +.service-card:hover .service-glow { + opacity: 1; +} + +.service-card:hover { + transform: translateY(-5px); + border-color: rgba(234, 179, 8, 0.3); + box-shadow: var(--shadow-card); +} + +.service-icon { + width: 56px; + height: 56px; + background: rgba(255,255,255,0.05); + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1.5rem; + color: var(--icon-color); + transition: var(--transition-normal); +} + +.service-icon svg { + width: 28px; + height: 28px; +} + +.service-card:hover .service-icon { + transform: scale(1.1); + background: rgba(234, 179, 8, 0.1); +} + +.service-name { + font-family: 'Space Grotesk', sans-serif; + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.service-desc { + color: var(--color-text-muted); + font-size: 0.9375rem; + margin-bottom: 1.5rem; +} + +.service-status { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.35rem 0.75rem; + background: rgba(34, 197, 94, 0.1); + border-radius: 100px; + font-size: 0.875rem; + color: #22c55e; +} + +.status-dot { + width: 6px; + height: 6px; + background: #22c55e; + border-radius: 50%; + animation: pulse-dot 2s infinite; +} + +.service-arrow { + position: absolute; + top: 2rem; + right: 2rem; + width: 32px; + height: 32px; + background: rgba(255,255,255,0.05); + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transform: translateX(-10px); + transition: var(--transition-normal); +} + +.service-arrow svg { + width: 16px; + height: 16px; +} + +.service-card:hover .service-arrow { + opacity: 1; + transform: translateX(0); +} + +/* ======================================== + Projects Section + ======================================== */ +.projects-section { + background: linear-gradient(180deg, transparent 0%, rgba(139, 92, 246, 0.03) 50%, transparent 100%); +} + +.projects-showcase { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.project-card { + background: var(--color-bg-card); + backdrop-filter: blur(20px); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + overflow: hidden; + transition: var(--transition-normal); +} + +.project-card:hover { + border-color: rgba(234, 179, 8, 0.3); + box-shadow: var(--shadow-card); +} + +.project-card.large { + display: grid; + grid-template-columns: 1fr 1fr; +} + +.project-image { + background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%); + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + min-height: 300px; +} + +.project-visual { + width: 100%; + max-width: 400px; +} + +.code-window { + background: #1e1e2e; + border-radius: var(--radius-md); + overflow: hidden; + box-shadow: 0 20px 60px rgba(0,0,0,0.5); +} + +.window-header { + display: flex; + gap: 6px; + padding: 12px; + background: #181825; +} + +.win-btn { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.win-btn.red { background: #f38ba8; } +.win-btn.yellow { background: #f9e2af; } +.win-btn.green { background: #a6e3a1; } + +.window-content { + padding: 1.5rem; + font-family: 'JetBrains Mono', monospace; + font-size: 0.875rem; +} + +.code-line { + margin-bottom: 0.5rem; +} + +.code-keyword { color: #cba6f7; } +.code-var { color: #f9e2af; } +.code-string { color: #a6e3a1; } +.code-number { color: #fab387; } + +.project-info { + padding: 2rem; + display: flex; + flex-direction: column; + justify-content: center; +} + +.project-tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.tag { + padding: 0.25rem 0.75rem; + background: rgba(255,255,255,0.05); + border: 1px solid var(--color-border); + border-radius: 100px; + font-size: 0.75rem; + font-weight: 500; + color: var(--color-text-muted); +} + +.project-name { + font-family: 'Space Grotesk', sans-serif; + font-size: 1.75rem; + font-weight: 700; + margin-bottom: 1rem; +} + +.project-desc { + color: var(--color-text-muted); + margin-bottom: 1.5rem; +} + +.project-links { + display: flex; + gap: 1rem; +} + +.project-link { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.25rem; + background: rgba(255,255,255,0.05); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + text-decoration: none; + color: var(--color-text); + font-weight: 500; + transition: var(--transition-fast); +} + +.project-link:hover { + background: rgba(234, 179, 8, 0.1); + border-color: rgba(234, 179, 8, 0.3); +} + +.project-link svg { + width: 18px; + height: 18px; +} + +.projects-small { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; +} + +.project-card.small { + padding: 1.5rem; + position: relative; +} + +.project-card.small .project-name { + font-size: 1.25rem; +} + +.project-card.small > .project-link { + position: absolute; + top: 1.5rem; + right: 1.5rem; + padding: 0.5rem; + opacity: 0; + transform: translateX(-10px); +} + +.project-card.small:hover > .project-link { + opacity: 1; + transform: translateX(0); +} + +/* ======================================== + Network Section + ======================================== */ +.network-section { + background: linear-gradient(180deg, transparent 0%, rgba(34, 197, 94, 0.03) 50%, transparent 100%); +} + +.network-map { + position: relative; +} + +.domain-hub { + display: flex; + justify-content: center; + margin-bottom: 3rem; +} + +.hub-center { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 2rem 3rem; + background: var(--color-bg-card); + backdrop-filter: blur(20px); + border: 2px solid var(--color-primary); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-glow); + position: relative; + z-index: 2; +} + +.hub-icon { + font-size: 3rem; + animation: lemon-bounce 2s infinite ease-in-out; +} + +.hub-text { + font-family: 'Space Grotesk', sans-serif; + font-size: 1.25rem; + font-weight: 700; + background: var(--gradient-primary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.domains-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1.5rem; +} + +.domain-node { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 1.5rem; + background: var(--color-bg-card); + backdrop-filter: blur(20px); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + text-decoration: none; + color: inherit; + transition: var(--transition-normal); +} + +.node-pulse { + position: absolute; + inset: -2px; + border-radius: var(--radius-lg); + border: 2px solid var(--color-primary); + opacity: 0; + animation: node-pulse 2s infinite; +} + +@keyframes node-pulse { + 0% { transform: scale(1); opacity: 0.5; } + 100% { transform: scale(1.05); opacity: 0; } +} + +.domain-node:hover { + transform: translateY(-5px); + border-color: var(--color-primary); + box-shadow: var(--shadow-glow); +} + +.domain-node:hover .node-pulse { + opacity: 1; +} + +.node-name { + font-family: 'Space Grotesk', sans-serif; + font-size: 1.125rem; + font-weight: 600; + color: var(--color-primary); +} + +.node-name::before { + content: ''; + color: var(--color-text-muted); +} + +.node-desc { + font-size: 0.875rem; + color: var(--color-text-muted); +} + +/* ======================================== + Contact Section + ======================================== */ +.contact-section { + padding-bottom: 2rem; +} + +.contact-card { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 3rem; + background: var(--color-bg-card); + backdrop-filter: blur(20px); + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); + padding: 3rem; + overflow: hidden; + position: relative; +} + +.contact-card::before { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 50%; + height: 100%; + background: linear-gradient(135deg, rgba(234, 179, 8, 0.05) 0%, transparent 50%); + pointer-events: none; +} + +.contact-content { + display: flex; + flex-direction: column; + justify-content: center; +} + +.contact-title { + font-family: 'Space Grotesk', sans-serif; + font-size: 2.5rem; + font-weight: 700; + margin: 1rem 0; +} + +.contact-desc { + color: var(--color-text-muted); + font-size: 1.125rem; + margin-bottom: 2rem; +} + +.contact-actions { + margin-bottom: 2rem; +} + +.btn-large { + padding: 1.25rem 2rem; + font-size: 1.125rem; +} + +.btn-large svg { + width: 24px; + height: 24px; +} + +.social-links { + display: flex; + gap: 1rem; +} + +.social-link { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255,255,255,0.05); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + color: var(--color-text); + transition: var(--transition-fast); +} + +.social-link:hover { + background: rgba(234, 179, 8, 0.1); + border-color: rgba(234, 179, 8, 0.3); + transform: translateY(-3px); +} + +.social-link svg { + width: 22px; + height: 22px; +} + +.contact-visual { + display: flex; + align-items: center; + justify-content: center; +} + +.terminal { + width: 100%; + max-width: 400px; + background: #0d1117; + border-radius: var(--radius-lg); + overflow: hidden; + box-shadow: 0 20px 60px rgba(0,0,0,0.5); + border: 1px solid #30363d; +} + +.terminal-header { + display: flex; + gap: 8px; + padding: 1rem; + background: #161b22; + border-bottom: 1px solid #30363d; +} + +.terminal-dot { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.terminal-dot.red { background: #ff5f56; } +.terminal-dot.yellow { background: #ffbd2e; } +.terminal-dot.green { background: #27c93f; } + +.terminal-body { + padding: 1.5rem; + font-family: 'JetBrains Mono', monospace; + font-size: 0.9375rem; +} + +.terminal-line { + display: flex; + gap: 0.5rem; + margin-bottom: 0.25rem; +} + +.prompt { + color: #22c55e; +} + +.path { + color: #3b82f6; +} + +.command { + color: var(--color-text); +} + +.terminal-output { + color: var(--color-text-muted); + margin-bottom: 1rem; + padding-left: 1.5rem; +} + +.cursor { + color: var(--color-primary); + animation: cursor-blink 1s infinite; +} + +@keyframes cursor-blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +/* ======================================== + Footer + ======================================== */ +.footer { + padding: 4rem 2rem 2rem; + border-top: 1px solid var(--color-border); +} + +.footer-container { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: 1fr 2fr; + gap: 4rem; + margin-bottom: 3rem; +} + +.footer-brand .logo { + margin-bottom: 1rem; +} + +.footer-tagline { + color: var(--color-text-muted); +} + +.footer-links { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2rem; +} + +.footer-col h4 { + font-family: 'Space Grotesk', sans-serif; + font-size: 1rem; + font-weight: 600; + margin-bottom: 1rem; + color: var(--color-text); +} + +.footer-col a { + display: block; + color: var(--color-text-muted); + text-decoration: none; + padding: 0.35rem 0; + transition: var(--transition-fast); +} + +.footer-col a:hover { + color: var(--color-primary); +} + +.footer-bottom { + max-width: 1200px; + margin: 0 auto; + padding-top: 2rem; + border-top: 1px solid var(--color-border); + display: flex; + justify-content: space-between; + align-items: center; + color: var(--color-text-muted); + font-size: 0.875rem; +} + +.footer-made { + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* ======================================== + Responsive Design + ======================================== */ +@media (max-width: 1024px) { + .hero-visual { + display: none; + } + + .infra-card.featured { + grid-template-columns: 1fr; + } + + .project-card.large { + grid-template-columns: 1fr; + } + + .projects-small { + grid-template-columns: 1fr; + } + + .domains-grid { + grid-template-columns: repeat(2, 1fr); + } + + .contact-card { + grid-template-columns: 1fr; + } + + .contact-visual { + display: none; + } +} + +@media (max-width: 768px) { + .nav-links { + display: none; + } + + .mobile-menu-btn { + display: flex; + } + + .hero-stats { + gap: 2rem; + } + + .homelab-grid { + grid-template-columns: 1fr; + } + + .infra-card.featured { + grid-column: span 1; + } + + .services-grid { + grid-template-columns: 1fr; + } + + .domains-grid { + grid-template-columns: 1fr; + } + + .footer-container { + grid-template-columns: 1fr; + gap: 2rem; + } + + .footer-links { + grid-template-columns: repeat(2, 1fr); + } + + .footer-bottom { + flex-direction: column; + gap: 1rem; + text-align: center; + } +} + +/* ======================================== + Animations + ======================================== */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.section { + animation: fadeInUp 0.8s ease-out; +} + +/* Smooth scrollbar */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: var(--color-bg); +} + +::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-primary); +} + +/* Selection */ +::selection { + background: rgba(234, 179, 8, 0.3); + color: var(--color-text); +}