From 2531ee44028d9122995cdd5c2725d589262dc367 Mon Sep 17 00:00:00 2001 From: ducoterra Date: Wed, 27 May 2026 21:35:15 -0400 Subject: [PATCH] init --- .gitea/workflows/build-push.yml | 40 ++ .gitignore | 10 + Dockerfile | 15 + README.md | 45 ++ homepage.container | 14 + nginx.conf | 44 ++ src/index.html | 409 ++++++++++++++++++ src/script.js | 36 ++ src/style.css | 741 ++++++++++++++++++++++++++++++++ 9 files changed, 1354 insertions(+) create mode 100644 .gitea/workflows/build-push.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 homepage.container create mode 100644 nginx.conf create mode 100644 src/index.html create mode 100644 src/script.js create mode 100644 src/style.css diff --git a/.gitea/workflows/build-push.yml b/.gitea/workflows/build-push.yml new file mode 100644 index 0000000..7e91dbb --- /dev/null +++ b/.gitea/workflows/build-push.yml @@ -0,0 +1,40 @@ +name: Build and Push Container + +on: + push: + branches: + - main + release: + types: + - published + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: gitea + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: gitea.reeseapps.com/services/homepage + tags: | + type=raw,value=main,enable=${{ github.ref == 'refs/heads/main' }} + type=ref,event=tag + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b8b950 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Build artifacts +*.tar +*.tar.gz + +# Local Docker build cache +.docker/ + +# OS files +.DS_Store +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bac99ff --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM docker.io/library/nginx:alpine + +RUN rm /etc/nginx/conf.d/default.conf + +COPY nginx.conf /etc/nginx/conf.d/default.conf + +COPY src/index.html /usr/share/nginx/html/ +COPY src/style.css /usr/share/nginx/html/ +COPY src/script.js /usr/share/nginx/html/ + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ + +CMD wget -qO- http://localhost:8080/ || exit 1 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..738296e --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Developer Homepage + +A static portfolio site packaged for deployment with Podman quadlets. + +## Build & Run with Podman + +### Build the image + +```bash +podman build -t homepage:latest . +``` + +### Run the container + +```bash +podman run --rm -p 8080:8080 -v ./src:/usr/share/nginx/html:z homepage:latest +``` + +Visit `http://localhost:8080` in your browser. + +## Deploy with Quadlet + +1. Copy the container definition to systemd: + +```bash +mkdir -p ~/.config/containers/systemd +cp homepage.container ~/.config/containers/systemd/ +``` + +2. Reload systemd and start the service: + +```bash +systemctl --user daemon-reload +systemctl --user start homepage.container +``` + +3. Enable auto-start on boot: + +```bash +systemctl --user enable homepage.container +``` + +## Development + +Source files are in the `src/` directory. Edit `index.html`, `style.css`, and `script.js` directly. Rebuild the image after making changes. diff --git a/homepage.container b/homepage.container new file mode 100644 index 0000000..d9674c6 --- /dev/null +++ b/homepage.container @@ -0,0 +1,14 @@ +[Unit] +Description=Developer Homepage +After=network.target + +[Container] +ContainerName=homepage +Image=localhost/homepage:latest +AddDevice=/dev/fuse +PublishPort=8080:8080 +HealthCheckUrl=http://localhost:8080/ +Environment=TZ=UTC + +[Install] +WantedBy=default.target diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..ac3781d --- /dev/null +++ b/nginx.conf @@ -0,0 +1,44 @@ +server { + listen 8080; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # 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; + add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';" always; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript; + gzip_min_length 256; + gzip_vary on; + + location / { + try_files $uri $uri/ /index.html; + } + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + } + + # Disable cache for HTML + location ~* \.html$ { + expires -1; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + # Custom 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/src/index.html b/src/index.html new file mode 100644 index 0000000..6ba9e2a --- /dev/null +++ b/src/index.html @@ -0,0 +1,409 @@ + + + + + + Reese Wells - Self-Hosting & Infrastructure + + + + + + + + +
+
+
+
+
+
+
+
+ + Running my own infrastructure +
+

+ Hi, I'm Reese Wells
+ I build and maintain self-hosted systems +

+

+ A systems-focused developer passionate about self-hosting, infrastructure automation, + and building reliable services that just work. From container orchestration to local AI, + I manage a full homelab stack from DNS to deployment. +

+ +
+
+ + +
+
+
+
+
+ + + + + + + + +
+
+
+
+ +

Infrastructure, automation, and self-hosting

+

