commit 724f16b589eae33b2f40d6693bae6bb788a90f83 Author: ScuroNeko Date: Tue Apr 14 16:20:22 2026 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8cf372 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +.vscode/ +.codex \ No newline at end of file diff --git a/data/links.json b/data/links.json new file mode 100644 index 0000000..9bf1f52 --- /dev/null +++ b/data/links.json @@ -0,0 +1,23 @@ +[ + { + "key": "git", + "name": "Git", + "label": "git.scuroneko.dev", + "url": "https://git.scuroneko.dev/ScuroNeko", + "tone": "red" + }, + { + "key": "gh", + "name": "GitHub", + "label": "github.com/ScuroNeko", + "url": "https://github.com/ScuroNeko", + "tone": "purple" + }, + { + "key": "tg", + "name": "Telegram", + "label": "@scuroneko", + "url": "https://t.me/scuroneko", + "tone": "blue" + } +] diff --git a/data/projects.json b/data/projects.json new file mode 100644 index 0000000..7e2ffa9 --- /dev/null +++ b/data/projects.json @@ -0,0 +1,21 @@ +[ + { + "description": "A personal page for projects, stack notes, and developer work.", + "key": "web", + "name": "ScuroNeko.dev", + "stack": ["html", "css", "js"], + "status": "active", + "tone": "purple" + }, + { + "description": "A personal page for projects, stack notes, and developer work.", + "key": "lib", + "links": { + "sources": "https://git.scuroneko.dev/ScuroNeko/Laniakea" + }, + "name": "Laniakea", + "stack": ["go", "tg"], + "status": "active", + "tone": "purple" + } +] diff --git a/data/stack.json b/data/stack.json new file mode 100644 index 0000000..55f6d9e --- /dev/null +++ b/data/stack.json @@ -0,0 +1,69 @@ +[ + { + "key": "go", + "name": "Go", + "tone": "blue", + "years": 7 + }, + { + "key": "py", + "name": "Python", + "tone": "gold", + "years": 8 + }, + { + "key": "ts", + "name": "TypeScript", + "tone": "purple", + "years": 5 + }, + { + "key": "re", + "name": "React", + "tags": ["lib", "js"], + "tone": "blue", + "years": 7 + }, + { + "key": "mdb", + "name": "MongoDB", + "tags": ["db"], + "tone": "red", + "years": 2 + }, + { + "key": "psql", + "name": "PostgreSQL", + "tags": ["db"], + "tone": "blue", + "years": 7 + }, + { + "key": "tg", + "name": "Telegram API", + "tags": ["api"], + "tone": "blue", + "years": 2 + }, + { + "hide": true, + "key": "html", + "name": "HTML", + "tone": "red", + "years": 7 + }, + { + "hide": true, + "key": "css", + "name": "CSS", + "tone": "cyan", + "years": 7 + }, + { + "hide": true, + "key": "js", + "name": "JavaScript", + "tone": "cyan", + "years": 7 + } +] diff --git a/errors/403.html b/errors/403.html new file mode 100644 index 0000000..9e610d3 --- /dev/null +++ b/errors/403.html @@ -0,0 +1,13 @@ + + + + + + + 403 Forbidden... + + + + + + diff --git a/errors/404.html b/errors/404.html new file mode 100644 index 0000000..b6ba95f --- /dev/null +++ b/errors/404.html @@ -0,0 +1,13 @@ + + + + + + + 404 Not found... + + + + + + diff --git a/errors/500.html b/errors/500.html new file mode 100644 index 0000000..70a333e --- /dev/null +++ b/errors/500.html @@ -0,0 +1,13 @@ + + + + + + + 500 Internal Server Error... + + + + + + diff --git a/errors/502.html b/errors/502.html new file mode 100644 index 0000000..3b2bb9b --- /dev/null +++ b/errors/502.html @@ -0,0 +1,13 @@ + + + + + + + 502 Bad gateway... + + + + + + diff --git a/errors/503.html b/errors/503.html new file mode 100644 index 0000000..06e7b7e --- /dev/null +++ b/errors/503.html @@ -0,0 +1,13 @@ + + + + + + + 503 Service unavailable... + + + + + + diff --git a/errors/504.html b/errors/504.html new file mode 100644 index 0000000..ed82ca5 --- /dev/null +++ b/errors/504.html @@ -0,0 +1,13 @@ + + + + + + + 504 Gateway timeout... + + + + + + diff --git a/errors/errors.css b/errors/errors.css new file mode 100644 index 0000000..1e60c18 --- /dev/null +++ b/errors/errors.css @@ -0,0 +1,24 @@ +@keyframes fade { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +html, +body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; +} +body { + background-color: #1f1f28; + display: flex; +} +img { + animation: fade 500ms ease-in-out; + margin: auto; + width: 100%; +} diff --git a/errors/img/403.png b/errors/img/403.png new file mode 100755 index 0000000..e450b88 Binary files /dev/null and b/errors/img/403.png differ diff --git a/errors/img/404.png b/errors/img/404.png new file mode 100755 index 0000000..6571800 Binary files /dev/null and b/errors/img/404.png differ diff --git a/errors/img/500.png b/errors/img/500.png new file mode 100755 index 0000000..f3336e1 Binary files /dev/null and b/errors/img/500.png differ diff --git a/errors/img/502.png b/errors/img/502.png new file mode 100755 index 0000000..9e2ebf6 Binary files /dev/null and b/errors/img/502.png differ diff --git a/errors/img/503.png b/errors/img/503.png new file mode 100755 index 0000000..b07fef3 Binary files /dev/null and b/errors/img/503.png differ diff --git a/errors/img/504.png b/errors/img/504.png new file mode 100755 index 0000000..90dcb62 Binary files /dev/null and b/errors/img/504.png differ diff --git a/icons/css.svg b/icons/css.svg new file mode 100644 index 0000000..59bb243 --- /dev/null +++ b/icons/css.svg @@ -0,0 +1 @@ +CSS \ No newline at end of file diff --git a/icons/gh.svg b/icons/gh.svg new file mode 100644 index 0000000..538ec5b --- /dev/null +++ b/icons/gh.svg @@ -0,0 +1 @@ +GitHub \ No newline at end of file diff --git a/icons/git.svg b/icons/git.svg new file mode 100644 index 0000000..d870f71 --- /dev/null +++ b/icons/git.svg @@ -0,0 +1 @@ +Gitea \ No newline at end of file diff --git a/icons/go.svg b/icons/go.svg new file mode 100644 index 0000000..0cadd56 --- /dev/null +++ b/icons/go.svg @@ -0,0 +1 @@ +Go \ No newline at end of file diff --git a/icons/html.svg b/icons/html.svg new file mode 100644 index 0000000..e8b1e37 --- /dev/null +++ b/icons/html.svg @@ -0,0 +1 @@ +HTML5 \ No newline at end of file diff --git a/icons/js.svg b/icons/js.svg new file mode 100644 index 0000000..91e5162 --- /dev/null +++ b/icons/js.svg @@ -0,0 +1 @@ +JavaScript \ No newline at end of file diff --git a/icons/mdb.svg b/icons/mdb.svg new file mode 100644 index 0000000..a4c8174 --- /dev/null +++ b/icons/mdb.svg @@ -0,0 +1 @@ +MongoDB \ No newline at end of file diff --git a/icons/psql.svg b/icons/psql.svg new file mode 100644 index 0000000..dcf75b7 --- /dev/null +++ b/icons/psql.svg @@ -0,0 +1 @@ +PostgreSQL \ No newline at end of file diff --git a/icons/py.svg b/icons/py.svg new file mode 100644 index 0000000..30587d8 --- /dev/null +++ b/icons/py.svg @@ -0,0 +1 @@ +Python \ No newline at end of file diff --git a/icons/re.svg b/icons/re.svg new file mode 100644 index 0000000..6006995 --- /dev/null +++ b/icons/re.svg @@ -0,0 +1 @@ +React \ No newline at end of file diff --git a/icons/tg.svg b/icons/tg.svg new file mode 100644 index 0000000..c46b0ed --- /dev/null +++ b/icons/tg.svg @@ -0,0 +1 @@ +Telegram \ No newline at end of file diff --git a/icons/ts.svg b/icons/ts.svg new file mode 100644 index 0000000..e8beb74 --- /dev/null +++ b/icons/ts.svg @@ -0,0 +1 @@ +TypeScript \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..3eb0e8e --- /dev/null +++ b/index.html @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + ScuroNeko + + +
+
+

scuroneko.dev · self-hosted

+
+

ScuroNeko

+ +
+

Go and JS developer

+ +
+
+ +
+
+ 01 +

Stack

+
+

Tools I reach for when the task needs to ship.

+
+
+ +
+
+ 02 +

Projects

+
+

Things with enough shape to point at.

+
+
+ + + + diff --git a/js/data.js b/js/data.js new file mode 100644 index 0000000..1da6ea2 --- /dev/null +++ b/js/data.js @@ -0,0 +1,29 @@ +let stackList = []; +let projectsList = []; +let linksList = []; + +const fetchJson = async (path) => { + const response = await fetch(path); + + if (!response.ok) { + throw new Error(`Failed to load ${path}: ${response.status}`); + } + + return response.json(); +}; + +const loadIcon = async (key) => { + const response = await fetch(`./icons/${key}.svg`); + + if (!response.ok) { + return key; + } + + return response.text(); +}; + +const formatIndex = (index) => String(index + 1).padStart(2, "0"); + +document.addEventListener("DOMContentLoaded", () => { + loadStack().then(loadProjects).then(loadLinks).catch(console.error); +}); diff --git a/js/links.js b/js/links.js new file mode 100644 index 0000000..b1d5fda --- /dev/null +++ b/js/links.js @@ -0,0 +1,48 @@ +const loadLinks = async () => { + const linkContainer = document.getElementById("links"); + + if (!linkContainer) { + return; + } + + linksList = await fetchJson("./data/links.json"); + const linkCards = linksList.map(buildLinkCard); + linkContainer.replaceChildren(...linkCards); +}; + +const buildLinkCard = (item, index) => { + const card = document.createElement("a"); + card.classList.add("link-card", `tone-${item.tone ?? "mint"}`); + card.href = item.url; + card.target = "_blank"; + card.rel = "noreferrer"; + + const indexBadge = document.createElement("span"); + indexBadge.classList.add("link-card__index"); + indexBadge.textContent = formatIndex(index); + + const badge = document.createElement("span"); + badge.classList.add("link-card__badge"); + badge.textContent = item.key; + loadIcon(item.key).then((svg) => (badge.innerHTML = svg)); + + const body = document.createElement("span"); + body.classList.add("link-card__body"); + + const name = document.createElement("span"); + name.classList.add("link-card__name"); + name.textContent = item.name; + + const label = document.createElement("span"); + label.classList.add("link-card__label"); + label.textContent = item.label; + + const arrow = document.createElement("span"); + arrow.classList.add("link-card__arrow"); + arrow.textContent = "open"; + + body.append(name, label); + card.append(indexBadge, badge, body, arrow); + + return card; +}; diff --git a/js/projects.js b/js/projects.js new file mode 100644 index 0000000..7ef69b8 --- /dev/null +++ b/js/projects.js @@ -0,0 +1,94 @@ +const loadProjects = async () => { + const projectContainer = document.getElementById("projects"); + + if (!projectContainer) { + return; + } + + projectsList = await fetchJson("./data/projects.json"); + + if (projectsList.length === 0) { + const empty = document.createElement("p"); + empty.classList.add("project-empty"); + empty.textContent = "Projects are taking shape."; + projectContainer.replaceChildren(empty); + return; + } + + const projectCards = projectsList.map(buildProjectCard); + projectContainer.replaceChildren(...projectCards); +}; + +const buildProjectCard = (project, index) => { + const card = document.createElement("article"); + card.classList.add("project-card", `project-card--${project.tone ?? "mint"}`); + + const header = document.createElement("div"); + header.classList.add("project-card__header"); + + const indexBadge = document.createElement("span"); + indexBadge.classList.add("project-card__index"); + indexBadge.textContent = formatIndex(index); + + const key = document.createElement("span"); + key.classList.add("project-card__key"); + key.textContent = project.key; + + const status = document.createElement("span"); + status.classList.add("project-card__status"); + status.textContent = project.status ?? "project"; + + const title = document.createElement("h3"); + title.classList.add("project-card__title"); + title.textContent = project.name; + + const description = document.createElement("p"); + description.classList.add("project-card__description"); + description.textContent = project.description; + + const stack = document.createElement("div"); + stack.classList.add("project-card__stack"); + + if (project.stack) { + const badgeToStack = loadStackForProject(project.stack); + + stack.append( + ...project.stack.map((item) => { + const chip = document.createElement("span"); + const stackItem = badgeToStack[item] ?? { + name: item, + tone: "blue", + }; + + chip.textContent = stackItem.name; + chip.classList.add( + "project-card__stack-chip", + `tone-${stackItem?.tone ?? "mint"}`, + ); + + return chip; + }), + ); + } + + header.append(indexBadge, key, status); + card.append(header, title, description, stack); + + if (project.links) { + const links = document.createElement("div"); + links.classList.add("project-card__links"); + links.append( + ...Object.entries(project.links).map(([label, url]) => { + const link = document.createElement("a"); + link.href = url; + link.textContent = label; + link.target = "_blank"; + link.rel = "noreferrer"; + return link; + }), + ); + card.append(links); + } + + return card; +}; diff --git a/js/stack.js b/js/stack.js new file mode 100644 index 0000000..8ee4901 --- /dev/null +++ b/js/stack.js @@ -0,0 +1,75 @@ +const getExperienceLevel = (years) => { + if (years >= 7) return "Core"; + if (years >= 4) return "Strong"; + return "Growing"; +}; + +const loadStackForProject = (keys) => { + const stacks = stackList.filter((stack) => keys.includes(stack.key)); + const output = {}; + + stacks.forEach((stack) => { + output[stack.key] = stack; + }); + + return output; +}; + +const loadStack = async () => { + const stackContainer = document.getElementById("stack"); + + if (!stackContainer) { + return; + } + + stackList = await fetchJson("./data/stack.json"); + const stackCards = stackList.map(buildStackCard).filter(Boolean); + stackContainer.replaceChildren(...stackCards); +}; + +const buildStackCard = (stack) => { + if (stack?.hide === true) { + return null; + } + + const card = document.createElement("div"); + card.classList.add("stack-card", `tone-${stack.tone ?? "mint"}`); + + const badge = document.createElement("div"); + badge.classList.add("stack-card__badge"); + badge.textContent = stack.key; + loadIcon(stack.key).then((svg) => (badge.innerHTML = svg)); + + const body = document.createElement("div"); + body.classList.add("stack-card__body"); + + const eyebrow = document.createElement("p"); + eyebrow.classList.add("stack-card__eyebrow"); + eyebrow.textContent = getExperienceLevel(stack.years); + + const name = document.createElement("h3"); + name.classList.add("stack-card__title"); + name.textContent = stack.name; + + const footer = document.createElement("div"); + footer.classList.add("stack-card__footer"); + + const years = document.createElement("span"); + years.textContent = `${stack.years}+ years`; + footer.append(years); + + if (stack.tags) { + footer.append( + ...stack.tags.map((tag) => { + const badge = document.createElement("span"); + badge.textContent = tag; + return badge; + }), + ); + } + + body.append(eyebrow, name, footer); + card.append(badge, body); + + return card; +}; diff --git a/styles/hero.css b/styles/hero.css new file mode 100644 index 0000000..c0cbf72 --- /dev/null +++ b/styles/hero.css @@ -0,0 +1,98 @@ +.site-hero { + align-items: center; + display: grid; + min-height: calc(100vh - 96px); + width: calc(100% - 24px); + max-width: var(--block-size); + margin-bottom: 20px; +} + +.site-hero__main { + justify-self: center; + min-width: 0; + width: min(100%, 460px); +} + +.site-pill { + display: inline-flex; + align-items: center; + min-height: 30px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-soft); + font-family: monospace; + font-size: 0.82rem; +} + +.site-title { + display: flex; + align-items: center; + gap: 14px; + margin-top: 16px; +} + +.site-mark { + display: grid; + place-items: center; + width: 36px; + height: 36px; + border: 1px solid color-mix(in srgb, var(--danger) 70%, var(--border)); + border-radius: 8px; + color: var(--danger); + font-family: serif; + font-size: 1.2rem; + line-height: 1; +} + +.site-subtitle { + margin-top: 8px; + max-width: 640px; + color: var(--text-soft); + font-size: 1.05rem; + line-height: 1.65; +} + +.site-nav { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 24px; +} + +.site-nav a { + min-height: 40px; + padding: 10px 18px; + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + text-decoration: none; + transition: + background-color 160ms ease, + border-color 160ms ease, + color 160ms ease; +} + +.site-nav a:hover { + border-color: var(--primary); + background-color: var(--glow-blue); + color: var(--text); +} + +@media (max-width: 780px) { + .site-hero { + grid-template-columns: 1fr; + } +} + +@media (max-width: 560px) { + .site-hero { + width: 100%; + } + + .site-title { + align-items: flex-start; + flex-direction: column; + gap: 10px; + } +} diff --git a/styles/links.css b/styles/links.css new file mode 100644 index 0000000..f4a463a --- /dev/null +++ b/styles/links.css @@ -0,0 +1,96 @@ +.link-grid { + display: grid; + gap: 12px; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-top: 18px; +} + +.link-card { + display: grid; + grid-template-columns: auto auto minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + min-height: 76px; + padding: 14px; + border: 1px solid color-mix(in srgb, var(--card-accent) 30%, var(--border)); + border-radius: 8px; + background-color: rgba(42, 42, 55, 0.72); + color: var(--text); + text-decoration: none; + transition: + background-color 160ms ease, + border-color 160ms ease, + transform 160ms ease; +} + +.link-card:hover { + border-color: var(--card-accent); + background-color: color-mix(in srgb, var(--card-glow) 36%, var(--surface)); + color: var(--text); + transform: translateY(-2px); +} + +.link-card__index { + color: var(--text-soft); + font-family: monospace; + font-size: 0.75rem; +} + +.link-card__badge { + display: grid; + place-items: center; + width: 42px; + height: 42px; + border: 1px solid color-mix(in srgb, var(--card-accent) 58%, var(--border)); + border-radius: 8px; + color: var(--card-accent); + font-family: "Barlow Condensed", sans-serif; + font-size: 1rem; + font-weight: 800; + text-transform: uppercase; +} + +.link-card__badge svg { + width: 22px; + height: 22px; + fill: currentColor; +} + +.link-card__body { + display: grid; + min-width: 0; +} + +.link-card__name { + color: var(--text); + font-family: "Barlow Condensed", sans-serif; + font-size: 1.25rem; + font-weight: 700; +} + +.link-card__label { + overflow: hidden; + color: var(--text-soft); + font-size: 0.92rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.link-card__arrow { + color: var(--card-accent); + font-size: 0.82rem; + font-weight: 700; + text-transform: uppercase; +} + +@media (max-width: 780px) { + .link-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 560px) { + .link-grid { + grid-template-columns: 1fr; + } +} diff --git a/styles/main.css b/styles/main.css new file mode 100644 index 0000000..ac5b0fd --- /dev/null +++ b/styles/main.css @@ -0,0 +1,172 @@ +:root { + --bg: #1f1f28; + --surface: #2a2a37; + --border: #4a4a61; + + --text: #dcd7ba; + --text-soft: #c8c093; + --text-muted: #938056; + + --primary: #7e9cd8; + --accent: #7aa89f; + --warm: #e6c384; + --danger: #c34043; + --glow-blue: rgba(126, 156, 216, 0.2); + --glow-gold: rgba(230, 195, 132, 0.2); + --glow-mint: rgba(122, 168, 159, 0.2); + --glow-purple: rgba(184, 118, 211, 0.26); + --glow-red: rgba(195, 64, 67, 0.2); + --glow-cyan: rgba(127, 180, 202, 0.2); + --glow-green: rgba(152, 187, 108, 0.18); + --glow-pink: rgba(210, 126, 153, 0.18); + + --block-size: 980px; + + font-size: 16px; +} + +* { + padding: 0; + margin: 0; + box-sizing: border-box; +} + +html, +body { + width: 100%; + height: 100%; + margin: 0; + scroll-behavior: smooth; +} + +body { + padding: 48px 12px; + font-family: "Barlow", sans-serif; + background-color: var(--bg); + color: var(--text); + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + min-height: 100%; +} + +a { + color: var(--primary); +} + +a:hover { + color: var(--accent); +} + +h1 { + font-size: 3.5rem; +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.5rem; +} + +h4 { + font-size: 1.2rem; +} + +h5 { + font-size: 1rem; +} + +h6 { + font-size: 0.8rem; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + padding: 0; + font-family: "Barlow Condensed", sans-serif; +} + +.block { + width: calc(100% - 24px); + max-width: var(--block-size); + padding: 18px 0; + margin: 12px 24px; + border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent); +} + +.section-heading { + display: flex; + align-items: baseline; + gap: 16px; +} + +.section-index { + color: var(--text-soft); + font-family: monospace; + font-size: 0.82rem; +} + +.block-subtitle { + color: var(--text-soft); + margin-top: 4px; + max-width: 560px; +} + +.tone-blue { + --card-accent: var(--primary); + --card-glow: var(--glow-blue); +} + +.tone-gold { + --card-accent: var(--warm); + --card-glow: var(--glow-gold); +} + +.tone-mint { + --card-accent: var(--accent); + --card-glow: var(--glow-mint); +} + +.tone-purple { + --card-accent: #b876d3; + --card-glow: var(--glow-purple); +} + +.tone-red { + --card-accent: var(--danger); + --card-glow: var(--glow-red); +} + +.tone-cyan { + --card-accent: #7fb4ca; + --card-glow: var(--glow-cyan); +} + +.tone-green { + --card-accent: #98bb6c; + --card-glow: var(--glow-green); +} + +.tone-pink { + --card-accent: #d27e99; + --card-glow: var(--glow-pink); +} + +@media (max-width: 560px) { + body { + padding-top: 32px; + } + + .block { + width: 100%; + margin-inline: 0; + } +} diff --git a/styles/projects.css b/styles/projects.css new file mode 100644 index 0000000..648e4e1 --- /dev/null +++ b/styles/projects.css @@ -0,0 +1,161 @@ +.project-card.project-card--blue { + --project-accent: var(--primary); + --project-glow: var(--glow-blue); +} + +.project-card.project-card--gold { + --project-accent: var(--warm); + --project-glow: var(--glow-gold); +} + +.project-card.project-card--mint { + --project-accent: var(--accent); + --project-glow: var(--glow-mint); +} + +.project-card.project-card--purple { + --project-accent: #b876d3; + --project-glow: var(--glow-purple); +} + +.project-card.project-card--red { + --project-accent: var(--danger); + --project-glow: var(--glow-red); +} + +.project-card.project-card--cyan { + --project-accent: #7fb4ca; + --project-glow: var(--glow-cyan); +} + +.project-card.project-card--green { + --project-accent: #98bb6c; + --project-glow: var(--glow-green); +} + +.project-card.project-card--pink { + --project-accent: #d27e99; + --project-glow: var(--glow-pink); +} + +.project-grid { + display: grid; + gap: 18px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin-top: 18px; +} + +.project-card { + --project-accent: var(--accent); + --project-glow: var(--glow-mint); + display: flex; + flex-direction: column; + min-height: 220px; + padding: 22px; + border: 1px solid color-mix(in srgb, var(--project-accent) 36%, var(--border)); + border-radius: 8px; + background: + linear-gradient(140deg, var(--project-glow), transparent 48%), + linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent), + var(--surface); + box-shadow: 0 18px 42px rgba(0, 0, 0, 0.2); + transition: + border-color 160ms ease, + transform 160ms ease, + box-shadow 160ms ease; +} + +.project-card:hover { + border-color: var(--project-accent); + box-shadow: 0 24px 52px rgba(0, 0, 0, 0.28); + transform: translateY(-2px); +} + +.project-card__header { + display: flex; + align-items: center; + gap: 12px; +} + +.project-card__index { + color: var(--text-soft); + font-family: monospace; + font-size: 0.82rem; +} + +.project-card__key, +.project-card__status { + border-radius: 8px; + padding: 4px 8px; + font-size: 0.82rem; + font-weight: 700; +} + +.project-card__key { + border: 1px solid color-mix(in srgb, var(--project-accent) 56%, var(--border)); + color: var(--project-accent); + text-transform: uppercase; +} + +.project-card__status { + margin-left: auto; + background-color: rgba(31, 31, 40, 0.52); + color: var(--text-soft); +} + +.project-card__title { + margin-top: 22px; + color: var(--text); + font-size: 1.75rem; +} + +.project-card__description { + margin-top: 8px; + color: var(--text-soft); + line-height: 1.55; +} + +.project-card__stack { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 18px; +} + +.project-card__stack span { + border-radius: 8px; + border: 1px solid var(--card-glow); + padding: 5px 9px; + background-color: rgba(31, 31, 40, 0.42); + color: var(--card-accent); + font-size: 0.9rem; +} + +.project-card__links { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: auto; + padding-top: 24px; +} + +.project-card__links a { + border-bottom: 1px solid currentColor; + color: var(--project-accent); + font-weight: 700; + text-decoration: none; +} + +.project-empty { + grid-column: 1 / -1; + border: 1px solid var(--border); + border-radius: 8px; + padding: 18px; + color: var(--text-soft); +} + +@media (max-width: 780px) { + .project-grid { + grid-template-columns: 1fr; + } +} diff --git a/styles/stack.css b/styles/stack.css new file mode 100644 index 0000000..c432b6e --- /dev/null +++ b/styles/stack.css @@ -0,0 +1,108 @@ +.stack-grid { + display: grid; + gap: 16px; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-top: 18px; +} + +.stack-card { + --card-accent: var(--accent); + --card-glow: var(--glow-mint); + position: relative; + display: grid; + grid-template-columns: auto 1fr; + gap: 16px; + min-height: 112px; + overflow: hidden; + padding: 18px; + border: 1px solid color-mix(in srgb, var(--card-accent) 44%, var(--border)); + border-radius: 8px; + background: + linear-gradient(135deg, var(--card-glow), transparent 42%), + linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent), + var(--surface); + box-shadow: 0 16px 36px rgba(0, 0, 0, 0.18); + transition: + border-color 160ms ease, + transform 160ms ease, + box-shadow 160ms ease; + user-select: none; +} + +.stack-card:hover { + border-color: var(--card-accent); + box-shadow: 0 20px 44px rgba(0, 0, 0, 0.26); + transform: translateY(-2px); +} + +.stack-card__badge { + display: grid; + place-items: center; + width: 58px; + height: 58px; + border-radius: 8px; + background-color: rgba(31, 31, 40, 0.8); + border: 1px solid color-mix(in srgb, var(--card-accent) 64%, var(--border)); + color: var(--card-accent); + font-family: "Barlow Condensed", sans-serif; + font-size: 1.45rem; + font-weight: 700; +} + +.stack-card__badge svg { + width: 34px; + height: 34px; + fill: currentColor; +} + +.stack-card__body { + display: flex; + flex-direction: column; + min-width: 0; +} + +.stack-card__eyebrow { + color: var(--card-accent); + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0; + text-transform: uppercase; +} + +.stack-card__title { + margin-top: 4px; + color: var(--text); +} + +.stack-card__footer { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: auto; + padding-top: 12px; + color: var(--text-soft); + font-size: 0.95rem; +} + +.stack-card__footer span { + border-radius: 8px; + border: 1px solid rgba(220, 215, 186, 0.16); + padding: 4px 8px; + background-color: rgba(31, 31, 40, 0.4); +} + +@media (max-width: 780px) { + .stack-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 560px) { + .stack-grid { + grid-template-columns: 1fr; + } + + .stack-card { + grid-template-columns: 1fr; + } +}