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.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 + + + + + + +
+ +

Ideas ยท Apps ยท Everything in between

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

Finished Apps

+ Live +
+
+
+ + +
+
+ +

Ongoing Projects

+ In Progress +
+
+
+ + +
+
+ +

Ideas

+ Concept +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + 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; } +}