NEW RELEASE
This commit is contained in:
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
# --- System / shell files (root home directory ki wajah se) ---
|
||||
.bash_history
|
||||
.bashrc
|
||||
.profile
|
||||
.lesshst
|
||||
.wget-hsts
|
||||
.cloud-locale-test.skip
|
||||
.Xauthority
|
||||
|
||||
# --- Credentials & secrets (KABHI git mein na jayen) ---
|
||||
.ssh/
|
||||
*.pem
|
||||
*.key
|
||||
*.env
|
||||
.env*
|
||||
|
||||
# --- Caches & local data ---
|
||||
.cache/
|
||||
.local/
|
||||
.thumbnail/
|
||||
|
||||
# --- Editor / IDE files ---
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# --- OS files ---
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# --- Node (agar future mein npm use ho) ---
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
|
||||
# --- Logs & backups ---
|
||||
*.log
|
||||
*.bak
|
||||
*~
|
||||
BIN
.thumbnail
Normal file
BIN
.thumbnail
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
288
README.md
Normal file
288
README.md
Normal file
@@ -0,0 +1,288 @@
|
||||
<div align="center">
|
||||
|
||||
# 🌐 Muaz Bhutta — Portfolio
|
||||
|
||||
**A blazing-fast, zero-dependency static portfolio with automated CI/CD deployment**
|
||||
|
||||
[](https://muazbhutta.online)
|
||||
[](#-cicd--auto-deploy-on-every-push)
|
||||
[](LICENSE)
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
*No build step. No framework. No node_modules black hole. Just clean HTML/CSS/JS — deployed automatically to a self-hosted VPS on every push.*
|
||||
|
||||
[Live Demo](https://muazbhutta.online) · [Report Bug](../../issues) · [Request Feature](../../issues)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [✨ Features](#-features)
|
||||
- [🏗️ Architecture](#️-architecture)
|
||||
- [📁 File Structure](#-file-structure)
|
||||
- [✏️ Updating Content](#️-updating-content-the-important-part)
|
||||
- [🚀 Getting Started](#-getting-started)
|
||||
- [Run Locally](#step-1--run-it-locally)
|
||||
- [Push to GitHub](#step-2--push-to-github)
|
||||
- [Manual Deploy](#step-3--manual-deploy-to-the-vps)
|
||||
- [CI/CD Setup](#step-4--cicd--auto-deploy-on-every-push)
|
||||
- [🔁 Deployment Flow](#-deployment-flow)
|
||||
- [🛠️ Tech Stack](#️-tech-stack)
|
||||
- [📄 License](#-license)
|
||||
- [📬 Contact](#-contact)
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
| Feature | Description |
|
||||
|---|---|
|
||||
| ⚡ **Fully Static** | Zero dependencies beyond Google Fonts — loads instantly |
|
||||
| 🌗 **Light / Dark / Auto Theme** | Auto mode follows the visitor's system setting and updates **live** if it changes |
|
||||
| 📜 **Scroll-Reveal Timeline** | Animated career timeline, fully responsive down to mobile |
|
||||
| 🗂️ **Single-File Content** | All content lives in `data.js` — edit one file, never touch markup |
|
||||
| 🤖 **Auto-Deploy CI/CD** | Every push to `main` deploys to the VPS via GitHub Actions + rsync |
|
||||
| 🔒 **Self-Hosted** | Served by Caddy with automatic HTTPS on a personal VPS |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
```
|
||||
┌─────────────┐ git push ┌──────────────────┐
|
||||
│ Developer │ ────────────────▶ │ GitHub (main) │
|
||||
└─────────────┘ └────────┬─────────┘
|
||||
│ triggers
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ GitHub Actions │
|
||||
│ (deploy.yml) │
|
||||
└────────┬─────────┘
|
||||
│ rsync over SSH
|
||||
▼
|
||||
┌─────────────┐ HTTPS (443) ┌──────────────────┐
|
||||
│ Visitor │ ◀──────────────── │ VPS + Caddy │
|
||||
└─────────────┘ auto-SSL 🔒 │ /var/www/portfolio│
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 File Structure
|
||||
|
||||
```
|
||||
portfolio/
|
||||
├── index.html → page structure
|
||||
├── style.css → design & theme tokens
|
||||
├── script.js → renders data.js into the page (no need to edit)
|
||||
├── data.js → ⭐ EDIT THIS FILE to add content
|
||||
├── .gitignore → keeps secrets & system files out of git
|
||||
└── .github/
|
||||
└── workflows/
|
||||
└── deploy.yml → auto-deploy pipeline
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✏️ Updating Content (the important part)
|
||||
|
||||
To add a new project, open **`data.js`** and add an object to the `PROJECTS` array:
|
||||
|
||||
```js
|
||||
{
|
||||
id: "unique-id",
|
||||
title: "Project Name",
|
||||
summary: "1–2 line description.",
|
||||
stack: ["Tech1", "Tech2"],
|
||||
link: "https://...", // or "#" if there's no link
|
||||
status: "production", // "production" or "lab"
|
||||
},
|
||||
```
|
||||
|
||||
> 💡 **Skills, credentials, About paragraphs, and the career timeline all work the same way** — add or edit an object in the matching array. Nothing in `index.html`, `style.css`, or `script.js` ever needs to change.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Step 1 — Run it locally
|
||||
|
||||
These are static files, so "running" it just means opening it in a browser:
|
||||
|
||||
**Option A (simplest):**
|
||||
```
|
||||
double-click index.html
|
||||
```
|
||||
|
||||
**Option B (recommended — mirrors production behavior):**
|
||||
```bash
|
||||
python3 -m http.server 8000
|
||||
```
|
||||
Then open **http://localhost:8000**. This matches how Caddy serves the files on the VPS, so local behavior = production behavior.
|
||||
|
||||
---
|
||||
|
||||
### Step 2 — Push to GitHub
|
||||
|
||||
```bash
|
||||
cd portfolio
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial portfolio"
|
||||
git branch -M main
|
||||
git remote add origin https://github.com/<your-username>/portfolio.git
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
Why this matters:
|
||||
1. 📌 Your code becomes **version-controlled** — every change is tracked
|
||||
2. 🤖 It becomes the **trigger source** for the CI/CD pipeline — every push to `main` auto-deploys
|
||||
|
||||
---
|
||||
|
||||
### Step 3 — Manual Deploy to the VPS
|
||||
|
||||
*(first time, or without CI/CD)*
|
||||
|
||||
The VPS already runs Caddy serving `muazbhutta.online` — just copy files into the directory Caddy points at:
|
||||
|
||||
```bash
|
||||
# from your local machine
|
||||
rsync -avz --delete ./portfolio/ user@your-server-ip:/var/www/portfolio/
|
||||
```
|
||||
|
||||
Confirm the Caddyfile block:
|
||||
|
||||
```caddy
|
||||
muazbhutta.online {
|
||||
root * /var/www/portfolio
|
||||
file_server
|
||||
}
|
||||
```
|
||||
|
||||
Then reload:
|
||||
|
||||
```bash
|
||||
sudo systemctl reload caddy
|
||||
```
|
||||
|
||||
✅ **`https://muazbhutta.online` is now live** — Caddy handles SSL automatically.
|
||||
|
||||
> 🔀 **Want a subdomain instead?** (e.g. `portfolio.muazbhutta.online`) — add a new Caddyfile block with the same directives, using the subdomain as the site address.
|
||||
|
||||
---
|
||||
|
||||
### Step 4 — CI/CD: Auto-Deploy on Every Push
|
||||
|
||||
`.github/workflows/deploy.yml` uses **GitHub Actions** to rsync files to the VPS over SSH on every push to `main`.
|
||||
|
||||
#### 🔑 Required Repository Secrets
|
||||
|
||||
Go to: **Repo → Settings → Secrets and variables → Actions → New repository secret**
|
||||
|
||||
| Secret | Value |
|
||||
|---|---|
|
||||
| `DEPLOY_HOST` | Your VPS IP address |
|
||||
| `DEPLOY_USER` | SSH username on the VPS |
|
||||
| `DEPLOY_PORT` | `22` (or your custom SSH port) |
|
||||
| `DEPLOY_PATH` | `/var/www/portfolio/` |
|
||||
| `DEPLOY_SSH_KEY` | Private SSH deploy key *(see below)* |
|
||||
|
||||
#### 🗝️ Generating a Dedicated Deploy Key *(best practice)*
|
||||
|
||||
```bash
|
||||
# on your local machine
|
||||
ssh-keygen -t ed25519 -C "github-deploy" -f ./deploy_key -N ""
|
||||
```
|
||||
|
||||
This creates `deploy_key` (private) + `deploy_key.pub` (public).
|
||||
|
||||
Authorize the **public** key on the VPS:
|
||||
|
||||
```bash
|
||||
ssh-copy-id -i deploy_key.pub user@your-server-ip
|
||||
```
|
||||
|
||||
*(or manually append `deploy_key.pub` contents to `~/.ssh/authorized_keys` on the VPS)*
|
||||
|
||||
Then paste the **full contents of `deploy_key`** (the private key) into the `DEPLOY_SSH_KEY` secret on GitHub.
|
||||
|
||||
> ⚠️ **Never commit the private key to the repo.** It lives only in GitHub Secrets. The included `.gitignore` blocks `*.key` and `.ssh/` as a safety net.
|
||||
|
||||
#### 🧪 Testing the Pipeline
|
||||
|
||||
```bash
|
||||
# make a small change, e.g. add a project in data.js
|
||||
git add .
|
||||
git commit -m "Add new project"
|
||||
git push
|
||||
```
|
||||
|
||||
Watch the **Actions** tab → pipeline runs → within **30–60 seconds** the change is live. No manual rsync. No SSH. ✨
|
||||
|
||||
---
|
||||
|
||||
## 🔁 Deployment Flow
|
||||
|
||||
```
|
||||
Edit data.js (add a project / skill / timeline entry)
|
||||
│
|
||||
▼
|
||||
git add . && git commit -m "update" && git push
|
||||
│
|
||||
▼
|
||||
GitHub Actions triggers automatically
|
||||
│
|
||||
▼
|
||||
rsync copies files to VPS over SSH
|
||||
│
|
||||
▼
|
||||
Caddy is already serving → change is LIVE 🎉
|
||||
```
|
||||
|
||||
**From now on: edit `data.js`, push, done.** Everything else is automatic.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
| Layer | Technology | Why |
|
||||
|---|---|---|
|
||||
| **Frontend** | Vanilla HTML5 / CSS3 / JS | Zero build step, instant loads, easy maintenance |
|
||||
| **Theming** | CSS custom properties + `prefers-color-scheme` | Native Light/Dark/Auto with live OS sync |
|
||||
| **Web Server** | [Caddy](https://caddyserver.com/) | Automatic HTTPS, dead-simple config |
|
||||
| **CI/CD** | GitHub Actions | Free, integrated, zero-maintenance pipeline |
|
||||
| **Transport** | rsync over SSH | Fast delta transfers — only changed files move |
|
||||
| **Hosting** | Self-hosted VPS | Full control, part of a larger self-hosted infra lab |
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
Distributed under the **MIT License**. See `LICENSE` for more information.
|
||||
|
||||
---
|
||||
|
||||
## 📬 Contact
|
||||
|
||||
**Muaz Bhutta** — Junior Network / Cloud & DevOps Engineer
|
||||
|
||||
[](https://muazbhutta.online)
|
||||
[](https://github.com/your-username)
|
||||
[](https://linkedin.com/in/your-profile)
|
||||
|
||||
<div align="center">
|
||||
|
||||
⭐ **If this repo helped you build your own portfolio pipeline, consider giving it a star!** ⭐
|
||||
|
||||
*Built with ❤️ and zero frameworks*
|
||||
|
||||
</div>
|
||||
104
data.js
Normal file
104
data.js
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
============================================================
|
||||
DATA FILE — this is the only file you should need to touch.
|
||||
To add a new project, skill, or credential, add a new object
|
||||
to the relevant array below. The layout/design does not need
|
||||
to change — everything renders automatically from here.
|
||||
============================================================
|
||||
*/
|
||||
|
||||
const SITE = {
|
||||
name: "Muaz Bhutta",
|
||||
initials: "MB",
|
||||
role: "Cloud & Network Infrastructure Engineer",
|
||||
tagline:
|
||||
"I build infrastructure that runs — and I understand exactly why it runs. From routing tables to container orchestration, from VPNs to observability stacks.",
|
||||
location: "Jeddah / Riyadh, Saudi Arabia",
|
||||
status: "Open to work",
|
||||
email: "you@muazbhutta.online", // put your real email here
|
||||
github: "https://github.com/yourusername",
|
||||
linkedin: "https://linkedin.com/in/yourusername",
|
||||
resumeLink: "#", // link to your resume PDF, e.g. /assets/resume.pdf
|
||||
};
|
||||
|
||||
// About section — short paragraphs, rendered as two columns.
|
||||
const ABOUT = [
|
||||
"I'm a Cloud & Network Infrastructure Engineer based between Jeddah and Riyadh, with a background in Computer Science and hands-on experience across routing, virtualization, and cloud platforms.",
|
||||
"My foundation is networking — CCNA-level routing and switching, VLAN design, and VPN architecture. I extended that into the cloud with AWS and self-managed Linux infrastructure, treating every VPS like a production environment.",
|
||||
"I learn by building and breaking things on my own servers: VPN dashboards, monitoring stacks, and internal APIs that I design, deploy, and keep running end to end.",
|
||||
"I'm currently looking for a junior network or cloud engineering role in Saudi Arabia, where I can bring that same hands-on discipline to a team solving real infrastructure problems.",
|
||||
];
|
||||
|
||||
// Career path — used for the timeline under the About section.
|
||||
const CAREER_PATH = [
|
||||
{ step: 1, label: "Multan, Pakistan", detail: "BS in Computer Science" },
|
||||
{ step: 2, label: "CCNA", detail: "Routing & switching foundations" },
|
||||
{ step: 3, label: "AWS + Linux", detail: "Cloud platforms & systems administration" },
|
||||
{ step: 4, label: "Self-hosted labs", detail: "VPNs, monitoring, and APIs — running live on a VPS" },
|
||||
{ step: 5, label: "Jeddah / Riyadh", detail: "Target: Junior Network / Cloud Engineer" },
|
||||
];
|
||||
|
||||
// Skills, grouped by category.
|
||||
const SKILLS = [
|
||||
{ group: "Networking", items: ["CCNA (R&S)", "VLANs & Subnetting", "OpenVPN", "WireGuard"] },
|
||||
{ group: "Cloud", items: ["AWS (EC2, S3, IAM)", "VPS Administration", "Caddy / Nginx", "DNS & Domain Management"] },
|
||||
{ group: "DevOps", items: ["Docker & Compose", "Prometheus + Grafana", "CI/CD (GitHub Actions)", "Bash Scripting"] },
|
||||
{ group: "Development", items: ["Linux (Ubuntu/Debian)", "Python + Flask", "REST APIs", "Git / GitHub"] },
|
||||
];
|
||||
|
||||
// Projects. Add a new object here to add a new project card.
|
||||
// status: "production" or "lab"
|
||||
const PROJECTS = [
|
||||
{
|
||||
id: "vpn-dashboard",
|
||||
title: "OpenVPN Management Dashboard",
|
||||
summary:
|
||||
"A full VPN control panel built and deployed on my own VPS — live client monitoring, connection status, and real-time updates via Server-Sent Events.",
|
||||
stack: ["Flask", "OpenVPN", "Easy-RSA", "SSE", "Linux"],
|
||||
link: "#",
|
||||
status: "production",
|
||||
},
|
||||
{
|
||||
id: "monitoring-stack",
|
||||
title: "Prometheus + Grafana Monitoring",
|
||||
summary:
|
||||
"A monitoring stack deployed with Docker Compose — Node Exporter collects system metrics, visualized on Grafana dashboards in real time.",
|
||||
stack: ["Docker Compose", "Prometheus", "Grafana", "Node Exporter"],
|
||||
link: "#",
|
||||
status: "production",
|
||||
},
|
||||
{
|
||||
id: "system-api",
|
||||
title: "Real-Time System Status API",
|
||||
summary:
|
||||
"A Flask API that streams live CPU, RAM, and disk metrics via psutil over Server-Sent Events, paired with a dark-themed live dashboard.",
|
||||
stack: ["Python", "Flask", "psutil", "SSE"],
|
||||
link: "#",
|
||||
status: "production",
|
||||
},
|
||||
{
|
||||
id: "wireguard-lab",
|
||||
title: "WireGuard VPN Lab",
|
||||
summary:
|
||||
"A WireGuard setup with WGDashboard integration, including troubleshooting mobile connectivity issues caused by carrier-level UDP blocking.",
|
||||
stack: ["WireGuard", "WGDashboard", "Networking"],
|
||||
link: "#",
|
||||
status: "lab",
|
||||
},
|
||||
{
|
||||
id: "this-site",
|
||||
title: "This Portfolio",
|
||||
summary:
|
||||
"A static site that deploys itself to my own VPS through GitHub Actions — the CI/CD pipeline is itself part of this project.",
|
||||
stack: ["HTML/CSS/JS", "GitHub Actions", "Caddy", "VPS"],
|
||||
link: "https://github.com/yourusername/portfolio",
|
||||
status: "production",
|
||||
},
|
||||
];
|
||||
|
||||
// Certifications / education.
|
||||
const CREDENTIALS = [
|
||||
{ title: "BS Computer Science", org: "University", year: "" },
|
||||
{ title: "CCNA (200-301)", org: "Cisco", year: "In progress" },
|
||||
{ title: "AWS Cloud Practitioner", org: "Amazon Web Services", year: "" },
|
||||
];
|
||||
23
deploy.yml
Normal file
23
deploy.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Deploy Portfolio
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy via rsync over SSH
|
||||
uses: burnett01/rsync-deployments@7.0.1
|
||||
with:
|
||||
switches: -avzr --delete --exclude=".git*" --exclude="README.md"
|
||||
path: ./
|
||||
remote_path: ${{ secrets.DEPLOY_PATH }}
|
||||
remote_host: ${{ secrets.DEPLOY_HOST }}
|
||||
remote_user: ${{ secrets.DEPLOY_USER }}
|
||||
remote_key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
remote_port: ${{ secrets.DEPLOY_PORT }}
|
||||
589
index.html
Normal file
589
index.html
Normal file
@@ -0,0 +1,589 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="./support.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<x-dc>
|
||||
<helmet>
|
||||
<title>Muaz Bhutta — Cloud & Network Infrastructure Engineer</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
html, body { margin:0; padding:0; background:#080d1f; scroll-behavior:smooth; }
|
||||
* { box-sizing:border-box; }
|
||||
::selection { background:#2ee6ff; color:#04060d; }
|
||||
@keyframes blink { 0%,49% { opacity:1; } 50%,100% { opacity:0; } }
|
||||
@keyframes pulseDot { 0%,100% { box-shadow:0 0 0 0 rgba(46,230,255,0.5); } 50% { box-shadow:0 0 0 8px rgba(46,230,255,0); } }
|
||||
@keyframes hopGlow { 0%, 12% { opacity:1; filter:drop-shadow(0 0 12px currentColor); } 20%, 100% { opacity:0.35; filter:none; } }
|
||||
@keyframes floaty { 0%,100% { transform:translateY(0); } 50% { transform:translateY(-10px); } }
|
||||
@keyframes scan { 0% { transform:translateY(-100%); } 100% { transform:translateY(1200%); } }
|
||||
@keyframes dashMove { to { stroke-dashoffset:-24; } }
|
||||
@keyframes gradientShift { 0% { background-position:0% 50%; } 50% { background-position:100% 50%; } 100% { background-position:0% 50%; } }
|
||||
/* Mobile responsiveness */
|
||||
#nav-toggle { display:none; }
|
||||
@media (max-width: 760px) {
|
||||
#nav-links { display:none !important; }
|
||||
#nav-toggle { display:flex !important; }
|
||||
#hero-status { position:static !important; margin-top:40px; animation:none !important; max-width:100%; }
|
||||
#hero-inner { padding:100px 24px 8px !important; order:1; }
|
||||
#hero-section { flex-direction:column; align-items:stretch; min-height:0 !important; padding-bottom:24px; }
|
||||
#hero-canvas-wrap { position:relative !important; inset:auto !important; width:100%; height:340px; order:2; }
|
||||
#hops-line { display:none !important; }
|
||||
#hops-grid { grid-template-columns:1fr !important; gap:28px !important; }
|
||||
#hops-grid > div { flex-direction:row !important; text-align:left !important; align-items:flex-start !important; }
|
||||
#hops-grid > div > div:first-child { flex:0 0 auto; margin-top:2px; }
|
||||
}
|
||||
@keyframes ticker { from { transform:translateX(0); } to { transform:translateX(-50%); } }
|
||||
</style>
|
||||
</helmet>
|
||||
|
||||
<div style="min-height:100vh;background:#080d1f;color:#e8f0ff;font-family:'Space Grotesk',sans-serif;overflow-x:hidden;position:relative;">
|
||||
|
||||
<!-- Ambient aurora background -->
|
||||
<div style="position:fixed;inset:0;pointer-events:none;z-index:0;background:radial-gradient(ellipse 55% 40% at 15% 0%, rgba(46,230,255,0.10), transparent 65%), radial-gradient(ellipse 50% 45% at 90% 20%, rgba(139,123,255,0.12), transparent 65%), radial-gradient(ellipse 60% 40% at 50% 60%, rgba(255,79,216,0.06), transparent 70%), radial-gradient(ellipse 55% 35% at 10% 95%, rgba(46,230,255,0.08), transparent 65%), radial-gradient(ellipse 45% 35% at 95% 90%, rgba(182,255,61,0.05), transparent 70%);"></div>
|
||||
<div style="position:fixed;inset:0;pointer-events:none;z-index:0;background:linear-gradient(180deg, rgba(8,13,31,0) 0%, rgba(8,13,31,0.35) 100%);"></div>
|
||||
|
||||
<div style="position:relative;z-index:1;">
|
||||
|
||||
<!-- NAV -->
|
||||
<header data-screen-label="Nav" style="position:fixed;top:0;left:0;right:0;z-index:50;backdrop-filter:blur(14px);background:rgba(13,20,48,0.65);border-bottom:1px solid rgba(255,255,255,0.07);">
|
||||
<div style="max-width:1200px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;padding:16px 32px;">
|
||||
<div id="nav-prompt" style="font-family:'JetBrains Mono',monospace;font-size:14px;color:#2ee6ff;white-space:nowrap;">muaz@infra:<span style="color:#ff4fd8;">~</span>$ <span style="color:#e8f0ff;">whoami</span><span style="display:inline-block;width:8px;height:15px;background:#2ee6ff;margin-left:6px;vertical-align:middle;animation:blink 1.1s step-end infinite;"></span></div>
|
||||
<nav id="nav-links" style="display:flex;gap:28px;font-size:14px;white-space:nowrap;">
|
||||
<a href="#about" style="color:#8ea2c0;text-decoration:none;" style-hover="color:#2ee6ff;">About</a>
|
||||
<a href="#skills" style="color:#8ea2c0;text-decoration:none;" style-hover="color:#8b7bff;">Skills</a>
|
||||
<a href="#projects" style="color:#8ea2c0;text-decoration:none;" style-hover="color:#ff4fd8;">Projects</a>
|
||||
<a href="#pipeline" style="color:#8ea2c0;text-decoration:none;" style-hover="color:#b6ff3d;">Pipeline</a>
|
||||
<a href="#contact" style="color:#8ea2c0;text-decoration:none;" style-hover="color:#ffb84d;">Contact</a>
|
||||
</nav>
|
||||
<button id="nav-toggle" onClick="{{ toggleMenu }}" aria-label="Menu" style="display:none;align-items:center;justify-content:center;width:44px;height:44px;border-radius:8px;border:1px solid rgba(46,230,255,0.35);background:rgba(46,230,255,0.06);color:#2ee6ff;font-size:20px;cursor:pointer;padding:0;">{{ menuIcon }}</button>
|
||||
</div>
|
||||
<sc-if value="{{ menuOpen }}" hint-placeholder-val="{{ false }}">
|
||||
<nav style="display:flex;flex-direction:column;padding:8px 20px 20px;gap:4px;border-top:1px solid rgba(255,255,255,0.07);background:rgba(13,20,48,0.9);backdrop-filter:blur(14px);">
|
||||
<a href="#about" onClick="{{ closeMenu }}" style="color:#e8f0ff;text-decoration:none;font-size:16px;padding:14px 12px;border-radius:8px;display:flex;align-items:center;gap:12px;" style-hover="background:rgba(46,230,255,0.08);"><span style="width:7px;height:7px;border-radius:50%;background:#2ee6ff;"></span>About</a>
|
||||
<a href="#skills" onClick="{{ closeMenu }}" style="color:#e8f0ff;text-decoration:none;font-size:16px;padding:14px 12px;border-radius:8px;display:flex;align-items:center;gap:12px;" style-hover="background:rgba(139,123,255,0.08);"><span style="width:7px;height:7px;border-radius:50%;background:#8b7bff;"></span>Skills</a>
|
||||
<a href="#projects" onClick="{{ closeMenu }}" style="color:#e8f0ff;text-decoration:none;font-size:16px;padding:14px 12px;border-radius:8px;display:flex;align-items:center;gap:12px;" style-hover="background:rgba(255,79,216,0.08);"><span style="width:7px;height:7px;border-radius:50%;background:#ff4fd8;"></span>Projects</a>
|
||||
<a href="#pipeline" onClick="{{ closeMenu }}" style="color:#e8f0ff;text-decoration:none;font-size:16px;padding:14px 12px;border-radius:8px;display:flex;align-items:center;gap:12px;" style-hover="background:rgba(182,255,61,0.08);"><span style="width:7px;height:7px;border-radius:50%;background:#b6ff3d;"></span>Pipeline</a>
|
||||
<a href="#contact" onClick="{{ closeMenu }}" style="color:#e8f0ff;text-decoration:none;font-size:16px;padding:14px 12px;border-radius:8px;display:flex;align-items:center;gap:12px;" style-hover="background:rgba(255,184,77,0.08);"><span style="width:7px;height:7px;border-radius:50%;background:#ffb84d;"></span>Contact</a>
|
||||
</nav>
|
||||
</sc-if>
|
||||
</header>
|
||||
|
||||
<!-- HERO with 3D network canvas -->
|
||||
<section id="hero-section" data-screen-label="Hero" style="position:relative;min-height:100vh;display:flex;align-items:center;overflow:hidden;">
|
||||
<div id="hero-canvas-wrap" style="position:absolute;inset:0;">
|
||||
<canvas ref="{{ canvasRef }}" style="position:absolute;inset:0;width:100%;height:100%;"></canvas>
|
||||
</div>
|
||||
<div style="position:absolute;inset:0;background:radial-gradient(ellipse 60% 50% at 70% 50%, rgba(46,230,255,0.06), transparent 70%), radial-gradient(ellipse 40% 40% at 20% 80%, rgba(255,79,216,0.05), transparent 70%);pointer-events:none;"></div>
|
||||
<div style="position:absolute;inset:0;background-image:linear-gradient(rgba(120,160,220,0.055) 1px,transparent 1px),linear-gradient(90deg,rgba(120,160,220,0.055) 1px,transparent 1px);background-size:56px 56px;-webkit-mask-image:radial-gradient(ellipse 85% 75% at 50% 45%,black,transparent 88%);mask-image:radial-gradient(ellipse 85% 75% at 50% 45%,black,transparent 88%);pointer-events:none;"></div>
|
||||
|
||||
<div id="hero-inner" style="position:relative;z-index:2;max-width:1200px;margin:0 auto;padding:120px 32px 80px;width:100%;">
|
||||
<div style="max-width:640px;">
|
||||
<p style="font-family:'JetBrains Mono',monospace;font-size:13px;letter-spacing:2px;color:#2ee6ff;margin:0 0 20px;">// CLOUD & NETWORK INFRASTRUCTURE</p>
|
||||
<h1 style="font-size:clamp(48px,7vw,84px);line-height:1.02;margin:0 0 20px;font-weight:700;background:linear-gradient(100deg,#2ee6ff 0%,#8b7bff 30%,#ff4fd8 55%,#ffb84d 75%,#2ee6ff 100%);background-size:220% 100%;animation:gradientShift 8s ease infinite;-webkit-background-clip:text;background-clip:text;color:transparent;filter:drop-shadow(0 0 28px rgba(139,123,255,0.35));">Muaz<br>Bhutta</h1>
|
||||
<p style="font-size:20px;color:#c3d0e8;margin:0 0 12px;font-weight:500;">Cloud & Network Infrastructure Engineer</p>
|
||||
<p style="font-family:'JetBrains Mono',monospace;font-size:14px;line-height:1.7;color:#8ea2c0;margin:0 0 36px;max-width:520px;">From routing tables to cloud pipelines — I build infrastructure that runs, and I understand why it runs.</p>
|
||||
<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:center;">
|
||||
<a href="#projects" style="text-decoration:none;display:inline-flex;align-items:center;gap:10px;padding:16px 32px;border-radius:99px;background:linear-gradient(120deg,#2ee6ff,#8b7bff 55%,#ff4fd8);background-size:180% 100%;color:#060a18;font-weight:700;font-size:15px;letter-spacing:0.3px;box-shadow:0 12px 40px -10px rgba(139,123,255,0.65), inset 0 1px 0 rgba(255,255,255,0.35);transition:all 0.3s ease;animation:gradientShift 6s ease infinite;" style-hover="transform:translateY(-2px);box-shadow:0 18px 50px -10px rgba(255,79,216,0.6), inset 0 1px 0 rgba(255,255,255,0.35);">View Projects <span style="font-size:17px;">→</span></a>
|
||||
<a href="#contact" style="text-decoration:none;display:inline-flex;align-items:center;gap:10px;padding:15px 30px;border-radius:99px;border:1px solid transparent;background:linear-gradient(rgba(13,20,48,0.85),rgba(13,20,48,0.85)) padding-box, linear-gradient(120deg,rgba(46,230,255,0.7),rgba(139,123,255,0.7)) border-box;color:#2ee6ff;font-weight:600;font-size:14px;font-family:'JetBrains Mono',monospace;backdrop-filter:blur(6px);transition:all 0.3s ease;" style-hover="transform:translateY(-2px);box-shadow:0 12px 36px -12px rgba(46,230,255,0.5);color:#7ff0ff;"><span style="width:8px;height:8px;border-radius:50%;background:#b6ff3d;animation:pulseDot 2s infinite;"></span>./contact.sh</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="hero-status" style="position:absolute;right:32px;bottom:64px;display:flex;flex-direction:column;gap:10px;font-family:'JetBrains Mono',monospace;font-size:12px;color:#8ea2c0;animation:floaty 6s ease-in-out infinite;">
|
||||
<div style="display:flex;align-items:center;gap:10px;padding:10px 16px;border:1px solid rgba(182,255,61,0.25);border-radius:6px;background:rgba(13,20,48,0.72);backdrop-filter:blur(6px);">
|
||||
<span style="width:8px;height:8px;border-radius:50%;background:#b6ff3d;animation:pulseDot 2s infinite;"></span>
|
||||
VPS 152.42.241.74 — all systems up
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;padding:10px 16px;border:1px solid rgba(46,230,255,0.25);border-radius:6px;background:rgba(13,20,48,0.72);backdrop-filter:blur(6px);">
|
||||
<span style="width:8px;height:8px;border-radius:50%;background:#2ee6ff;animation:pulseDot 2s 0.7s infinite;"></span>
|
||||
Jeddah / Riyadh, Saudi Arabia
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TICKER -->
|
||||
<div data-screen-label="Ticker" style="overflow:hidden;border-top:1px solid rgba(255,255,255,0.07);border-bottom:1px solid rgba(255,255,255,0.07);background:rgba(255,255,255,0.015);padding:18px 0;">
|
||||
<div style="display:flex;gap:44px;width:max-content;animation:ticker 30s linear infinite;font-family:'JetBrains Mono',monospace;font-size:13px;letter-spacing:3px;align-items:center;">
|
||||
<span style="color:#2ee6ff;">OSPF</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#8b7bff;">AWS</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#ff4fd8;">DOCKER</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#b6ff3d;">WIREGUARD</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#ffb84d;">PROMETHEUS</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#2ee6ff;">GRAFANA</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#8b7bff;">CI/CD</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#ff4fd8;">LINUX</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#b6ff3d;">CADDY</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#ffb84d;">VLAN</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#2ee6ff;">FLASK</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#8b7bff;">BASH</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#2ee6ff;">OSPF</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#8b7bff;">AWS</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#ff4fd8;">DOCKER</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#b6ff3d;">WIREGUARD</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#ffb84d;">PROMETHEUS</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#2ee6ff;">GRAFANA</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#8b7bff;">CI/CD</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#ff4fd8;">LINUX</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#b6ff3d;">CADDY</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#ffb84d;">VLAN</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#2ee6ff;">FLASK</span><span style="color:#3a4560;">◆</span>
|
||||
<span style="color:#8b7bff;">BASH</span><span style="color:#3a4560;">◆</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ABOUT / TRACEROUTE -->
|
||||
<section id="about" data-screen-label="About — Traceroute" style="position:relative;padding:120px 32px;border-top:1px solid rgba(255,255,255,0.06);">
|
||||
<div style="max-width:1200px;margin:0 auto;">
|
||||
<p style="font-family:'JetBrains Mono',monospace;font-size:13px;letter-spacing:2px;color:#8b7bff;margin:0 0 12px;">$ traceroute muaz --journey</p>
|
||||
<h2 style="font-size:40px;margin:0 0 56px;font-weight:700;">The route so far</h2>
|
||||
|
||||
<div id="hops-grid" style="display:grid;grid-template-columns:repeat(5,1fr);gap:0;position:relative;">
|
||||
<div id="hops-line" style="position:absolute;top:11px;left:10%;right:10%;height:2px;background:linear-gradient(90deg,#2ee6ff,#8b7bff,#ff4fd8,#ffb84d,#b6ff3d);opacity:0.4;"></div>
|
||||
|
||||
<div style="display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;position:relative;">
|
||||
<div style="width:24px;height:24px;border-radius:50%;background:#0b1226;border:2px solid #2ee6ff;color:#2ee6ff;animation:hopGlow 10s 0s infinite;"></div>
|
||||
<div><div style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#2ee6ff;margin-bottom:6px;">HOP 1</div><div style="font-weight:600;font-size:16px;">Multan, PK</div><div style="font-size:13px;color:#8ea2c0;margin-top:4px;">BS Computer Science</div></div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;position:relative;">
|
||||
<div style="width:24px;height:24px;border-radius:50%;background:#0b1226;border:2px solid #8b7bff;color:#8b7bff;animation:hopGlow 10s 2s infinite;"></div>
|
||||
<div><div style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#8b7bff;margin-bottom:6px;">HOP 2</div><div style="font-weight:600;font-size:16px;">CCNA</div><div style="font-size:13px;color:#8ea2c0;margin-top:4px;">Routing & Switching foundation</div></div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;position:relative;">
|
||||
<div style="width:24px;height:24px;border-radius:50%;background:#0b1226;border:2px solid #ff4fd8;color:#ff4fd8;animation:hopGlow 10s 4s infinite;"></div>
|
||||
<div><div style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#ff4fd8;margin-bottom:6px;">HOP 3</div><div style="font-weight:600;font-size:16px;">AWS + Linux</div><div style="font-size:13px;color:#8ea2c0;margin-top:4px;">Cloud & systems skills</div></div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;position:relative;">
|
||||
<div style="width:24px;height:24px;border-radius:50%;background:#0b1226;border:2px solid #ffb84d;color:#ffb84d;animation:hopGlow 10s 6s infinite;"></div>
|
||||
<div><div style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#ffb84d;margin-bottom:6px;">HOP 4</div><div style="font-weight:600;font-size:16px;">Self-hosted labs</div><div style="font-size:13px;color:#8ea2c0;margin-top:4px;">VPN, monitoring, APIs — live on VPS</div></div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:center;text-align:center;gap:14px;position:relative;">
|
||||
<div style="width:24px;height:24px;border-radius:50%;background:#0b1226;border:2px solid #b6ff3d;color:#b6ff3d;animation:hopGlow 10s 8s infinite;"></div>
|
||||
<div><div style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#b6ff3d;margin-bottom:6px;">HOP 5</div><div style="font-weight:600;font-size:16px;">Jeddah / Riyadh</div><div style="font-size:13px;color:#8ea2c0;margin-top:4px;">Junior Network / Cloud Engineer — target</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SKILLS -->
|
||||
<section id="skills" data-screen-label="Skills" style="padding:120px 32px;border-top:1px solid rgba(255,255,255,0.06);background:radial-gradient(ellipse 50% 60% at 50% 0%, rgba(139,123,255,0.06), transparent 70%);">
|
||||
<div style="max-width:1200px;margin:0 auto;">
|
||||
<p style="font-family:'JetBrains Mono',monospace;font-size:13px;letter-spacing:2px;color:#2ee6ff;margin:0 0 12px;">$ show ip interface brief</p>
|
||||
<h2 style="font-size:40px;margin:0 0 56px;font-weight:700;">Interfaces — all up / up</h2>
|
||||
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px;">
|
||||
|
||||
<div style="border:1px solid rgba(46,230,255,0.2);border-top:3px solid #2ee6ff;border-radius:10px;padding:28px;background:rgba(255,255,255,0.02);transition:all 0.35s ease;" style-hover="background:rgba(46,230,255,0.05);transform:translateY(-4px);box-shadow:0 20px 40px -20px rgba(46,230,255,0.35);">
|
||||
<div style="font-family:'JetBrains Mono',monospace;font-size:12px;color:#2ee6ff;margin-bottom:16px;">Gi0/0 — NETWORKING</div>
|
||||
<div style="display:flex;flex-direction:column;gap:12px;font-size:15px;color:#c3d0e8;">
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>CCNA (R&S)</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>OSPF / EIGRP</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>VLAN & Subnetting</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>OpenVPN</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>WireGuard</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid rgba(139,123,255,0.2);border-top:3px solid #8b7bff;border-radius:10px;padding:28px;background:rgba(255,255,255,0.02);transition:all 0.35s ease;" style-hover="background:rgba(139,123,255,0.05);transform:translateY(-4px);box-shadow:0 20px 40px -20px rgba(139,123,255,0.35);">
|
||||
<div style="font-family:'JetBrains Mono',monospace;font-size:12px;color:#8b7bff;margin-bottom:16px;">Gi0/1 — CLOUD</div>
|
||||
<div style="display:flex;flex-direction:column;gap:12px;font-size:15px;color:#c3d0e8;">
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>AWS (EC2, S3, IAM)</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>VPS Administration</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>Caddy / Nginx</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>DNS & Domains</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid rgba(255,79,216,0.2);border-top:3px solid #ff4fd8;border-radius:10px;padding:28px;background:rgba(255,255,255,0.02);transition:all 0.35s ease;" style-hover="background:rgba(255,79,216,0.05);transform:translateY(-4px);box-shadow:0 20px 40px -20px rgba(255,79,216,0.35);">
|
||||
<div style="font-family:'JetBrains Mono',monospace;font-size:12px;color:#ff4fd8;margin-bottom:16px;">Gi0/2 — DEVOPS</div>
|
||||
<div style="display:flex;flex-direction:column;gap:12px;font-size:15px;color:#c3d0e8;">
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>Docker & Compose</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>Prometheus + Grafana</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>CI/CD (GitHub Actions)</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>Bash scripting</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid rgba(255,184,77,0.2);border-top:3px solid #ffb84d;border-radius:10px;padding:28px;background:rgba(255,255,255,0.02);transition:all 0.35s ease;" style-hover="background:rgba(255,184,77,0.05);transform:translateY(-4px);box-shadow:0 20px 40px -20px rgba(255,184,77,0.35);">
|
||||
<div style="font-family:'JetBrains Mono',monospace;font-size:12px;color:#ffb84d;margin-bottom:16px;">Gi0/3 — DEVELOPMENT</div>
|
||||
<div style="display:flex;flex-direction:column;gap:12px;font-size:15px;color:#c3d0e8;">
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>Linux (Ubuntu/Debian)</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>Python + Flask</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>REST APIs</div>
|
||||
<div style="display:flex;align-items:center;gap:10px;"><span style="width:6px;height:6px;border-radius:50%;background:#b6ff3d;"></span>Git / GitHub</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PROJECTS -->
|
||||
<section id="projects" data-screen-label="Projects" style="padding:120px 32px;border-top:1px solid rgba(255,255,255,0.06);">
|
||||
<div style="max-width:1200px;margin:0 auto;">
|
||||
<p style="font-family:'JetBrains Mono',monospace;font-size:13px;letter-spacing:2px;color:#ff4fd8;margin:0 0 12px;">$ show ip route — selected work</p>
|
||||
<h2 style="font-size:40px;margin:0 0 16px;font-weight:700;">Routing table</h2>
|
||||
<p style="color:#8ea2c0;margin:0 0 56px;max-width:560px;">Infrastructure I've designed, deployed, and maintain on my own VPS.</p>
|
||||
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:20px;">
|
||||
|
||||
<div style="border:1px solid rgba(255,255,255,0.09);border-radius:10px;padding:28px;background:rgba(255,255,255,0.02);transition:all 0.35s ease;display:flex;flex-direction:column;gap:16px;" style-hover="border-color:rgba(46,230,255,0.5);box-shadow:0 24px 48px -24px rgba(46,230,255,0.4);transform:translateY(-4px);">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#b6ff3d;border:1px solid rgba(182,255,61,0.35);padding:4px 10px;border-radius:99px;">● LIVE</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#5a6a85;">vpn-dashboard</span>
|
||||
</div>
|
||||
<h3 style="margin:0;font-size:20px;font-weight:600;">OpenVPN Management Dashboard</h3>
|
||||
<p style="margin:0;font-size:14px;line-height:1.7;color:#8ea2c0;">Full VPN control panel on my VPS — live client monitoring, connection status, and Server-Sent Events for real-time data.</p>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:auto;">
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#2ee6ff;background:rgba(46,230,255,0.08);padding:4px 10px;border-radius:4px;">Flask</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#2ee6ff;background:rgba(46,230,255,0.08);padding:4px 10px;border-radius:4px;">OpenVPN</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#2ee6ff;background:rgba(46,230,255,0.08);padding:4px 10px;border-radius:4px;">Easy-RSA</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#2ee6ff;background:rgba(46,230,255,0.08);padding:4px 10px;border-radius:4px;">SSE</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#2ee6ff;background:rgba(46,230,255,0.08);padding:4px 10px;border-radius:4px;">Linux</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid rgba(255,255,255,0.09);border-radius:10px;padding:28px;background:rgba(255,255,255,0.02);transition:all 0.35s ease;display:flex;flex-direction:column;gap:16px;" style-hover="border-color:rgba(139,123,255,0.5);box-shadow:0 24px 48px -24px rgba(139,123,255,0.4);transform:translateY(-4px);">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#b6ff3d;border:1px solid rgba(182,255,61,0.35);padding:4px 10px;border-radius:99px;">● LIVE</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#5a6a85;">monitoring-stack</span>
|
||||
</div>
|
||||
<h3 style="margin:0;font-size:20px;font-weight:600;">Prometheus + Grafana Monitoring</h3>
|
||||
<p style="margin:0;font-size:14px;line-height:1.7;color:#8ea2c0;">Monitoring stack deployed via Docker Compose — Node Exporter collects system metrics, visualized on Grafana dashboards.</p>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:auto;">
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#8b7bff;background:rgba(139,123,255,0.1);padding:4px 10px;border-radius:4px;">Docker Compose</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#8b7bff;background:rgba(139,123,255,0.1);padding:4px 10px;border-radius:4px;">Prometheus</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#8b7bff;background:rgba(139,123,255,0.1);padding:4px 10px;border-radius:4px;">Grafana</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#8b7bff;background:rgba(139,123,255,0.1);padding:4px 10px;border-radius:4px;">Node Exporter</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid rgba(255,255,255,0.09);border-radius:10px;padding:28px;background:rgba(255,255,255,0.02);transition:all 0.35s ease;display:flex;flex-direction:column;gap:16px;" style-hover="border-color:rgba(255,79,216,0.5);box-shadow:0 24px 48px -24px rgba(255,79,216,0.4);transform:translateY(-4px);">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#b6ff3d;border:1px solid rgba(182,255,61,0.35);padding:4px 10px;border-radius:99px;">● LIVE</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#5a6a85;">system-api</span>
|
||||
</div>
|
||||
<h3 style="margin:0;font-size:20px;font-weight:600;">Real-time System Status API</h3>
|
||||
<p style="margin:0;font-size:14px;line-height:1.7;color:#8ea2c0;">Flask API streaming live CPU, RAM and disk metrics from psutil over SSE, with a dark-themed dashboard.</p>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:auto;">
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#ff4fd8;background:rgba(255,79,216,0.1);padding:4px 10px;border-radius:4px;">Python</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#ff4fd8;background:rgba(255,79,216,0.1);padding:4px 10px;border-radius:4px;">Flask</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#ff4fd8;background:rgba(255,79,216,0.1);padding:4px 10px;border-radius:4px;">psutil</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#ff4fd8;background:rgba(255,79,216,0.1);padding:4px 10px;border-radius:4px;">SSE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid rgba(255,255,255,0.09);border-radius:10px;padding:28px;background:rgba(255,255,255,0.02);transition:all 0.35s ease;display:flex;flex-direction:column;gap:16px;" style-hover="border-color:rgba(255,184,77,0.5);box-shadow:0 24px 48px -24px rgba(255,184,77,0.4);transform:translateY(-4px);">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#ffb84d;border:1px solid rgba(255,184,77,0.35);padding:4px 10px;border-radius:99px;">◐ LAB</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#5a6a85;">wireguard-lab</span>
|
||||
</div>
|
||||
<h3 style="margin:0;font-size:20px;font-weight:600;">WireGuard VPN Lab</h3>
|
||||
<p style="margin:0;font-size:14px;line-height:1.7;color:#8ea2c0;">WireGuard setup, WGDashboard integration, and troubleshooting mobile connectivity issues (carrier-level UDP blocking).</p>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:auto;">
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#ffb84d;background:rgba(255,184,77,0.1);padding:4px 10px;border-radius:4px;">WireGuard</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#ffb84d;background:rgba(255,184,77,0.1);padding:4px 10px;border-radius:4px;">WGDashboard</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#ffb84d;background:rgba(255,184,77,0.1);padding:4px 10px;border-radius:4px;">Networking</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid rgba(255,255,255,0.09);border-radius:10px;padding:28px;background:rgba(255,255,255,0.02);transition:all 0.35s ease;display:flex;flex-direction:column;gap:16px;" style-hover="border-color:rgba(182,255,61,0.5);box-shadow:0 24px 48px -24px rgba(182,255,61,0.4);transform:translateY(-4px);">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#b6ff3d;border:1px solid rgba(182,255,61,0.35);padding:4px 10px;border-radius:99px;">● LIVE</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#5a6a85;">this-site</span>
|
||||
</div>
|
||||
<h3 style="margin:0;font-size:20px;font-weight:600;">This Portfolio</h3>
|
||||
<p style="margin:0;font-size:14px;line-height:1.7;color:#8ea2c0;">Static site that auto-deploys to my own VPS via GitHub Actions — the CI/CD pipeline is itself part of the site.</p>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:auto;">
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#b6ff3d;background:rgba(182,255,61,0.1);padding:4px 10px;border-radius:4px;">HTML/CSS/JS</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#b6ff3d;background:rgba(182,255,61,0.1);padding:4px 10px;border-radius:4px;">GitHub Actions</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#b6ff3d;background:rgba(182,255,61,0.1);padding:4px 10px;border-radius:4px;">Caddy</span>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:#b6ff3d;background:rgba(182,255,61,0.1);padding:4px 10px;border-radius:4px;">VPS</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- LIVE TERMINAL / PIPELINE -->
|
||||
<section id="pipeline" data-screen-label="DevOps Terminal" style="padding:120px 32px;border-top:1px solid rgba(255,255,255,0.06);background:radial-gradient(ellipse 50% 60% at 50% 100%, rgba(182,255,61,0.05), transparent 70%);">
|
||||
<div style="max-width:900px;margin:0 auto;">
|
||||
<p style="font-family:'JetBrains Mono',monospace;font-size:13px;letter-spacing:2px;color:#b6ff3d;margin:0 0 12px;">$ tail -f /var/log/deploy.log</p>
|
||||
<h2 style="font-size:40px;margin:0 0 56px;font-weight:700;">Live from the VPS</h2>
|
||||
|
||||
<div style="border:1px solid rgba(255,255,255,0.12);border-radius:12px;overflow:hidden;background:#0a101f;box-shadow:0 40px 100px -30px rgba(46,230,255,0.35), 0 0 0 1px rgba(46,230,255,0.08);position:relative;">
|
||||
<div style="display:flex;align-items:center;gap:8px;padding:12px 16px;background:rgba(255,255,255,0.04);border-bottom:1px solid rgba(255,255,255,0.08);">
|
||||
<span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;"></span>
|
||||
<span style="width:12px;height:12px;border-radius:50%;background:#febc2e;"></span>
|
||||
<span style="width:12px;height:12px;border-radius:50%;background:#28c840;"></span>
|
||||
<span style="margin-left:12px;font-family:'JetBrains Mono',monospace;font-size:12px;color:#8ea2c0;">muaz@mj — ssh — 152.42.241.74</span>
|
||||
</div>
|
||||
<div style="position:absolute;left:0;right:0;top:44px;height:60px;background:linear-gradient(180deg,rgba(46,230,255,0.05),transparent);animation:scan 7s linear infinite;pointer-events:none;"></div>
|
||||
<div style="padding:24px 24px 32px;min-height:340px;font-family:'JetBrains Mono',monospace;font-size:14px;line-height:2;">
|
||||
<sc-for list="{{ termLines }}" as="ln" hint-placeholder-count="4">
|
||||
<div style="color:{{ ln.color }};white-space:pre-wrap;">{{ ln.text }}</div>
|
||||
</sc-for>
|
||||
<span style="display:inline-block;width:9px;height:17px;background:#b6ff3d;vertical-align:middle;animation:blink 1s step-end infinite;"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CREDENTIALS -->
|
||||
<section id="credentials" data-screen-label="Credentials" style="padding:120px 32px;border-top:1px solid rgba(255,255,255,0.06);">
|
||||
<div style="max-width:900px;margin:0 auto;">
|
||||
<p style="font-family:'JetBrains Mono',monospace;font-size:13px;letter-spacing:2px;color:#ffb84d;margin:0 0 12px;">$ cat /etc/credentials</p>
|
||||
<h2 style="font-size:40px;margin:0 0 48px;font-weight:700;">Education & Certifications</h2>
|
||||
<div style="display:flex;flex-direction:column;gap:14px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:16px;padding:22px 28px;border:1px solid rgba(255,255,255,0.09);border-radius:10px;background:rgba(255,255,255,0.02);transition:all 0.3s ease;" style-hover="border-color:rgba(46,230,255,0.4);">
|
||||
<div><div style="font-weight:600;font-size:17px;">BS Computer Science</div><div style="font-size:13px;color:#8ea2c0;margin-top:4px;">University</div></div>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:12px;color:#2ee6ff;">DEGREE</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:16px;padding:22px 28px;border:1px solid rgba(255,255,255,0.09);border-radius:10px;background:rgba(255,255,255,0.02);transition:all 0.3s ease;" style-hover="border-color:rgba(139,123,255,0.4);">
|
||||
<div><div style="font-weight:600;font-size:17px;">CCNA (200-301)</div><div style="font-size:13px;color:#8ea2c0;margin-top:4px;">Cisco — in progress / planned</div></div>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:12px;color:#8b7bff;">CERT</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:16px;padding:22px 28px;border:1px solid rgba(255,255,255,0.09);border-radius:10px;background:rgba(255,255,255,0.02);transition:all 0.3s ease;" style-hover="border-color:rgba(255,79,216,0.4);">
|
||||
<div><div style="font-weight:600;font-size:17px;">AWS Cloud Practitioner</div><div style="font-size:13px;color:#8ea2c0;margin-top:4px;">Amazon Web Services</div></div>
|
||||
<span style="font-family:'JetBrains Mono',monospace;font-size:12px;color:#ff4fd8;">CERT</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CONTACT -->
|
||||
<section id="contact" data-screen-label="Contact" style="padding:120px 32px 80px;border-top:1px solid rgba(255,255,255,0.06);">
|
||||
<div style="max-width:900px;margin:0 auto;text-align:center;padding:72px 40px;border-radius:16px;border:1px solid transparent;background:linear-gradient(#101736,#101736) padding-box, linear-gradient(120deg,rgba(46,230,255,0.55),rgba(139,123,255,0.45),rgba(255,79,216,0.55)) border-box;box-shadow:0 40px 100px -50px rgba(139,123,255,0.45);">
|
||||
<p style="font-family:'JetBrains Mono',monospace;font-size:13px;letter-spacing:2px;color:#2ee6ff;margin:0 0 16px;">// ESTABLISH CONNECTION</p>
|
||||
<h2 style="font-size:44px;margin:0 0 16px;font-weight:700;background:linear-gradient(100deg,#2ee6ff,#8b7bff,#ff4fd8);-webkit-background-clip:text;background-clip:text;color:transparent;">Let's connect</h2>
|
||||
<p style="color:#8ea2c0;margin:0 0 40px;">Open to Junior Network / Cloud Engineer roles in Jeddah & Riyadh.</p>
|
||||
<div style="display:flex;gap:14px;justify-content:center;flex-wrap:wrap;">
|
||||
<a href="mailto:you@muazbhutta.online" style="text-decoration:none;padding:14px 28px;border-radius:6px;background:linear-gradient(90deg,#2ee6ff,#8b7bff);color:#04060d;font-weight:600;font-size:15px;box-shadow:0 8px 32px -8px rgba(46,230,255,0.55);transition:all 0.25s ease;" style-hover="filter:brightness(1.15);">Email</a>
|
||||
<a href="https://github.com/yourusername" style="text-decoration:none;padding:14px 28px;border-radius:6px;border:1px solid rgba(255,255,255,0.2);color:#e8f0ff;font-weight:600;font-size:15px;" style-hover="border-color:#ff4fd8;color:#ff4fd8;">GitHub</a>
|
||||
<a href="https://linkedin.com/in/yourusername" style="text-decoration:none;padding:14px 28px;border-radius:6px;border:1px solid rgba(255,255,255,0.2);color:#e8f0ff;font-weight:600;font-size:15px;" style-hover="border-color:#2ee6ff;color:#2ee6ff;">LinkedIn</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer style="padding:32px;text-align:center;font-family:'JetBrains Mono',monospace;font-size:12px;color:#5a6a85;border-top:1px solid rgba(255,255,255,0.06);">
|
||||
© 2026 Muaz Bhutta
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</x-dc>
|
||||
<script type="text/x-dc" data-dc-script data-props="{
|
||||
"colorScheme": { "editor": "enum", "options": ["spectrum", "cyberpunk", "aurora"], "default": "spectrum", "tsType": "string", "section": "3D Network" },
|
||||
"nodeDensity": { "editor": "range", "min": 60, "max": 320, "step": 10, "default": 170, "tsType": "number", "section": "3D Network" },
|
||||
"rotationSpeed": { "editor": "range", "min": 0.2, "max": 3, "step": 0.1, "default": 1, "tsType": "number", "section": "3D Network" }
|
||||
}">
|
||||
class Component extends DCLogic {
|
||||
state = { done: [], cur: "", menuOpen: false };
|
||||
|
||||
palette() {
|
||||
const scheme = this.props.colorScheme ?? "spectrum";
|
||||
if (scheme === "cyberpunk") return ["#00f0ff", "#ff2ee6", "#ffe600", "#7d5cff"];
|
||||
if (scheme === "aurora") return ["#3dffb0", "#2ee6ff", "#8b7bff", "#b6ff3d"];
|
||||
return ["#2ee6ff", "#8b7bff", "#ff4fd8", "#b6ff3d", "#ffb84d"];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.startTyping();
|
||||
this.startCanvas();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
cancelAnimationFrame(this._raf);
|
||||
clearTimeout(this._typeT);
|
||||
if (this._io) this._io.disconnect();
|
||||
}
|
||||
|
||||
// ---------- terminal typing ----------
|
||||
script() {
|
||||
return [
|
||||
{ text: "$ ssh muaz@152.42.241.74", color: "#e8f0ff" },
|
||||
{ text: "Welcome to Ubuntu 24.04 LTS — uptime: 214 days", color: "#8ea2c0" },
|
||||
{ text: "$ docker compose ps", color: "#e8f0ff" },
|
||||
{ text: " vpn-dashboard running :8443", color: "#b6ff3d" },
|
||||
{ text: " prometheus running :9090", color: "#b6ff3d" },
|
||||
{ text: " grafana running :3000", color: "#b6ff3d" },
|
||||
{ text: "$ git push origin main", color: "#e8f0ff" },
|
||||
{ text: "→ GitHub Actions: deploy.yml triggered", color: "#2ee6ff" },
|
||||
{ text: "→ rsync -avz --delete ./ /var/www/portfolio/", color: "#2ee6ff" },
|
||||
{ text: "✔ muazbhutta.online deployed in 38s", color: "#ffb84d" },
|
||||
];
|
||||
}
|
||||
|
||||
startTyping() {
|
||||
const lines = this.script();
|
||||
let li = 0, ci = 0;
|
||||
const step = () => {
|
||||
if (li >= lines.length) {
|
||||
this._typeT = setTimeout(() => {
|
||||
li = 0; ci = 0;
|
||||
this.setState({ done: [], cur: "" });
|
||||
this._typeT = setTimeout(step, 400);
|
||||
}, 4000);
|
||||
return;
|
||||
}
|
||||
const line = lines[li];
|
||||
ci += 3; // type in chunks — fewer re-renders, smoother page
|
||||
if (ci >= line.text.length) {
|
||||
this.setState(s => ({ done: [...s.done, line], cur: "" }));
|
||||
li++; ci = 0;
|
||||
this._typeT = setTimeout(step, line.text.startsWith("$") ? 500 : 180);
|
||||
} else {
|
||||
this.setState({ cur: line.text.slice(0, ci) });
|
||||
this._typeT = setTimeout(step, 55 + Math.random() * 30);
|
||||
}
|
||||
};
|
||||
this._typeT = setTimeout(step, 600);
|
||||
}
|
||||
|
||||
// ---------- 3D network canvas ----------
|
||||
startCanvas() {
|
||||
const cv = this._cv;
|
||||
if (!cv) return;
|
||||
const ctx = cv.getContext("2d");
|
||||
const N = Math.round(this.props.nodeDensity ?? 120);
|
||||
const speed = this.props.rotationSpeed ?? 1;
|
||||
const pal = this.palette();
|
||||
|
||||
const nodes = [];
|
||||
for (let i = 0; i < N; i++) {
|
||||
const t = Math.acos(2 * Math.random() - 1), p = Math.random() * Math.PI * 2;
|
||||
const r = 0.75 + Math.random() * 0.25;
|
||||
nodes.push({
|
||||
x: Math.sin(t) * Math.cos(p) * r,
|
||||
y: Math.cos(t) * r,
|
||||
z: Math.sin(t) * Math.sin(p) * r,
|
||||
c: pal[i % pal.length],
|
||||
rad: 1.2 + Math.random() * 1.8,
|
||||
});
|
||||
}
|
||||
const edges = [];
|
||||
for (let i = 0; i < N; i++) {
|
||||
for (let j = i + 1; j < N; j++) {
|
||||
const dx = nodes[i].x - nodes[j].x, dy = nodes[i].y - nodes[j].y, dz = nodes[i].z - nodes[j].z;
|
||||
if (dx * dx + dy * dy + dz * dz < 0.14) edges.push([i, j]);
|
||||
}
|
||||
}
|
||||
const packets = Array.from({ length: Math.min(30, edges.length) }, () => ({
|
||||
e: Math.floor(Math.random() * edges.length),
|
||||
t: Math.random(),
|
||||
s: 0.004 + Math.random() * 0.012,
|
||||
}));
|
||||
|
||||
// Pre-rendered glow sprites (shadowBlur per-frame is very slow)
|
||||
const sprites = {};
|
||||
const makeSprite = (color) => {
|
||||
const s = document.createElement("canvas");
|
||||
s.width = 32; s.height = 32;
|
||||
const sc = s.getContext("2d");
|
||||
const g = sc.createRadialGradient(16, 16, 0, 16, 16, 16);
|
||||
g.addColorStop(0, color);
|
||||
g.addColorStop(0.35, color);
|
||||
g.addColorStop(1, "rgba(0,0,0,0)");
|
||||
sc.fillStyle = g;
|
||||
sc.fillRect(0, 0, 32, 32);
|
||||
return s;
|
||||
};
|
||||
for (const c of pal) sprites[c] = makeSprite(c);
|
||||
|
||||
// Only animate while the hero is on screen
|
||||
this._heroVisible = true;
|
||||
this._io = new IntersectionObserver((entries) => {
|
||||
this._heroVisible = entries[0].isIntersecting;
|
||||
});
|
||||
this._io.observe(cv);
|
||||
|
||||
let ang = 0;
|
||||
let mx = 0, my = 0;
|
||||
cv.addEventListener("mousemove", (ev) => {
|
||||
const b = cv.getBoundingClientRect();
|
||||
mx = (ev.clientX - b.left) / b.width - 0.5;
|
||||
my = (ev.clientY - b.top) / b.height - 0.5;
|
||||
});
|
||||
|
||||
const loop = () => {
|
||||
this._raf = requestAnimationFrame(loop);
|
||||
if (!this._heroVisible) return;
|
||||
const w = cv.clientWidth, h = cv.clientHeight;
|
||||
if (!w || !h) return;
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
if (cv.width !== Math.round(w * dpr)) { cv.width = Math.round(w * dpr); cv.height = Math.round(h * dpr); }
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
ang += 0.0022 * speed;
|
||||
const tiltX = 0.25 + my * 0.3;
|
||||
const mobile = window.innerWidth < 760;
|
||||
const cx = w * (mobile ? 0.5 : 0.68);
|
||||
const cy = h * (mobile ? 0.5 : 0.52);
|
||||
const R = Math.min(w, h) * (mobile ? 0.46 : 0.42);
|
||||
const dim = 1; // canvas has its own container on mobile now
|
||||
|
||||
const pts = nodes.map(n => {
|
||||
let x = n.x * Math.cos(ang + mx * 0.6) - n.z * Math.sin(ang + mx * 0.6);
|
||||
let z = n.x * Math.sin(ang + mx * 0.6) + n.z * Math.cos(ang + mx * 0.6);
|
||||
let y = n.y * Math.cos(tiltX) - z * Math.sin(tiltX);
|
||||
z = n.y * Math.sin(tiltX) + z * Math.cos(tiltX);
|
||||
const s = 1 / (1.9 - z * 0.75);
|
||||
return { X: cx + x * R * s, Y: cy + y * R * s, s, z, c: n.c, rad: n.rad };
|
||||
});
|
||||
|
||||
// edges
|
||||
ctx.lineWidth = 0.7;
|
||||
for (const [i, j] of edges) {
|
||||
const a = pts[i], b = pts[j];
|
||||
const depth = (a.z + b.z) / 2;
|
||||
const alpha = (0.05 + Math.max(0, depth + 0.6) * 0.16) * dim;
|
||||
ctx.strokeStyle = "rgba(120,160,220," + alpha.toFixed(3) + ")";
|
||||
ctx.beginPath(); ctx.moveTo(a.X, a.Y); ctx.lineTo(b.X, b.Y); ctx.stroke();
|
||||
}
|
||||
|
||||
// packets (glow via pre-rendered sprite)
|
||||
for (const p of packets) {
|
||||
p.t += p.s * speed;
|
||||
if (p.t > 1) { p.t = 0; p.e = Math.floor(Math.random() * edges.length); }
|
||||
const [i, j] = edges[p.e];
|
||||
const a = pts[i], b = pts[j];
|
||||
const X = a.X + (b.X - a.X) * p.t, Y = a.Y + (b.Y - a.Y) * p.t;
|
||||
ctx.globalAlpha = dim;
|
||||
ctx.drawImage(sprites[pts[i].c] || sprites[pal[0]], X - 6, Y - 6, 12, 12);
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// nodes (glow via pre-rendered sprite)
|
||||
for (const p of pts) {
|
||||
const alpha = (0.35 + Math.max(0, p.z + 0.7) * 0.5) * dim;
|
||||
ctx.globalAlpha = Math.min(1, alpha);
|
||||
const d = p.rad * p.s * 6;
|
||||
ctx.drawImage(sprites[p.c] || sprites[pal[0]], p.X - d / 2, p.Y - d / 2, d, d);
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
};
|
||||
this._raf = requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
renderVals() {
|
||||
const termLines = this.state.cur
|
||||
? [...this.state.done, { text: this.state.cur, color: "#e8f0ff" }]
|
||||
: this.state.done;
|
||||
return {
|
||||
termLines,
|
||||
menuOpen: this.state.menuOpen,
|
||||
menuIcon: this.state.menuOpen ? "✕" : "☰",
|
||||
toggleMenu: () => this.setState(s => ({ menuOpen: !s.menuOpen })),
|
||||
closeMenu: () => this.setState({ menuOpen: false }),
|
||||
canvasRef: (el) => { if (el && el !== this._cv) { this._cv = el; if (this._mounted) this.startCanvas(); } },
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
229
script.js
Normal file
229
script.js
Normal file
@@ -0,0 +1,229 @@
|
||||
// ============================================================
|
||||
// RENDER LOGIC — you shouldn't need to touch this file.
|
||||
// To add or change content, edit data.js instead.
|
||||
// ============================================================
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initThemeToggle();
|
||||
renderHero();
|
||||
renderAbout();
|
||||
renderCareerPath();
|
||||
renderSkills();
|
||||
renderProjects();
|
||||
renderCredentials();
|
||||
renderContact();
|
||||
renderFooter();
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// THEME — three modes: light, dark, auto (follows system setting)
|
||||
// ----------------------------------------------------------------
|
||||
function initThemeToggle() {
|
||||
const root = document.documentElement;
|
||||
const toggle = document.getElementById("theme-toggle");
|
||||
const label = document.getElementById("theme-mode-label");
|
||||
const media = window.matchMedia("(prefers-color-scheme: light)");
|
||||
const modes = ["light", "dark", "auto"];
|
||||
|
||||
function resolve(mode) {
|
||||
if (mode === "auto") return media.matches ? "light" : "dark";
|
||||
return mode;
|
||||
}
|
||||
|
||||
function apply(mode) {
|
||||
root.setAttribute("data-theme-mode", mode);
|
||||
root.setAttribute("data-theme", resolve(mode));
|
||||
if (label) label.textContent = mode.charAt(0).toUpperCase() + mode.slice(1);
|
||||
localStorage.setItem("themeMode", mode);
|
||||
}
|
||||
|
||||
let currentMode = localStorage.getItem("themeMode") || "auto";
|
||||
apply(currentMode);
|
||||
|
||||
toggle.addEventListener("click", () => {
|
||||
const next = modes[(modes.indexOf(currentMode) + 1) % modes.length];
|
||||
currentMode = next;
|
||||
apply(currentMode);
|
||||
});
|
||||
|
||||
media.addEventListener("change", () => {
|
||||
if (currentMode === "auto") apply("auto");
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// HERO
|
||||
// ----------------------------------------------------------------
|
||||
function renderHero() {
|
||||
document.getElementById("hero-title").innerHTML =
|
||||
`Hi, I'm ${SITE.name}.<br><span class="accent">${SITE.role}</span>`;
|
||||
document.getElementById("hero-tagline").textContent = SITE.tagline;
|
||||
|
||||
const cta = document.getElementById("hero-cta");
|
||||
cta.appendChild(makeLink("View Projects", "#projects", "btn btn-primary"));
|
||||
cta.appendChild(makeLink("Get in Touch", "#contact", "btn btn-ghost"));
|
||||
if (SITE.resumeLink && SITE.resumeLink !== "#") {
|
||||
cta.appendChild(makeLink("Download Resume", SITE.resumeLink, "btn btn-ghost", true));
|
||||
}
|
||||
|
||||
document.getElementById("id-avatar").textContent = SITE.initials;
|
||||
document.getElementById("id-name").textContent = SITE.name;
|
||||
document.getElementById("id-role").textContent = SITE.role;
|
||||
document.getElementById("id-location").textContent = SITE.location;
|
||||
document.getElementById("id-status").textContent = SITE.status;
|
||||
}
|
||||
|
||||
function makeLink(text, href, className, external) {
|
||||
const a = document.createElement("a");
|
||||
a.href = href;
|
||||
a.className = className;
|
||||
a.textContent = text;
|
||||
if (external) {
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener";
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// ABOUT
|
||||
// ----------------------------------------------------------------
|
||||
function renderAbout() {
|
||||
const container = document.getElementById("about-body");
|
||||
const mid = Math.ceil(ABOUT.length / 2);
|
||||
const columns = [ABOUT.slice(0, mid), ABOUT.slice(mid)];
|
||||
|
||||
columns.forEach((paragraphs) => {
|
||||
const col = document.createElement("div");
|
||||
col.className = "about-col";
|
||||
paragraphs.forEach((text) => {
|
||||
const p = document.createElement("p");
|
||||
p.textContent = text;
|
||||
col.appendChild(p);
|
||||
});
|
||||
container.appendChild(col);
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// CAREER PATH (timeline)
|
||||
// ----------------------------------------------------------------
|
||||
function renderCareerPath() {
|
||||
const track = document.getElementById("path-track");
|
||||
CAREER_PATH.forEach((step) => {
|
||||
const div = document.createElement("div");
|
||||
div.className = "path-step";
|
||||
div.innerHTML = `
|
||||
<div class="step-label">${step.label}</div>
|
||||
<div class="step-detail">${step.detail}</div>
|
||||
`;
|
||||
track.appendChild(div);
|
||||
});
|
||||
|
||||
if ("IntersectionObserver" in window) {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("in-view");
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
);
|
||||
document.querySelectorAll(".path-step").forEach((el) => observer.observe(el));
|
||||
} else {
|
||||
document.querySelectorAll(".path-step").forEach((el) => el.classList.add("in-view"));
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// SKILLS
|
||||
// ----------------------------------------------------------------
|
||||
function renderSkills() {
|
||||
const grid = document.getElementById("skills-grid");
|
||||
SKILLS.forEach((group) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "skill-card";
|
||||
card.innerHTML = `
|
||||
<div class="group-title">${group.group}</div>
|
||||
${group.items.map((item) => `<div class="skill-item">${item}</div>`).join("")}
|
||||
`;
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// PROJECTS
|
||||
// ----------------------------------------------------------------
|
||||
function renderProjects() {
|
||||
const list = document.getElementById("proj-list");
|
||||
PROJECTS.forEach((p) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "proj-card";
|
||||
card.innerHTML = `
|
||||
<div class="proj-top">
|
||||
<h3 class="heading-md">${p.title}</h3>
|
||||
<span class="status-tag" data-status="${p.status}">${p.status}</span>
|
||||
</div>
|
||||
<p>${p.summary}</p>
|
||||
<div class="stack-row">
|
||||
${p.stack.map((s) => `<span class="stack-tag">${s}</span>`).join("")}
|
||||
</div>
|
||||
${p.link && p.link !== "#" ? `<a class="proj-link" href="${p.link}" target="_blank" rel="noopener">View project →</a>` : ""}
|
||||
`;
|
||||
list.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// CREDENTIALS
|
||||
// ----------------------------------------------------------------
|
||||
function renderCredentials() {
|
||||
const list = document.getElementById("cred-list");
|
||||
CREDENTIALS.forEach((c) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "cred-row";
|
||||
row.innerHTML = `
|
||||
<div>
|
||||
<div class="title">${c.title}</div>
|
||||
<div class="org">${c.org}</div>
|
||||
</div>
|
||||
<div class="year">${c.year}</div>
|
||||
`;
|
||||
list.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// CONTACT
|
||||
// ----------------------------------------------------------------
|
||||
function renderContact() {
|
||||
document.getElementById("contact-text").textContent =
|
||||
"Open to job opportunities, collaborations, or just talking networking and cloud — reach out any time.";
|
||||
|
||||
const links = document.getElementById("contact-links");
|
||||
const items = [
|
||||
{ label: "Email", href: `mailto:${SITE.email}` },
|
||||
{ label: "GitHub", href: SITE.github },
|
||||
{ label: "LinkedIn", href: SITE.linkedin },
|
||||
];
|
||||
items.forEach((item, i) => {
|
||||
const a = document.createElement("a");
|
||||
a.className = i === 0 ? "btn btn-primary" : "btn btn-ghost";
|
||||
a.href = item.href;
|
||||
a.target = item.href.startsWith("mailto") ? "_self" : "_blank";
|
||||
a.rel = "noopener";
|
||||
a.textContent = item.label;
|
||||
links.appendChild(a);
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// FOOTER
|
||||
// ----------------------------------------------------------------
|
||||
function renderFooter() {
|
||||
document.getElementById("footer-text").textContent =
|
||||
`© ${new Date().getFullYear()} ${SITE.name} — built & deployed from scratch.`;
|
||||
}
|
||||
462
style.css
Normal file
462
style.css
Normal file
@@ -0,0 +1,462 @@
|
||||
/* ============================================================
|
||||
THEME TOKENS
|
||||
[data-theme="dark"] is the default. [data-theme="light"]
|
||||
overrides the same variable names. The actual color scheme
|
||||
is always resolved to "light" or "dark" in data-theme; the
|
||||
user's chosen mode (light / dark / auto) lives separately in
|
||||
data-theme-mode, used only to drive the toggle icon.
|
||||
============================================================ */
|
||||
:root,
|
||||
[data-theme="dark"] {
|
||||
--bg: #0b0f17;
|
||||
--panel: #121826;
|
||||
--panel-2: #182236;
|
||||
--border: #232e42;
|
||||
--text: #e9eef5;
|
||||
--muted: #8b96a8;
|
||||
--accent: #22b8a3;
|
||||
--accent-contrast: #04211d;
|
||||
--accent-soft: rgba(34, 184, 163, 0.12);
|
||||
--warn: #d1a350;
|
||||
--warn-soft: rgba(209, 163, 80, 0.12);
|
||||
--focus: #5b9dff;
|
||||
--shadow: 0 20px 40px -20px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--bg: #f7f9fc;
|
||||
--panel: #ffffff;
|
||||
--panel-2: #f1f4f9;
|
||||
--border: #e2e7f0;
|
||||
--text: #101828;
|
||||
--muted: #5b6472;
|
||||
--accent: #0f9488;
|
||||
--accent-contrast: #ffffff;
|
||||
--accent-soft: rgba(15, 148, 136, 0.08);
|
||||
--warn: #9a6b1e;
|
||||
--warn-soft: rgba(154, 107, 30, 0.1);
|
||||
--focus: #2563eb;
|
||||
--shadow: 0 20px 40px -20px rgba(16, 24, 40, 0.15);
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-display: "Space Grotesk", sans-serif;
|
||||
--font-mono: "JetBrains Mono", monospace;
|
||||
--font-body: "Inter", sans-serif;
|
||||
--radius: 10px;
|
||||
--radius-sm: 6px;
|
||||
--maxw: 1080px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html { scroll-behavior: smooth; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font-body);
|
||||
line-height: 1.65;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
a { color: inherit; }
|
||||
img { max-width: 100%; display: block; }
|
||||
::selection { background: var(--accent); color: var(--accent-contrast); }
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--focus);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
max-width: var(--maxw);
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin: 0 0 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.heading-lg { font-family: var(--font-display); font-weight: 600; letter-spacing: -0.01em; margin: 0; }
|
||||
.heading-md { font-family: var(--font-display); font-weight: 600; letter-spacing: -0.005em; margin: 0; }
|
||||
|
||||
/* ============================================================
|
||||
NAV
|
||||
============================================================ */
|
||||
.site-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
backdrop-filter: blur(10px);
|
||||
background: color-mix(in srgb, var(--bg) 82%, transparent);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.nav-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
gap: 16px;
|
||||
}
|
||||
.nav-logo {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
.nav-logo span { color: var(--accent); }
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 26px;
|
||||
list-style: none;
|
||||
margin: 0; padding: 0;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.nav-links a {
|
||||
text-decoration: none;
|
||||
color: var(--muted);
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.nav-links a:hover { color: var(--text); }
|
||||
|
||||
.nav-right { display: flex; align-items: center; gap: 10px; }
|
||||
|
||||
.theme-toggle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, transform 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.theme-toggle:hover { border-color: var(--accent); transform: translateY(-1px); }
|
||||
.theme-toggle svg { width: 17px; height: 17px; }
|
||||
|
||||
/* Icon visibility follows the chosen mode (light / dark / auto),
|
||||
not the resolved color scheme, so "auto" always shows its own icon. */
|
||||
.theme-toggle .icon-sun,
|
||||
.theme-toggle .icon-moon,
|
||||
.theme-toggle .icon-auto { display: none; }
|
||||
[data-theme-mode="light"] .theme-toggle .icon-sun { display: block; }
|
||||
[data-theme-mode="dark"] .theme-toggle .icon-moon { display: block; }
|
||||
[data-theme-mode="auto"] .theme-toggle .icon-auto { display: block; }
|
||||
|
||||
.theme-mode-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
min-width: 34px;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
HERO
|
||||
============================================================ */
|
||||
.hero {
|
||||
padding: 84px 0 72px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.hero-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr;
|
||||
gap: 48px;
|
||||
align-items: start;
|
||||
}
|
||||
.hero h1 {
|
||||
font-size: clamp(32px, 5vw, 48px);
|
||||
line-height: 1.12;
|
||||
}
|
||||
.hero h1 .accent { color: var(--accent); }
|
||||
.hero .tagline {
|
||||
margin-top: 18px;
|
||||
color: var(--muted);
|
||||
font-size: 17px;
|
||||
max-width: 520px;
|
||||
}
|
||||
.cta-row {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn {
|
||||
font-family: var(--font-body);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
padding: 11px 22px;
|
||||
border-radius: var(--radius-sm);
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s, border-color 0.15s, background 0.15s, opacity 0.15s;
|
||||
}
|
||||
.btn-primary { background: var(--accent); color: var(--accent-contrast); }
|
||||
.btn-primary:hover { transform: translateY(-1px); opacity: 0.92; }
|
||||
.btn-ghost { border-color: var(--border); color: var(--text); background: transparent; }
|
||||
.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
/* identity card */
|
||||
.id-card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 26px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.id-avatar {
|
||||
width: 52px; height: 52px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
border: 1px solid var(--border);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
font-size: 17px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.id-card .id-name { font-family: var(--font-display); font-weight: 600; font-size: 17px; }
|
||||
.id-card .id-role { color: var(--muted); font-size: 13.5px; margin-top: 3px; }
|
||||
.id-divider { height: 1px; background: var(--border); margin: 18px 0; }
|
||||
.id-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
padding: 7px 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
.id-row span:last-child { color: var(--text); text-align: right; }
|
||||
.id-status {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12.5px;
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 9px 12px;
|
||||
}
|
||||
.id-status .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
|
||||
|
||||
/* ============================================================
|
||||
SECTION SHELL
|
||||
============================================================ */
|
||||
.section { padding: 68px 0; border-bottom: 1px solid var(--border); }
|
||||
.section:last-of-type { border-bottom: none; }
|
||||
.section-head { margin-bottom: 34px; }
|
||||
.section-head .heading-md { font-size: 26px; }
|
||||
.section-head .desc { color: var(--muted); margin-top: 8px; font-size: 15px; max-width: 480px; }
|
||||
|
||||
/* ============================================================
|
||||
ABOUT
|
||||
============================================================ */
|
||||
.about-body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 40px;
|
||||
}
|
||||
.about-col p { color: var(--muted); font-size: 15.5px; margin: 0 0 16px; }
|
||||
.about-col p:last-child { margin-bottom: 0; }
|
||||
|
||||
/* ============================================================
|
||||
PATH (career timeline)
|
||||
============================================================ */
|
||||
.path-track {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 0;
|
||||
position: relative;
|
||||
}
|
||||
.path-track::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
left: 4%;
|
||||
right: 4%;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
.path-step {
|
||||
position: relative;
|
||||
padding-top: 30px;
|
||||
padding-right: 16px;
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
}
|
||||
.path-step.in-view { opacity: 1; transform: translateY(0); }
|
||||
.path-step::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 9px; height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg);
|
||||
border: 2px solid var(--accent);
|
||||
}
|
||||
.path-step .step-label { font-weight: 600; font-size: 14.5px; }
|
||||
.path-step .step-detail { color: var(--muted); font-size: 13px; margin-top: 4px; line-height: 1.5; }
|
||||
|
||||
/* ============================================================
|
||||
SKILLS
|
||||
============================================================ */
|
||||
.skills-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.skill-card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
}
|
||||
.skill-card .group-title {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.skill-item {
|
||||
font-size: 14px;
|
||||
padding: 6px 0;
|
||||
color: var(--text);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.skill-item:first-of-type { border-top: none; }
|
||||
|
||||
/* ============================================================
|
||||
PROJECTS
|
||||
============================================================ */
|
||||
.proj-list { display: flex; flex-direction: column; gap: 14px; }
|
||||
.proj-card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 24px;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.proj-card:hover { border-color: var(--accent); }
|
||||
.proj-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.proj-top .heading-md { font-size: 17px; }
|
||||
.status-tag {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status-tag[data-status="production"] { color: var(--accent); background: var(--accent-soft); }
|
||||
.status-tag[data-status="lab"] { color: var(--warn); background: var(--warn-soft); }
|
||||
.proj-card p { color: var(--muted); margin: 10px 0 14px; font-size: 14.5px; }
|
||||
.stack-row { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.stack-tag {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
background: var(--panel-2);
|
||||
padding: 4px 9px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.proj-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
.proj-link:hover { text-decoration: underline; }
|
||||
|
||||
/* ============================================================
|
||||
CREDENTIALS
|
||||
============================================================ */
|
||||
.cred-list { border-top: 1px solid var(--border); }
|
||||
.cred-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.cred-row .title { font-weight: 600; font-size: 15px; }
|
||||
.cred-row .org { color: var(--muted); font-size: 13.5px; margin-top: 2px; }
|
||||
.cred-row .year { font-family: var(--font-mono); font-size: 12px; color: var(--muted); }
|
||||
|
||||
/* ============================================================
|
||||
CONTACT
|
||||
============================================================ */
|
||||
.contact-panel {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 44px;
|
||||
text-align: center;
|
||||
}
|
||||
.contact-panel .heading-md { font-size: 25px; margin-bottom: 12px; }
|
||||
.contact-panel p { color: var(--muted); max-width: 440px; margin: 0 auto 26px; font-size: 15px; }
|
||||
.contact-links { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
|
||||
|
||||
.site-footer {
|
||||
padding: 30px 0 44px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
RESPONSIVE
|
||||
============================================================ */
|
||||
@media (max-width: 860px) {
|
||||
.hero-grid { grid-template-columns: 1fr; }
|
||||
.about-body { grid-template-columns: 1fr; }
|
||||
.path-track { grid-template-columns: repeat(2, 1fr); row-gap: 28px; }
|
||||
.path-track::before { display: none; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.nav-links { display: none; }
|
||||
.theme-mode-label { display: none; }
|
||||
.hero { padding: 60px 0 48px; }
|
||||
.section { padding: 48px 0; }
|
||||
.contact-panel { padding: 30px 22px; }
|
||||
}
|
||||
1658
support.js
Normal file
1658
support.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user