diff --git a/data/projects.json b/data/projects.json index 7e2ffa9..e7201c2 100644 --- a/data/projects.json +++ b/data/projects.json @@ -2,6 +2,10 @@ { "description": "A personal page for projects, stack notes, and developer work.", "key": "web", + "links": { + "homepage": "https://scuroneko.dev", + "sources": "https://git.scuroneko.dev/ScuroNeko/scuroneko.dev" + }, "name": "ScuroNeko.dev", "stack": ["html", "css", "js"], "status": "active", @@ -16,6 +20,6 @@ "name": "Laniakea", "stack": ["go", "tg"], "status": "active", - "tone": "purple" + "tone": "rose" } ] diff --git a/data/schemas/links.schema.json b/data/schemas/links.schema.json new file mode 100644 index 0000000..bc3f3b7 --- /dev/null +++ b/data/schemas/links.schema.json @@ -0,0 +1,32 @@ +{ + "$id": "https://scuroneko.dev/schemas/links.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "items": { + "additionalProperties": false, + "properties": { + "key": { + "minLength": 1, + "type": "string" + }, + "label": { + "minLength": 1, + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "tone": { + "$ref": "./shared.schema.json#/$defs/tone" + }, + "url": { + "format": "uri", + "type": "string" + } + }, + "required": ["key", "name", "label", "url", "tone"], + "type": "object" + }, + "title": "Links", + "type": "array" +} diff --git a/data/schemas/projects.schema.json b/data/schemas/projects.schema.json new file mode 100644 index 0000000..e6eb155 --- /dev/null +++ b/data/schemas/projects.schema.json @@ -0,0 +1,47 @@ +{ + "$id": "https://scuroneko.dev/schemas/projects.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "items": { + "additionalProperties": false, + "properties": { + "description": { + "minLength": 1, + "type": "string" + }, + "key": { + "minLength": 1, + "type": "string" + }, + "links": { + "additionalProperties": { + "format": "uri", + "type": "string" + }, + "type": "object" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "stack": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + "status": { + "enum": ["active", "archive", "wip", "paused"], + "type": "string" + }, + "tone": { + "$ref": "./shared.schema.json#/$defs/tone" + } + }, + "required": ["key", "name", "description", "stack", "status", "tone"], + "type": "object" + }, + "title": "Projects", + "type": "array" +} diff --git a/data/schemas/shared.schema.json b/data/schemas/shared.schema.json new file mode 100644 index 0000000..13d12c0 --- /dev/null +++ b/data/schemas/shared.schema.json @@ -0,0 +1,23 @@ +{ + "$defs": { + "tone": { + "enum": [ + "blue", + "gold", + "mint", + "purple", + "red", + "cyan", + "green", + "pink", + "teal", + "lime", + "rose" + ], + "type": "string" + } + }, + "$id": "https://scuroneko.dev/schemas/shared.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Shared schema definitions" +} diff --git a/data/schemas/stack.schema.json b/data/schemas/stack.schema.json new file mode 100644 index 0000000..aff5077 --- /dev/null +++ b/data/schemas/stack.schema.json @@ -0,0 +1,39 @@ +{ + "$id": "https://scuroneko.dev/schemas/stack.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "items": { + "additionalProperties": false, + "properties": { + "hide": { + "type": "boolean" + }, + "key": { + "minLength": 1, + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "tags": { + "items": { + "minLength": 1, + "type": "string" + }, + "type": "array", + "uniqueItems": true + }, + "tone": { + "$ref": "./shared.schema.json#/$defs/tone" + }, + "years": { + "minimum": 0, + "type": "number" + } + }, + "required": ["key", "name", "tone", "years"], + "type": "object" + }, + "title": "Stack", + "type": "array" +} diff --git a/data/schemas/workstations.schema.json b/data/schemas/workstations.schema.json new file mode 100644 index 0000000..2a0fb4b --- /dev/null +++ b/data/schemas/workstations.schema.json @@ -0,0 +1,87 @@ +{ + "$id": "https://scuroneko.dev/schemas/workstations.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "items": { + "additionalProperties": false, + "properties": { + "actual": { + "type": "boolean" + }, + "devices": { + "items": { + "additionalProperties": false, + "properties": { + "key": { + "minLength": 1, + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + } + }, + "required": ["key", "name"], + "type": "object" + }, + "type": "array" + }, + "key": { + "minLength": 1, + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "specs": { + "properties": { + "cpu": { + "minLength": 1, + "type": "string" + }, + "gpu": { + "minLength": 1, + "type": "string" + }, + "os": { + "type": "string" + }, + "ram": { + "minLength": 1, + "type": "string" + }, + "storage": { + "items": { + "additionalProperties": false, + "properties": { + "capacity": { + "minLength": 1, + "type": "string" + }, + "name": { + "minLength": 1, + "type": "string" + }, + "type": { + "minLength": 1, + "type": "string" + } + }, + "required": ["name", "capacity"], + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "tone": { + "$ref": "./shared.schema.json#/$defs/tone" + } + }, + "required": ["key", "name", "tone"], + "type": "object" + }, + "title": "Workstations", + "type": "array" +} diff --git a/data/stack.json b/data/stack.json index c5f1fa6..f1e220d 100644 --- a/data/stack.json +++ b/data/stack.json @@ -45,6 +45,20 @@ "tone": "blue", "years": 2 }, + { + "key": "jetbrains", + "name": "JetBrains IDE", + "tags": ["tools"], + "tone": "purple", + "years": 10 + }, + { + "key": "vscode", + "name": "Visual Studio Code", + "tags": ["tools"], + "tone": "purple", + "years": 7 + }, { "hide": true, "key": "html", @@ -65,5 +79,12 @@ "name": "JavaScript", "tone": "gold", "years": 7 + }, + { + "hide": true, + "key": "sass", + "name": "Sass", + "tone": "rose", + "years": 5 } ] diff --git a/data/workstations.json b/data/workstations.json new file mode 100644 index 0000000..7702acc --- /dev/null +++ b/data/workstations.json @@ -0,0 +1,110 @@ +[ + { + "actual": true, + "devices": [ + { + "key": "monitor1", + "name": "AOC 24G2SPU" + }, + { + "key": "monitor2", + "name": "AOC 24E1Q" + }, + { + "key": "keyboard", + "name": "Ardor Wakizashi" + }, + { + "key": "mouse", + "name": "Ardor Immortaliry PRO" + }, + { + "key": "microphone", + "name": "Fifine AM8" + } + ], + "key": "pc", + "name": "PC", + "specs": { + "cpu": "AMD Ryzen 7 7700", + "gpu": "NVIDIA GeForce RTX 5070", + "os": "Windows 11 Pro", + "ram": "32GB DDR5", + "storage": [ + { + "capacity": "1TB", + "name": "Adata XPG GAMMIX S11 Pro", + "type": "M2 PCI-E 3.0" + }, + { + "capacity": "1TB", + "name": "WD Blue", + "type": "SATA HDD" + } + ] + }, + "tone": "rose" + }, + { + "actual": true, + "key": "notebook", + "name": "Notebook", + "specs": { + "cpu": "Intel Core i3-8470U", + "gpu": "Intel UHD Graphics 620", + "os": "Windows 10 LTSC 2021", + "ram": "16GB DDR4", + "storage": [ + { + "capacity": "256GB", + "name": "WD Blue SN550", + "type": "M2 PCI-E 3.0" + }, + { + "capacity": "256GB", + "name": "Neo Forza NFS12", + "type": "SATA SSD" + } + ] + }, + "tone": "blue" + }, + { + "actual": true, + "key": "vps", + "name": "VPS", + "specs": { + "cpu": "2 vCore (AMD EPYC 7763)", + "gpu": "N/A", + "os": "Arch Linux", + "ram": "4GB", + "storage": [ + { + "capacity": "60GB", + "name": "NVMe SSD", + "type": "NVMe" + } + ] + }, + "tone": "green" + }, + { + "actual": false, + "key": "pc", + "name": "PC", + "specs": { + "cpu": "Intel Core i7-6700", + "gpu": "AMD Radeon RX580", + "os": "Windows 11 Pro", + "ram": "16GB DDR4", + "storage": [ + { + "capacity": "256GB", + "name": "Samsung 850 EVO", + "type": "SATA SSD" + } + ] + }, + "tone": "teal" + } +] diff --git a/favicon.png b/icons/favicon-512.png similarity index 100% rename from favicon.png rename to icons/favicon-512.png diff --git a/icons/css.svg b/img/icons/css.svg similarity index 100% rename from icons/css.svg rename to img/icons/css.svg diff --git a/icons/gh.svg b/img/icons/gh.svg similarity index 100% rename from icons/gh.svg rename to img/icons/gh.svg diff --git a/icons/git.svg b/img/icons/git.svg similarity index 100% rename from icons/git.svg rename to img/icons/git.svg diff --git a/icons/go.svg b/img/icons/go.svg similarity index 100% rename from icons/go.svg rename to img/icons/go.svg diff --git a/icons/html.svg b/img/icons/html.svg similarity index 100% rename from icons/html.svg rename to img/icons/html.svg diff --git a/img/icons/jetbrains.svg b/img/icons/jetbrains.svg new file mode 100644 index 0000000..2f669ff --- /dev/null +++ b/img/icons/jetbrains.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/js.svg b/img/icons/js.svg similarity index 100% rename from icons/js.svg rename to img/icons/js.svg diff --git a/icons/mdb.svg b/img/icons/mdb.svg similarity index 100% rename from icons/mdb.svg rename to img/icons/mdb.svg diff --git a/icons/psql.svg b/img/icons/psql.svg similarity index 100% rename from icons/psql.svg rename to img/icons/psql.svg diff --git a/icons/py.svg b/img/icons/py.svg similarity index 100% rename from icons/py.svg rename to img/icons/py.svg diff --git a/icons/re.svg b/img/icons/re.svg similarity index 100% rename from icons/re.svg rename to img/icons/re.svg diff --git a/img/icons/sass.svg b/img/icons/sass.svg new file mode 100644 index 0000000..1a3eea2 --- /dev/null +++ b/img/icons/sass.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/tg.svg b/img/icons/tg.svg similarity index 100% rename from icons/tg.svg rename to img/icons/tg.svg diff --git a/icons/ts.svg b/img/icons/ts.svg similarity index 100% rename from icons/ts.svg rename to img/icons/ts.svg diff --git a/img/icons/vscode.svg b/img/icons/vscode.svg new file mode 100644 index 0000000..cf03825 --- /dev/null +++ b/img/icons/vscode.svg @@ -0,0 +1,4 @@ + diff --git a/index.html b/index.html index ee9c225..09db850 100644 --- a/index.html +++ b/index.html @@ -3,11 +3,50 @@
+ + + + + + + + + + + + + + + + + + + + + + - + + +Tools I reach for when the task needs to ship.
+ @@ -63,9 +108,26 @@ -Machines, peripherals, and daily drivers.
+ + +diff --git a/js/data.js b/js/data.js index 1da6ea2..fa4a8ca 100644 --- a/js/data.js +++ b/js/data.js @@ -1,5 +1,6 @@ let stackList = []; let projectsList = []; +let workstationsList = []; let linksList = []; const fetchJson = async (path) => { @@ -13,7 +14,7 @@ const fetchJson = async (path) => { }; const loadIcon = async (key) => { - const response = await fetch(`./icons/${key}.svg`); + const response = await fetch(`./img/icons/${key}.svg`); if (!response.ok) { return key; @@ -25,5 +26,9 @@ const loadIcon = async (key) => { const formatIndex = (index) => String(index + 1).padStart(2, "0"); document.addEventListener("DOMContentLoaded", () => { - loadStack().then(loadProjects).then(loadLinks).catch(console.error); + loadStack() + .then(loadProjects) + .then(loadWorkstations) + .then(loadLinks) + .catch(console.error); }); diff --git a/js/projects.js b/js/projects.js index 7ef69b8..4f0239c 100644 --- a/js/projects.js +++ b/js/projects.js @@ -21,7 +21,7 @@ const loadProjects = async () => { const buildProjectCard = (project, index) => { const card = document.createElement("article"); - card.classList.add("project-card", `project-card--${project.tone ?? "mint"}`); + card.classList.add("project-card", `tone-${project.tone ?? "mint"}`); const header = document.createElement("div"); header.classList.add("project-card__header"); diff --git a/js/stack.js b/js/stack.js index 8ee4901..e91398b 100644 --- a/js/stack.js +++ b/js/stack.js @@ -1,3 +1,5 @@ +let isStackExpanded = false; + const getExperienceLevel = (years) => { if (years >= 7) return "Core"; if (years >= 4) return "Strong"; @@ -17,21 +19,34 @@ const loadStackForProject = (keys) => { const loadStack = async () => { const stackContainer = document.getElementById("stack"); + const stackToggle = document.getElementById("stack-toggle"); if (!stackContainer) { return; } stackList = await fetchJson("./data/stack.json"); - const stackCards = stackList.map(buildStackCard).filter(Boolean); + renderStack(stackContainer); + + if (stackToggle && stackList.some((stack) => stack.hide === true)) { + stackToggle.hidden = false; + stackToggle.addEventListener("click", () => { + isStackExpanded = !isStackExpanded; + stackToggle.textContent = isStackExpanded ? "Show less" : "View all"; + stackToggle.setAttribute("aria-expanded", String(isStackExpanded)); + renderStack(stackContainer); + }); + } +}; + +const renderStack = (stackContainer) => { + const stackCards = stackList + .filter((stack) => isStackExpanded || stack.hide !== true) + .map(buildStackCard); 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"}`); diff --git a/js/workstations.js b/js/workstations.js new file mode 100644 index 0000000..59b9fff --- /dev/null +++ b/js/workstations.js @@ -0,0 +1,144 @@ +let areWorkstationsExpanded = false; + +const loadWorkstations = async () => { + const workstationContainer = document.getElementById("workstations"); + const workstationToggle = document.getElementById("workstation-toggle"); + + if (!workstationContainer) { + return; + } + + workstationsList = await fetchJson("./data/workstations.json"); + renderWorkstations(workstationContainer); + + if ( + workstationToggle && + workstationsList.some((workstation) => workstation.actual === false) + ) { + workstationToggle.hidden = false; + workstationToggle.addEventListener("click", () => { + areWorkstationsExpanded = !areWorkstationsExpanded; + workstationToggle.textContent = areWorkstationsExpanded + ? "Show less" + : "View all"; + workstationToggle.setAttribute( + "aria-expanded", + String(areWorkstationsExpanded), + ); + renderWorkstations(workstationContainer); + }); + } +}; + +const renderWorkstations = (workstationContainer) => { + const workstationCards = workstationsList + .filter( + (workstation) => areWorkstationsExpanded || workstation.actual !== false, + ) + .map(buildWorkstationCard); + workstationContainer.replaceChildren(...workstationCards); +}; + +const buildWorkstationCard = (workstation, index) => { + const card = document.createElement("article"); + card.classList.add("workstation-card", `tone-${workstation.tone ?? "mint"}`); + + const header = document.createElement("div"); + header.classList.add("workstation-card__header"); + + const indexBadge = document.createElement("span"); + indexBadge.classList.add("workstation-card__index"); + indexBadge.textContent = formatIndex(index); + + const key = document.createElement("span"); + key.classList.add("workstation-card__key"); + key.textContent = workstation.key; + + const status = document.createElement("span"); + status.classList.add( + "workstation-card__status", + workstation.actual + ? "workstation-card__status--actual" + : "workstation-card__status--archive", + ); + status.textContent = workstation.actual ? "active" : "archive"; + + const title = document.createElement("h3"); + title.classList.add("workstation-card__title"); + title.textContent = workstation.name; + + const specs = document.createElement("dl"); + specs.classList.add("workstation-card__specs"); + + appendSpec(specs, "CPU", workstation.specs?.cpu); + appendSpec(specs, "GPU", workstation.specs?.gpu); + appendSpec(specs, "RAM", workstation.specs?.ram); + appendSpec(specs, "OS", workstation.specs?.os); + + const storage = buildChipGroup( + "Storage", + workstation.specs?.storage?.map(formatStorage) ?? [], + ); + const devices = buildChipGroup( + "Devices", + workstation.devices?.map((device) => device.name) ?? [], + ); + + header.append(indexBadge, key, status); + card.append(header, title, specs); + + if (storage) { + card.append(storage); + } + + if (devices) { + card.append(devices); + } + + return card; +}; + +const appendSpec = (list, label, value) => { + if (!value) { + return; + } + + const term = document.createElement("dt"); + term.textContent = label; + + const description = document.createElement("dd"); + description.textContent = value; + + list.append(term, description); +}; + +const buildChipGroup = (label, items) => { + if (items.length === 0) { + return null; + } + + const group = document.createElement("div"); + group.classList.add("workstation-card__group"); + + const title = document.createElement("p"); + title.classList.add("workstation-card__group-title"); + title.textContent = label; + + const chips = document.createElement("div"); + chips.classList.add("workstation-card__chips"); + chips.append( + ...items.map((item) => { + const chip = document.createElement("span"); + chip.textContent = item; + return chip; + }), + ); + + group.append(title, chips); + return group; +}; + +const formatStorage = (storage) => { + const parts = [storage.capacity, storage.name, storage.type].filter(Boolean); + return parts.join(" ยท "); +}; diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..cd2bb06 --- /dev/null +++ b/manifest.json @@ -0,0 +1,17 @@ +{ + "background_color": "#1f1f28", + "description": "Go and JavaScript developer building self-hosted tools, small web systems, and developer-focused projects.", + "display": "standalone", + "icons": [ + { + "sizes": "512x512", + "src": "icons/favicon-512.png", + "type": "image/png" + } + ], + "name": "ScuroNeko", + "scope": "/", + "short_name": "ScuroNeko", + "start_url": "/", + "theme_color": "#1f1f28" +} diff --git a/styles/links.css b/styles/links.css index 4538e60..acbf69f 100644 --- a/styles/links.css +++ b/styles/links.css @@ -27,7 +27,7 @@ border-color: var(--card-accent); background-color: color-mix(in srgb, var(--card-glow) 36%, var(--surface)); color: var(--text); - transform: translateY(-2px); + transform: translateY(-5px); } .link-card__index { @@ -64,8 +64,9 @@ .link-card__name { color: var(--text); font-family: "Barlow Condensed", sans-serif; - font-size: 1.25rem; font-weight: 700; + font-size: 1.25rem; + line-height: 1; } .link-card__label { diff --git a/styles/main.css b/styles/main.css index 5b38160..8b31d31 100644 --- a/styles/main.css +++ b/styles/main.css @@ -19,6 +19,9 @@ --glow-cyan: rgba(127, 180, 202, 0.2); --glow-green: rgba(152, 187, 108, 0.18); --glow-pink: rgba(210, 126, 153, 0.18); + --glow-teal: rgba(107, 196, 180, 0.18); + --glow-lime: rgba(184, 204, 100, 0.18); + --glow-rose: rgba(224, 112, 128, 0.2); --block-size: 980px; @@ -64,15 +67,18 @@ a:hover { } h1 { - font-size: 3.5rem; + font-size: clamp(3.5rem, 7vw, 5rem); + line-height: 0.95; } h2 { - font-size: 2rem; + font-size: clamp(1.85rem, 3vw, 2.25rem); + line-height: 1; } h3 { font-size: 1.5rem; + line-height: 1.1; } h4 { @@ -124,46 +130,6 @@ h6 { 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; diff --git a/styles/projects.css b/styles/projects.css index b2b3954..edbe3bc 100644 --- a/styles/projects.css +++ b/styles/projects.css @@ -1,43 +1,3 @@ -.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; @@ -46,16 +6,14 @@ } .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: 1px solid color-mix(in srgb, var(--card-accent) 36%, var(--border)); border-radius: 8px; background: - linear-gradient(140deg, var(--project-glow), transparent 48%), + linear-gradient(140deg, var(--card-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); @@ -66,9 +24,9 @@ } .project-card:hover { - border-color: var(--project-accent); + border-color: var(--card-accent); box-shadow: 0 24px 52px rgba(0, 0, 0, 0.28); - transform: translateY(-2px); + transform: translateY(-5px); } .project-card__header { @@ -92,8 +50,8 @@ } .project-card__key { - border: 1px solid color-mix(in srgb, var(--project-accent) 56%, var(--border)); - color: var(--project-accent); + border: 1px solid color-mix(in srgb, var(--card-accent) 56%, var(--border)); + color: var(--card-accent); text-transform: uppercase; } @@ -107,7 +65,8 @@ .project-card__title { margin-top: 22px; color: var(--text); - font-size: 1.75rem; + font-size: clamp(1.6rem, 2.4vw, 1.9rem); + line-height: 1; } .project-card__description { @@ -131,6 +90,7 @@ color: var(--card-accent); font-size: 0.9rem; font-family: var(--fonts-monospace); + font-style: italic; } .project-card__links { @@ -144,7 +104,7 @@ .project-card__links a { border-bottom: 1px solid currentColor; - color: var(--project-accent); + color: var(--card-accent); font-weight: 700; text-decoration: none; } diff --git a/styles/stack.css b/styles/stack.css index 3c54bf5..5cec76a 100644 --- a/styles/stack.css +++ b/styles/stack.css @@ -5,6 +5,32 @@ margin-top: 18px; } +.stack-toggle { + margin-top: 14px; + min-height: 38px; + padding: 8px 16px; + border: 1px solid var(--border); + border-radius: 8px; + background-color: transparent; + color: var(--text); + cursor: pointer; + font: inherit; + transition: + background-color 160ms ease, + border-color 160ms ease, + color 160ms ease; +} + +.stack-toggle:hover { + border-color: var(--accent); + background-color: var(--glow-mint); + color: var(--text); +} + +.stack-toggle[hidden] { + display: none; +} + .stack-card { position: relative; display: grid; @@ -30,7 +56,7 @@ .stack-card:hover { border-color: var(--card-accent); box-shadow: 0 20px 44px rgba(0, 0, 0, 0.26); - transform: translateY(-2px); + transform: translateY(-5px); } .stack-card__badge { @@ -70,6 +96,8 @@ .stack-card__title { margin-top: 4px; color: var(--text); + font-size: 1.5rem; + line-height: 1; } .stack-card__footer { @@ -88,6 +116,7 @@ border: 1px solid rgba(220, 215, 186, 0.16); padding: 4px 8px; background-color: rgba(31, 31, 40, 0.4); + font-style: italic; } @media (max-width: 780px) { diff --git a/styles/theme-light.css b/styles/theme-light.css new file mode 100644 index 0000000..4981c9e --- /dev/null +++ b/styles/theme-light.css @@ -0,0 +1,107 @@ +@media (prefers-color-scheme: light) { + :root { + --bg: #f2ecbc; + --surface: #e5ddb0; + --border: #c8c093; + + --text: #545464; + --text-soft: #43436c; + --text-muted: #77713f; + + --primary: #2f5d95; + --accent: #4e8ca2; + --warm: #cc6d00; + --danger: #c84053; + --glow-blue: rgba(47, 93, 149, 0.14); + --glow-gold: rgba(204, 109, 0, 0.13); + --glow-mint: rgba(78, 140, 162, 0.12); + --glow-purple: rgba(118, 107, 144, 0.14); + --glow-red: rgba(200, 64, 83, 0.13); + --glow-cyan: rgba(58, 143, 183, 0.12); + --glow-green: rgba(111, 137, 78, 0.13); + --glow-pink: rgba(179, 91, 121, 0.12); + --glow-teal: rgba(90, 119, 133, 0.12); + --glow-lime: rgba(119, 113, 63, 0.12); + --glow-rose: rgba(215, 71, 75, 0.13); + } + + body { + color-scheme: light; + } + + .tone-purple { + --card-accent: #766b90; + } + + .tone-cyan { + --card-accent: #3a8fb7; + } + + .tone-green { + --card-accent: #6f894e; + } + + .tone-pink { + --card-accent: #b35b79; + } + + .tone-teal { + --card-accent: #5a7785; + } + + .tone-lime { + --card-accent: #77713f; + } + + .tone-rose { + --card-accent: #d7474b; + } + + .stack-card, + .project-card, + .workstation-card { + background: + linear-gradient(135deg, var(--card-glow), transparent 46%), + linear-gradient(180deg, rgba(247, 241, 197, 0.52), transparent), + var(--surface); + box-shadow: 0 16px 36px rgba(67, 67, 108, 0.1); + } + + .stack-card:hover, + .project-card:hover, + .workstation-card:hover { + box-shadow: 0 22px 48px rgba(67, 67, 108, 0.14); + } + + .stack-card__badge, + .stack-card__footer span, + .project-card__status, + .project-card__stack span, + .workstation-card__status, + .workstation-card__specs dt, + .workstation-card__chips span, + .link-card { + background-color: rgba(242, 236, 188, 0.58); + border-color: color-mix(in srgb, var(--card-accent) 28%, var(--border)); + } + .workstation-card__status--actual { + background-color: var(--card-accent); + color: var(--surface); + } + .workstation-card__status--archive { + background-color: rgba(31, 31, 40, 0.52); + color: var(--surface); + font-style: italic; + } + + .link-card:hover { + background-color: color-mix(in srgb, var(--card-glow) 46%, var(--surface)); + } + + .site-pill, + .site-nav a, + .stack-toggle, + .workstation-toggle { + background-color: rgba(242, 236, 188, 0.42); + } +} diff --git a/styles/tones.css b/styles/tones.css new file mode 100644 index 0000000..26d124f --- /dev/null +++ b/styles/tones.css @@ -0,0 +1,54 @@ +.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); +} + +.tone-teal { + --card-accent: #6bc4b4; + --card-glow: var(--glow-teal); +} + +.tone-lime { + --card-accent: #b8cc64; + --card-glow: var(--glow-lime); +} + +.tone-rose { + --card-accent: #e07080; + --card-glow: var(--glow-rose); +} diff --git a/styles/workstations.css b/styles/workstations.css new file mode 100644 index 0000000..6cd2558 --- /dev/null +++ b/styles/workstations.css @@ -0,0 +1,180 @@ +.workstation-grid { + display: grid; + gap: 18px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin-top: 18px; +} + +.workstation-toggle { + margin-top: 14px; + min-height: 38px; + padding: 8px 16px; + border: 1px solid var(--border); + border-radius: 8px; + background-color: transparent; + color: var(--text); + cursor: pointer; + font: inherit; + transition: + background-color 160ms ease, + border-color 160ms ease, + color 160ms ease; +} + +.workstation-toggle:hover { + border-color: var(--accent); + background-color: var(--glow-mint); + color: var(--text); +} + +.workstation-toggle[hidden] { + display: none; +} + +.workstation-card { + display: flex; + flex-direction: column; + min-height: 260px; + padding: 22px; + border: 1px solid color-mix(in srgb, var(--card-accent) 36%, var(--border)); + border-radius: 8px; + background: + linear-gradient(140deg, var(--card-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; +} + +.workstation-card:hover { + border-color: var(--card-accent); + box-shadow: 0 24px 52px rgba(0, 0, 0, 0.28); + transform: translateY(-5px); +} + +.workstation-card__header { + display: flex; + align-items: center; + gap: 12px; +} + +.workstation-card__index { + color: var(--text-soft); + font-family: var(--fonts-monospace); + font-size: 0.82rem; +} + +.workstation-card__key, +.workstation-card__status { + border-radius: 8px; + padding: 4px 8px; + font-family: var(--fonts-monospace); + font-size: 0.82rem; + font-weight: 700; +} + +.workstation-card__key { + border: 1px solid color-mix(in srgb, var(--card-accent) 56%, var(--border)); + color: var(--card-accent); + text-transform: uppercase; +} + +.workstation-card__status { + margin-left: auto; + background-color: rgba(31, 31, 40, 0.52); + color: var(--text); +} +.workstation-card__status--actual { + background-color: var(--card-accent); + color: var(--surface); +} +.workstation-card__status--archive { + background-color: rgba(31, 31, 40, 0.52); + color: var(--text); + font-style: italic; +} + +.workstation-card__title { + margin-top: 22px; + color: var(--text); + font-size: clamp(1.6rem, 2.4vw, 1.9rem); + line-height: 1; +} + +.workstation-card__specs { + display: grid; + grid-template-columns: 68px minmax(0, 1fr); + gap: 8px 12px; + margin-top: 18px; + font-family: var(--fonts-monospace); +} + +.workstation-card__specs dt { + align-self: start; + border: 1px solid color-mix(in srgb, var(--card-accent) 48%, var(--border)); + border-radius: 8px; + padding: 4px 7px; + background-color: rgba(31, 31, 40, 0.36); + color: var(--card-accent); + font-size: 0.72rem; + font-weight: 700; + line-height: 1; + text-align: center; + text-transform: uppercase; +} + +.workstation-card__specs dd { + align-self: center; + min-width: 0; + color: var(--text); + font-weight: 400; + line-height: 1.4; +} + +.workstation-card__group { + margin-top: 20px; +} + +.workstation-card__group-title { + color: var(--card-accent); + font-family: var(--fonts-monospace); + font-size: 0.78rem; + font-weight: 700; + text-transform: uppercase; +} + +.workstation-card__chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 8px; +} + +.workstation-card__chips 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-family: var(--fonts-monospace); + font-size: 0.9rem; +} + +@media (max-width: 780px) { + .workstation-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 560px) { + .workstation-card__specs { + grid-template-columns: 58px minmax(0, 1fr); + } + + .workstation-card__specs dd { + margin-bottom: 0; + } +}