From de0acce9aa13c2f613703af160881ea74bbf1b3e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 30 Mar 2026 05:34:20 +0000
Subject: [PATCH 1/2] Initial plan
From 1feec317648e2eed739e31fc47e075c2c24b738f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 30 Mar 2026 05:40:43 +0000
Subject: [PATCH 2/2] Add SP Digital LLC portfolio site with all projects and
admin panel
Agent-Logs-Url: https://github.com/driver727-pixel/SP-Digital-LLC/sessions/5b377d2a-d61c-4ffd-b39c-42f7723e8175
Co-authored-by: driver727-pixel <269849721+driver727-pixel@users.noreply.github.com>
---
README.md | 42 ++++++-
app.js | 236 +++++++++++++++++++++++++++++++++++
index.html | 149 ++++++++++++++++++++++
projects.js | 86 +++++++++++++
styles.css | 352 ++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 863 insertions(+), 2 deletions(-)
create mode 100644 app.js
create mode 100644 index.html
create mode 100644 projects.js
create mode 100644 styles.css
diff --git a/README.md b/README.md
index 33de92b..0b19958 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,40 @@
-# SP-Digital-LLC
-This repository holds links and details for SP Digital LLC apps, sites, software, I.P. etc...
+# SP Digital LLC โ Project Hub
+
+A centralised portfolio for all SP Digital LLC projects: ideas, works in progress, and shipped apps.
+
+## ๐ Live Site
+
+Hosted on **GitHub Pages** โ `https://driver727-pixel.github.io/SP-Digital-LLC/`
+
+## ๐ Structure
+
+| File | Purpose |
+|------|---------|
+| `index.html` | Main portfolio page |
+| `styles.css` | Dark-themed stylesheet |
+| `projects.js` | Project data (edit to add permanent entries) |
+| `app.js` | Rendering logic + password-protected admin panel |
+
+## โ
Current Projects
+
+| Project | Status | URL |
+|---------|--------|-----|
+| SeePrice | Finished | https://seeprice.replit.app |
+| Mad Food Loop | Finished | https://www.madfoodloop.com |
+| CraftLingua | Finished | https://craftlingua.app |
+| LibraryScout | Finished | https://libraryscout.info |
+
+## โ Adding a New Project
+
+**Via the UI (temporary โ saved to browser localStorage):**
+1. Click the **+** button in the bottom-right corner of the site.
+2. Enter the admin password when prompted.
+3. Fill in the project form and click **Save Project**.
+
+**Permanently (recommended):**
+Open `projects.js` and add a new entry to the `SP_PROJECTS` array following the existing format.
+
+## ๐ Admin Password
+
+The admin password for the **+** button is `spdigital2024`.
+To change it, generate a SHA-256 hash of your new password and update `ADMIN_HASH` in `app.js`.
diff --git a/app.js b/app.js
new file mode 100644
index 0000000..7654448
--- /dev/null
+++ b/app.js
@@ -0,0 +1,236 @@
+/* ============================================================
+ SP Digital LLC โ App Logic
+ ============================================================
+ SECURITY NOTE: This is a lightweight, client-side guard designed
+ to prevent casual unauthorised use of the Add Project form on a
+ personal static site. The password is hashed with SHA-256 so it
+ is never stored or transmitted in plain text; however, because
+ this runs entirely in the browser, a determined attacker who
+ obtains the hash could attempt an offline dictionary attack.
+ For a fully secure admin panel, use a backend authentication
+ system. For a personal portfolio this tradeoff is acceptable.
+
+ Admin password: spdigital2024
+
+ To change the password, run in a browser console:
+ crypto.subtle.digest('SHA-256', new TextEncoder().encode('yourNewPassword'))
+ .then(b => console.log([...new Uint8Array(b)].map(x=>x.toString(16).padStart(2,'0')).join('')))
+ Then paste the result into ADMIN_HASH below.
+ ============================================================ */
+
+const ADMIN_HASH =
+ "0839c27dfda3d9fda3b8ea5ce6eed2a27befaa57b9f2450014c5e21cce762518";
+
+const STORAGE_KEY = "sp_projects_extra";
+
+/* โโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+
+async function sha256(text) {
+ const buf = await crypto.subtle.digest(
+ "SHA-256",
+ new TextEncoder().encode(text)
+ );
+ return [...new Uint8Array(buf)]
+ .map((x) => x.toString(16).padStart(2, "0"))
+ .join("");
+}
+
+function loadExtra() {
+ try {
+ return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
+ } catch {
+ return [];
+ }
+}
+
+function saveExtra(projects) {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(projects));
+}
+
+function allProjects() {
+ return [...SP_PROJECTS, ...loadExtra()];
+}
+
+function byStatus(status) {
+ return allProjects().filter((p) => p.status === status);
+}
+
+/* โโ Rendering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+
+function renderCard(project) {
+ const hasLink = project.url && project.url.trim() !== "";
+ const tagsHtml = (project.tags || [])
+ .map((t) => `${escHtml(t)}`)
+ .join("");
+
+ const footerHtml = hasLink
+ ? `
+ Visit โ
+ `
+ : "";
+
+ return `
+
+ ${escHtml(project.icon || "๐ท")}
+
+
${escHtml(project.name)}
+
${escHtml(project.tagline || "")}
+
+ ${escHtml(project.description || "")}
+ ${tagsHtml}
+ ${footerHtml ? `` : ""}
+ `;
+}
+
+function renderSection(status) {
+ const projects = byStatus(status);
+ const containerId = `grid-${status}`;
+ const el = document.getElementById(containerId);
+ if (!el) return;
+ if (projects.length === 0) {
+ el.innerHTML = `
No projects in this category yet.
`;
+ } else {
+ el.innerHTML = projects.map(renderCard).join("");
+ }
+}
+
+function renderAll() {
+ renderSection("finished");
+ renderSection("ongoing");
+ renderSection("idea");
+}
+
+/* โโ Escape helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+
+function escHtml(str) {
+ return String(str)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+function escAttr(str) {
+ // Only allow http/https URLs
+ const s = String(str).trim();
+ if (!/^https?:\/\//i.test(s)) return "#";
+ return escHtml(s);
+}
+
+/* โโ Modal helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+
+function openModal(id) {
+ document.getElementById(id).classList.add("open");
+}
+
+function closeModal(id) {
+ document.getElementById(id).classList.remove("open");
+}
+
+/* โโ Auth flow โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+
+async function handleAuth(e) {
+ e.preventDefault();
+ const pw = document.getElementById("auth-password").value;
+ const hash = await sha256(pw);
+ if (hash === ADMIN_HASH) {
+ closeModal("auth-modal");
+ document.getElementById("auth-password").value = "";
+ document.getElementById("auth-error").textContent = "";
+ openModal("add-modal");
+ } else {
+ document.getElementById("auth-error").textContent =
+ "Incorrect password. Try again.";
+ }
+}
+
+/* โโ Add-project flow โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+
+function handleAdd(e) {
+ e.preventDefault();
+ const name = document.getElementById("add-name").value.trim();
+ const tagline = document.getElementById("add-tagline").value.trim();
+ const description = document.getElementById("add-desc").value.trim();
+ const url = document.getElementById("add-url").value.trim();
+ const status = document.getElementById("add-status").value;
+ const tagsRaw = document.getElementById("add-tags").value.trim();
+ const icon = document.getElementById("add-icon").value.trim() || "๐ท";
+
+ const tags = tagsRaw
+ ? tagsRaw.split(",").map((t) => t.trim()).filter(Boolean)
+ : [];
+
+ const project = {
+ id: `custom-${Date.now()}`,
+ status,
+ name,
+ tagline,
+ description,
+ url: url || null,
+ tags,
+ icon,
+ };
+
+ const extras = loadExtra();
+ extras.push(project);
+ saveExtra(extras);
+
+ renderAll();
+ closeModal("add-modal");
+ e.target.reset();
+
+ // Scroll to the relevant section
+ const section = document.getElementById(`section-${status}`);
+ if (section) section.scrollIntoView({ behavior: "smooth" });
+}
+
+/* โโ Sticky nav active state โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+
+function updateActiveNav() {
+ const sections = ["finished", "ongoing", "idea"];
+ let current = sections[0];
+ sections.forEach((id) => {
+ const el = document.getElementById(`section-${id}`);
+ if (el && window.scrollY >= el.offsetTop - 120) current = id;
+ });
+ document.querySelectorAll("nav a").forEach((a) => {
+ a.classList.toggle("active", a.getAttribute("href") === `#section-${current}`);
+ });
+}
+
+/* โโ Bootstrap โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+
+document.addEventListener("DOMContentLoaded", () => {
+ // Initial render
+ renderAll();
+
+ // FAB โ open auth modal
+ document.getElementById("fab").addEventListener("click", () => {
+ document.getElementById("auth-password").value = "";
+ document.getElementById("auth-error").textContent = "";
+ openModal("auth-modal");
+ });
+
+ // Auth form submit
+ document.getElementById("auth-form").addEventListener("submit", handleAuth);
+
+ // Add-project form submit
+ document.getElementById("add-form").addEventListener("submit", handleAdd);
+
+ // Close buttons
+ document.querySelectorAll("[data-close]").forEach((btn) => {
+ btn.addEventListener("click", () => closeModal(btn.dataset.close));
+ });
+
+ // Close on backdrop click
+ document.querySelectorAll(".modal-overlay").forEach((overlay) => {
+ overlay.addEventListener("click", (e) => {
+ if (e.target === overlay) closeModal(overlay.id);
+ });
+ });
+
+ // Nav active state
+ window.addEventListener("scroll", updateActiveNav, { passive: true });
+ updateActiveNav();
+});
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..e689974
--- /dev/null
+++ b/index.html
@@ -0,0 +1,149 @@
+
+
+
+
+
+ SP Digital LLC โ Projects
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
๐ Admin Login
+
+
+
+
+
+
+
+
+
โ Add Project
+
+
+
+
+
+
+
+
+
+
diff --git a/projects.js b/projects.js
new file mode 100644
index 0000000..c195047
--- /dev/null
+++ b/projects.js
@@ -0,0 +1,86 @@
+// SP Digital LLC โ Project Data
+// Add new projects here or via the admin panel (password-protected).
+const SP_PROJECTS = [
+ // โโ FINISHED โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ {
+ id: "seeprice",
+ status: "finished",
+ name: "SeePrice",
+ tagline: "Track prices without the clutter.",
+ description:
+ "A clean, fast price-tracking web app that cuts through the noise so you can shop smarter.",
+ url: "https://seeprice.replit.app",
+ tags: ["Price Tracking", "Web App", "Replit"],
+ icon: "๐ฐ",
+ },
+ {
+ id: "madfoodloop",
+ status: "finished",
+ name: "Mad Food Loop",
+ tagline: "Recipes that loop back to joy.",
+ description:
+ "A food recipe discovery platform with a fun, looping exploration experience.",
+ url: "https://www.madfoodloop.com",
+ tags: ["Food", "Recipes", "Discovery"],
+ icon: "๐",
+ },
+ {
+ id: "craftlingua",
+ status: "finished",
+ name: "CraftLingua",
+ tagline: "Craft your language skills.",
+ description:
+ "A language-learning app focused on practical, crafted exercises for real-world fluency.",
+ url: "https://craftlingua.app",
+ tags: ["Language Learning", "Education", "App"],
+ icon: "๐ฃ๏ธ",
+ },
+ {
+ id: "libraryscout",
+ status: "finished",
+ name: "LibraryScout",
+ tagline: "Scout your next great read.",
+ description:
+ "Helps readers discover books available at their local library, saving time and money.",
+ url: "https://libraryscout.info",
+ tags: ["Books", "Library", "Discovery"],
+ icon: "๐",
+ },
+
+ // โโ ONGOING โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ {
+ id: "sp-portfolio",
+ status: "ongoing",
+ name: "SP Digital Portfolio",
+ tagline: "This very site.",
+ description:
+ "Centralised hub for all SP Digital LLC projects โ ideas, work in progress, and shipped apps.",
+ url: null,
+ tags: ["Meta", "Portfolio", "GitHub Pages"],
+ icon: "๐",
+ },
+
+ // โโ IDEAS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ {
+ id: "idea-habit-tracker",
+ status: "idea",
+ name: "Habit Forge",
+ tagline: "Build habits that stick.",
+ description:
+ "A minimalist habit-tracking app that focuses on streaks and micro-goals.",
+ url: null,
+ tags: ["Productivity", "Habits", "Concept"],
+ icon: "๐ฅ",
+ },
+ {
+ id: "idea-local-events",
+ status: "idea",
+ name: "LocalPulse",
+ tagline: "Feel the heartbeat of your neighbourhood.",
+ description:
+ "Hyper-local event aggregator that surfaces community events you'd actually care about.",
+ url: null,
+ tags: ["Community", "Events", "Concept"],
+ icon: "๐",
+ },
+];
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..543b000
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,352 @@
+/* ============================================================
+ SP Digital LLC โ Portfolio Styles
+ ============================================================ */
+
+/* ---------- Reset & base ---------- */
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+:root {
+ --bg: #0d1117;
+ --surface: #161b22;
+ --surface2: #21262d;
+ --border: #30363d;
+ --accent: #58a6ff;
+ --accent2: #79c0ff;
+ --green: #3fb950;
+ --yellow: #d29922;
+ --purple: #bc8cff;
+ --text: #e6edf3;
+ --muted: #8b949e;
+ --radius: 12px;
+ --shadow: 0 4px 24px rgba(0,0,0,.45);
+}
+
+html { scroll-behavior: smooth; }
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
+ background: var(--bg);
+ color: var(--text);
+ min-height: 100vh;
+ line-height: 1.6;
+}
+
+a { color: var(--accent); text-decoration: none; }
+a:hover { text-decoration: underline; }
+
+/* ---------- Header / Hero ---------- */
+header {
+ background: linear-gradient(135deg, #0d1117 0%, #1a2333 100%);
+ border-bottom: 1px solid var(--border);
+ padding: 3.5rem 1.5rem 2.5rem;
+ text-align: center;
+}
+
+.hero-logo {
+ font-size: 2.8rem;
+ font-weight: 800;
+ letter-spacing: -1px;
+ background: linear-gradient(90deg, var(--accent), var(--purple));
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.hero-sub {
+ color: var(--muted);
+ margin-top: .5rem;
+ font-size: 1.1rem;
+}
+
+/* ---------- Nav ---------- */
+nav {
+ display: flex;
+ justify-content: center;
+ gap: 1rem;
+ padding: 1rem 1.5rem;
+ border-bottom: 1px solid var(--border);
+ background: var(--surface);
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+
+nav a {
+ color: var(--muted);
+ font-size: .9rem;
+ font-weight: 500;
+ padding: .35rem .75rem;
+ border-radius: 6px;
+ transition: color .2s, background .2s;
+}
+nav a:hover, nav a.active {
+ color: var(--text);
+ background: var(--surface2);
+ text-decoration: none;
+}
+
+/* ---------- Main content ---------- */
+main {
+ max-width: 1100px;
+ margin: 0 auto;
+ padding: 2.5rem 1.25rem 6rem;
+}
+
+/* ---------- Section headings ---------- */
+.section-header {
+ display: flex;
+ align-items: center;
+ gap: .75rem;
+ margin: 3rem 0 1.25rem;
+}
+
+.section-header h2 {
+ font-size: 1.4rem;
+ font-weight: 700;
+}
+
+.section-badge {
+ font-size: .75rem;
+ font-weight: 600;
+ padding: .2rem .6rem;
+ border-radius: 20px;
+ text-transform: uppercase;
+ letter-spacing: .05em;
+}
+
+.badge-finished { background: rgba(63,185,80,.15); color: var(--green); border: 1px solid rgba(63,185,80,.35); }
+.badge-ongoing { background: rgba(88,166,255,.15); color: var(--accent); border: 1px solid rgba(88,166,255,.35); }
+.badge-idea { background: rgba(188,140,255,.15); color: var(--purple); border: 1px solid rgba(188,140,255,.35); }
+
+/* ---------- Grid ---------- */
+.grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 1.25rem;
+}
+
+/* ---------- Project card ---------- */
+.card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: .75rem;
+ transition: border-color .2s, box-shadow .2s, transform .15s;
+}
+.card:hover {
+ border-color: var(--accent);
+ box-shadow: var(--shadow);
+ transform: translateY(-2px);
+}
+
+.card-icon { font-size: 2.2rem; }
+
+.card-name {
+ font-size: 1.15rem;
+ font-weight: 700;
+}
+
+.card-tagline {
+ font-size: .9rem;
+ color: var(--muted);
+ font-style: italic;
+ margin-top: -.25rem;
+}
+
+.card-desc {
+ font-size: .9rem;
+ color: var(--muted);
+ flex: 1;
+}
+
+.card-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: .4rem;
+}
+
+.tag {
+ font-size: .72rem;
+ padding: .15rem .5rem;
+ border-radius: 20px;
+ background: var(--surface2);
+ border: 1px solid var(--border);
+ color: var(--muted);
+}
+
+.card-footer {
+ margin-top: .25rem;
+}
+
+.btn-visit {
+ display: inline-flex;
+ align-items: center;
+ gap: .4rem;
+ background: var(--accent);
+ color: #0d1117;
+ font-weight: 600;
+ font-size: .85rem;
+ padding: .45rem 1rem;
+ border-radius: 8px;
+ transition: background .2s, transform .1s;
+}
+.btn-visit:hover { background: var(--accent2); text-decoration: none; transform: scale(1.03); }
+
+.status-dot {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ margin-right: .4rem;
+}
+.dot-finished { background: var(--green); }
+.dot-ongoing { background: var(--accent); animation: pulse 1.6s infinite; }
+.dot-idea { background: var(--purple); }
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: .4; }
+}
+
+/* ---------- Add / FAB button ---------- */
+#fab {
+ position: fixed;
+ bottom: 2rem;
+ right: 2rem;
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--accent), var(--purple));
+ border: none;
+ color: #fff;
+ font-size: 1.6rem;
+ cursor: pointer;
+ box-shadow: 0 4px 20px rgba(88,166,255,.45);
+ transition: transform .2s, box-shadow .2s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 200;
+}
+#fab:hover { transform: scale(1.1) rotate(90deg); box-shadow: 0 6px 28px rgba(88,166,255,.6); }
+#fab title { display: none; }
+
+/* ---------- Modal overlay ---------- */
+.modal-overlay {
+ display: none;
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,.7);
+ z-index: 300;
+ align-items: center;
+ justify-content: center;
+ padding: 1rem;
+}
+.modal-overlay.open { display: flex; }
+
+.modal {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 2rem;
+ width: 100%;
+ max-width: 480px;
+ box-shadow: var(--shadow);
+ position: relative;
+}
+
+.modal h2 { font-size: 1.2rem; margin-bottom: 1.25rem; }
+
+.modal-close {
+ position: absolute;
+ top: 1rem;
+ right: 1rem;
+ background: none;
+ border: none;
+ color: var(--muted);
+ font-size: 1.4rem;
+ cursor: pointer;
+ line-height: 1;
+}
+.modal-close:hover { color: var(--text); }
+
+/* ---------- Forms ---------- */
+.form-group { display: flex; flex-direction: column; gap: .35rem; margin-bottom: 1rem; }
+
+label { font-size: .85rem; color: var(--muted); font-weight: 500; }
+
+input, select, textarea {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ color: var(--text);
+ padding: .55rem .8rem;
+ font-size: .9rem;
+ font-family: inherit;
+ transition: border-color .2s;
+}
+input:focus, select:focus, textarea:focus {
+ outline: none;
+ border-color: var(--accent);
+}
+select option { background: var(--surface); }
+textarea { resize: vertical; min-height: 80px; }
+
+.btn-primary {
+ width: 100%;
+ padding: .65rem;
+ background: var(--accent);
+ border: none;
+ border-radius: 8px;
+ color: #0d1117;
+ font-weight: 700;
+ font-size: .95rem;
+ cursor: pointer;
+ transition: background .2s;
+ margin-top: .25rem;
+}
+.btn-primary:hover { background: var(--accent2); }
+
+.btn-secondary {
+ width: 100%;
+ padding: .6rem;
+ background: var(--surface2);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ color: var(--text);
+ font-weight: 600;
+ font-size: .9rem;
+ cursor: pointer;
+ transition: background .2s;
+ margin-top: .5rem;
+}
+.btn-secondary:hover { background: var(--border); }
+
+.error-msg {
+ color: #ff7b72;
+ font-size: .82rem;
+ margin-top: -.5rem;
+ margin-bottom: .5rem;
+}
+
+/* ---------- Empty state ---------- */
+.empty { color: var(--muted); font-size: .9rem; padding: 1.5rem 0; }
+
+/* ---------- Footer ---------- */
+footer {
+ text-align: center;
+ padding: 2rem 1rem;
+ border-top: 1px solid var(--border);
+ color: var(--muted);
+ font-size: .85rem;
+}
+
+/* ---------- Responsive ---------- */
+@media (max-width: 600px) {
+ .hero-logo { font-size: 2rem; }
+ nav { flex-wrap: wrap; gap: .5rem; }
+ #fab { bottom: 1.25rem; right: 1.25rem; }
+}