+ I'm a systems developer who thrives on building and maintaining self-hosted infrastructure. + My work spans the full stack of homelab operations: from OS image building with osbuild + and Ansible-driven deployments, to container orchestration with Podman quadlets and Kubernetes, + to local AI inference and observability. +

+

+ I maintain two main repositories: one for documentation, notes, and tutorials on + self-hosted services, and another for the automation layer that deploys and manages + services across a fleet of servers. Every service runs as a rootless container with + dedicated systemd user sessions, backed by centralized BorgBackup. +

+
+
+
80+
+
Services
+
+
+
2
+
Domains
+
+
+
100%
+
Self-Hosted
+
+
+
+
+
+ + +
+
+ +

What I work with

+

+ A broad toolkit focused on infrastructure, automation, and self-hosted services. +

+
+
+
+

Container Orchestration

+

Rootless containers managed via Podman quadlets, Docker Compose, and Kubernetes clusters with Helm charts.

+
+ Podman + Docker + Kubernetes + k3s +
+
+
+
+

Infrastructure Automation

+

Ansible playbooks drive deployments across a multi-server fleet with strict SOP ordering and centralized configuration.

+
+ Ansible + osbuild + systemd + BorgBackup +
+
+
+
+

Networking & DNS

+

AWS Route53 powers all DNS management with DDNS auto-updating, Caddy reverse proxy with Route53 DNS-validated TLS, and dual-domain strategy.

+
+ Route53 + Caddy + Nginx + WireGuard +
+
+
+
🤖
+

Local AI & ML

+

Full local AI stack: Ollama, LiteLLM, LocalAI for inference, Langfuse for observability, with CUDA and ROCm support.

+
+ Ollama + LocalAI + Langfuse + ROCm +
+
+
+
+

Python

+

Python is the backbone of the homelab: DDNS updates, fleet-wide deployment scripts, AWS integration, and automation tooling with boto3, rich, and uv.

+
+ Python + boto3 + uv + mypy +
+
+
+
+
+ + +
+ +

What I've built

+

+ A selection of projects from my homelab and deployment infrastructure. +

+
+
+
+
+
version: '3.8'
+
services:
+
  caddy:
+
    image: caddy:2-alpine
+
    networks:
+
      - default
+
    labels:
+
      - "caddy.*.reeseapps.com"
+
+
+
+

Reverse Proxy Infrastructure

+

Caddy and Nginx reverse proxies serving all *.reeseapps.com domains with AWS Route53 DNS-validated TLS. DDNS auto-updates IPv4/IPv6 records across the fleet.

+
+
+ Caddy + Nginx + Route53 + Podman +
+ +
+
+
+
+
+
+
def update_record(domain):
+
  ipv4 = get_public_ip()
+
  record = route53.find(domain)
+
  if record.value != ipv4:
+
    route53.update(record, ipv4)
+
    log(f"Updated {domain}")
+
 
+
# Run every 5 minutes
+
+
+
+

Dynamic DNS Service

+

Automated DDNS keeping AWS Route53 records updated for all servers. Manages dual-domain strategy: reeseapps.com for public services and reeselink.com for internal machine-to-machine connections.

+
+
+ Python + AWS CLI + Route53 + Podman +
+ +
+
+
+
+
+
+
from ollama import Client
+
 
+
client = Client("http://localhost:11434")
+
response = client.chat(
+
  model="llama3",
+
  messages=[...]
+
)
+
 
+
# LiteLLM proxy for unified API
+
+
+
+

Local AI Stack

+

Complete local AI infrastructure: Ollama and LocalAI for inference, LiteLLM as a unified API proxy, Bifrost for model routing, and Langfuse for observability. Supports both CUDA and ROCm.

+
+
+ Ollama + LocalAI + LiteLLM + Langfuse +
+ +
+
+
+
+
+
+
# Ansible playbook
+
- name: Deploy Gitea
+
  hosts: gitea
+
  tasks:
+
  - docker_compose_v2:
+
    project_src: /opt/gitea
+
    state: present
+
+
+
+

Deployment Automation

+

Ansible-driven deployment pipeline with strict SOP ordering (osbuild -> ddns -> caddy -> nginx -> ntfy -> gitea). Each service runs as a rootless container with dedicated systemd user sessions and centralized BorgBackup.

+
+
+ Ansible + Podman + systemd + Borg +
+ +
+
+
+
+
+
+
# Self-hosted services
+
- Immich # Photo/video management
+
- Jellyfin # Media streaming
+
- Nextcloud # Cloud storage & sync
+
- Gitea # Git service
+
- Matrix # Chat protocol
+
- Home Assistant # Smart home
+
- Pi-hole # DNS ad blocking
+
+
+
+

Self-Hosted Services

+

A diverse fleet of self-hosted services: Immich for photos, Jellyfin for media, Nextcloud for storage, Matrix for chat, Home Assistant for IoT, and more. Each running as rootless Podman containers with SELinux awareness.

+
+
+ Immich + Jellyfin + Nextcloud + Matrix +
+ +
+
+
+
+
+
+
# Kubernetes with k3s
+
- metallb # L2 load balancer
+
- longhorn # Distributed storage
+
- traefik # Ingress gateway
+
- external-dns # Route53 integration
+
- grafana # Metrics dashboards
+
- minecraft # Game server
+
+
+
+

Kubernetes Cluster

+

k3s and k0s Kubernetes clusters with MetalLB for L2 failover, Longhorn for distributed storage, Traefik/Nginx ingress, cert-manager with Route53 DNS challenge, and Helm charts for service deployment.

+
+
+ k3s + Kubernetes + Helm + MetalLB +
+ +
+
+
+
+
+ + +
+ +

Let's connect

+

+ Always open to discussing self-hosting, infrastructure, open source, or just sharing homelab stories. +

+ +
+ + + + + + + diff --git a/src/script.js b/src/script.js new file mode 100644 index 0000000..346eb63 --- /dev/null +++ b/src/script.js @@ -0,0 +1,36 @@ +// Mobile menu toggle +const hamburger = document.getElementById('hamburger'); +const navLinks = document.getElementById('navLinks'); + +hamburger.addEventListener('click', () => { + navLinks.classList.toggle('active'); +}); + +// Close menu on link click +navLinks.querySelectorAll('a').forEach(link => { + link.addEventListener('click', () => { + navLinks.classList.remove('active'); + }); +}); + +// Scroll animations +const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + } + }); +}, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' }); + +document.querySelectorAll('.fade-in').forEach(el => observer.observe(el)); + +// Smooth scroll for nav links +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' }); + } + }); +}); diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..4ea6c70 --- /dev/null +++ b/src/style.css @@ -0,0 +1,741 @@ +*, *::before, *::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --bg-primary: #0a0a0b; + --bg-secondary: #111113; + --bg-card: #18181b; + --border: #27272a; + --text-primary: #fafafa; + --text-secondary: #a1a1aa; + --text-muted: #71717a; + --accent: #eab308; + --accent-hover: #facc15; + --accent-glow: rgba(234, 179, 8, 0.15); + --gradient-1: linear-gradient(135deg, #eab308, #ca8a04, #a16207); + --gradient-2: linear-gradient(135deg, #eab308, #ca8a04); +} + +html { + scroll-behavior: smooth; + scrollbar-width: thin; + scrollbar-color: var(--border) var(--bg-primary); +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.6; + overflow-x: hidden; +} + +::selection { + background: var(--accent); + color: white; +} + +/* Navigation */ +nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + padding: 1rem 2rem; + background: rgba(10, 10, 11, 0.8); + backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + transition: transform 0.3s ease; +} + +nav .nav-inner { + max-width: 1200px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo { + font-size: 1.25rem; + font-weight: 700; + background: var(--gradient-1); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + letter-spacing: -0.02em; +} + +.nav-links { + display: flex; + gap: 2rem; + list-style: none; +} + +.nav-links a { + color: var(--text-secondary); + text-decoration: none; + font-size: 0.875rem; + font-weight: 500; + transition: color 0.2s; + position: relative; +} + +.nav-links a:hover { + color: var(--text-primary); +} + +.nav-links a::after { + content: ''; + position: absolute; + bottom: -4px; + left: 0; + width: 0; + height: 2px; + background: var(--gradient-1); + transition: width 0.3s ease; + border-radius: 1px; +} + +.nav-links a:hover::after { + width: 100%; +} + +.hamburger { + display: none; + flex-direction: column; + gap: 5px; + cursor: pointer; + background: none; + border: none; + padding: 4px; +} + +.hamburger span { + display: block; + width: 24px; + height: 2px; + background: var(--text-primary); + border-radius: 1px; + transition: all 0.3s; +} + +/* Hero Section */ +.hero { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 6rem 2rem 4rem; + position: relative; +} + +.hero-bg { + position: absolute; + inset: 0; + overflow: hidden; + pointer-events: none; +} + +.hero-bg .orb { + position: absolute; + border-radius: 50%; + filter: blur(80px); + opacity: 0.4; + animation: float 8s ease-in-out infinite; +} + +.hero-bg .orb-1 { + width: 400px; + height: 400px; + background: var(--accent); + top: 10%; + left: 15%; + animation-delay: 0s; +} + +.hero-bg .orb-2 { + width: 300px; + height: 300px; + background: #ca8a04; + bottom: 15%; + right: 15%; + animation-delay: -3s; +} + +.hero-bg .orb-3 { + width: 200px; + height: 200px; + background: #eab308; + top: 50%; + right: 30%; + animation-delay: -5s; +} + +@keyframes float { + 0%, 100% { transform: translate(0, 0) scale(1); } + 33% { transform: translate(30px, -20px) scale(1.05); } + 66% { transform: translate(-20px, 20px) scale(0.95); } +} + +.hero-content { + max-width: 800px; + text-align: center; + position: relative; + z-index: 1; +} + +.hero-badge { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.375rem 1rem; + background: var(--accent-glow); + border: 1px solid rgba(234, 179, 8, 0.3); + border-radius: 100px; + font-size: 0.8rem; + color: var(--accent-hover); + margin-bottom: 2rem; + font-weight: 500; +} + +.hero-badge .dot { + width: 6px; + height: 6px; + background: #22c55e; + border-radius: 50%; + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +.hero h1 { + font-size: clamp(2.5rem, 6vw, 4.5rem); + font-weight: 800; + line-height: 1.1; + letter-spacing: -0.03em; + margin-bottom: 1.5rem; +} + +.hero h1 .gradient-text { + background: var(--gradient-1); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero p { + font-size: 1.15rem; + color: var(--text-secondary); + max-width: 560px; + margin: 0 auto 2.5rem; + line-height: 1.7; +} + +.hero-buttons { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.75rem; + border-radius: 10px; + font-size: 0.9rem; + font-weight: 600; + text-decoration: none; + transition: all 0.25s ease; + cursor: pointer; + border: none; +} + +.btn-primary { + background: var(--gradient-1); + color: white; + box-shadow: 0 4px 20px var(--accent-glow); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 30px rgba(234, 179, 8, 0.3); +} + +.btn-secondary { + background: var(--bg-card); + color: var(--text-primary); + border: 1px solid var(--border); +} + +.btn-secondary:hover { + background: var(--border); + transform: translateY(-2px); +} + +/* Sections */ +section { + padding: 6rem 2rem; + max-width: 1200px; + margin: 0 auto; +} + +.section-label { + font-size: 0.8rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--accent-hover); + margin-bottom: 0.75rem; +} + +.section-title { + font-size: clamp(1.75rem, 4vw, 2.5rem); + font-weight: 800; + letter-spacing: -0.02em; + margin-bottom: 1rem; +} + +.section-desc { + color: var(--text-secondary); + max-width: 560px; + margin-bottom: 3rem; + font-size: 1.05rem; +} + +/* About */ +.about-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4rem; + align-items: center; +} + +.about-image { + position: relative; +} + +.about-image .avatar-frame { + width: 100%; + aspect-ratio: 1; + max-width: 360px; + margin: 0 auto; + display: block; + background: var(--gradient-1); + border-radius: 24px; + padding: 3px; + position: relative; +} + +.about-image .avatar-inner { + width: 100%; + height: 100%; + border-radius: 22px; + background: var(--bg-secondary); + display: flex; + align-items: center; + justify-content: center; + font-size: 6rem; + overflow: hidden; +} + +.about-image .avatar-inner svg { + width: 60%; + height: 60%; + opacity: 0.6; +} + +.about-text p { + color: var(--text-secondary); + margin-bottom: 1.25rem; + font-size: 1.02rem; +} + +.about-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + margin-top: 2rem; +} + +.stat { + text-align: center; + padding: 1.25rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; +} + +.stat-number { + font-size: 1.75rem; + font-weight: 800; + background: var(--gradient-1); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stat-label { + font-size: 0.75rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-top: 0.25rem; +} + +/* Skills */ +.skills-section { + background: var(--bg-secondary); + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + max-width: 100%; + padding: 6rem 2rem; +} + +.skills-section .skills-inner { + max-width: 1200px; + margin: 0 auto; +} + +.skills-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.25rem; +} + +.skill-card { + padding: 1.75rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 16px; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.skill-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: var(--gradient-1); + opacity: 0; + transition: opacity 0.3s; +} + +.skill-card:hover { + border-color: rgba(234, 179, 8, 0.3); + transform: translateY(-4px); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3); +} + +.skill-card:hover::before { + opacity: 1; +} + +.skill-icon { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: var(--accent-glow); + border: 1px solid rgba(234, 179, 8, 0.2); + border-radius: 12px; + font-size: 1.5rem; + margin-bottom: 1.25rem; +} + +.skill-card h3 { + font-size: 1.1rem; + font-weight: 700; + margin-bottom: 0.5rem; +} + +.skill-card p { + color: var(--text-secondary); + font-size: 0.875rem; + line-height: 1.6; +} + +.skill-tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 1rem; +} + +.skill-tag { + padding: 0.25rem 0.625rem; + background: rgba(234, 179, 8, 0.1); + border: 1px solid rgba(234, 179, 8, 0.15); + border-radius: 6px; + font-size: 0.75rem; + color: var(--accent-hover); + font-weight: 500; +} + +/* Projects */ +.projects-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); + gap: 1.5rem; +} + +.project-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 16px; + overflow: hidden; + transition: all 0.3s ease; +} + +.project-card:hover { + border-color: rgba(234, 179, 8, 0.3); + transform: translateY(-4px); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3); +} + +.project-preview { + height: 200px; + background: var(--bg-secondary); + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; +} + +.project-preview .code-block { + font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; + font-size: 0.75rem; + color: var(--text-muted); + padding: 1.5rem; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: flex-start; +} + +.code-line { + line-height: 1.8; +} + +.code-keyword { color: #c084fc; } +.code-function { color: #60a5fa; } +.code-string { color: #34d399; } +.code-comment { color: #52525b; font-style: italic; } +.code-variable { color: #f472b6; } +.code-operator { color: #fbbf24; } + +.project-info { + padding: 1.5rem; +} + +.project-info h3 { + font-size: 1.15rem; + font-weight: 700; + margin-bottom: 0.5rem; +} + +.project-info p { + color: var(--text-secondary); + font-size: 0.875rem; + line-height: 1.6; + margin-bottom: 1.25rem; +} + +.project-meta { + display: flex; + justify-content: space-between; + align-items: center; +} + +.project-tech { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.project-tech span { + padding: 0.2rem 0.5rem; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 6px; + font-size: 0.7rem; + color: var(--text-muted); + font-weight: 500; +} + +.project-links { + display: flex; + gap: 0.75rem; +} + +.project-links a { + color: var(--text-muted); + transition: color 0.2s; + display: flex; + align-items: center; +} + +.project-links a:hover { + color: var(--accent-hover); +} + +/* Contact */ +.contact-section { + text-align: center; +} + +.contact-section .section-desc { + margin-left: auto; + margin-right: auto; +} + +.contact-links { + display: flex; + justify-content: center; + gap: 1.25rem; + flex-wrap: wrap; + margin-top: 2rem; +} + +.contact-link { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1rem 1.75rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + text-decoration: none; + color: var(--text-primary); + transition: all 0.25s; + font-weight: 500; +} + +.contact-link:hover { + border-color: rgba(234, 179, 8, 0.3); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2); +} + +.contact-link .link-icon { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + background: var(--accent-glow); + border-radius: 8px; + font-size: 1.1rem; +} + +/* Footer */ +footer { + padding: 3rem 2rem; + text-align: center; + border-top: 1px solid var(--border); + max-width: 1200px; + margin: 0 auto; +} + +footer p { + color: var(--text-muted); + font-size: 0.85rem; +} + +/* Scroll animations */ +.fade-in { + opacity: 0; + transform: translateY(30px); + transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1); +} + +.fade-in.visible { + opacity: 1; + transform: translateY(0); +} + +/* Responsive */ +@media (max-width: 768px) { + .nav-links { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + flex-direction: column; + gap: 0; + background: rgba(10, 10, 11, 0.95); + backdrop-filter: blur(20px); + border-bottom: 1px solid var(--border); + padding: 1rem 2rem; + } + + .nav-links.active { + display: flex; + } + + .nav-links li { + padding: 0.75rem 0; + } + + .hamburger { + display: flex; + } + + .about-grid { + grid-template-columns: 1fr; + gap: 2.5rem; + } + + .about-image { + order: -1; + } + + .about-image .avatar-frame { + max-width: 240px; + } + + .about-stats { + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; + } + + .projects-grid { + grid-template-columns: 1fr; + } + + section { + padding: 4rem 1.25rem; + } + + .hero { + padding: 5rem 1.25rem 3rem; + } + + .contact-links { + flex-direction: column; + align-items: center; + } +} + +@media (max-width: 480px) { + .about-stats { + grid-template-columns: 1fr; + } +}