Clove Twilight
+(fae/faer)
+Tech Stack
+diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..9af8d46 --- /dev/null +++ b/css/style.css @@ -0,0 +1,429 @@ +/* Comic Code Import*/ +@font-face { + font-family: 'Comic Code'; + src: url('https://fonts.doughmination.co.uk/ComicCode-Regular_2022-05-24-151938_hsmz.woff2') format('woff2'), + url('https://fonts.doughmination.co.uk/ComicCode-Regular_2022-05-24-151938_hsmz.woff') format('woff'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Comic Code'; + src: url('https://fonts.doughmination.co.uk/ComicCode-Italic_2022-05-24-151939_rdtu.woff2') format('woff2'), + url('https://fonts.doughmination.co.uk/ComicCode-Italic_2022-05-24-151939_rdtu.woff') format('woff'); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: 'Comic Code'; + src: url('https://fonts.doughmination.co.uk/ComicCode-Medium_2022-05-24-151941_ugqm.woff2') format('woff2'), + url('https://fonts.doughmination.co.uk/ComicCode-Medium_2022-05-24-151941_ugqm.woff') format('woff'); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: 'Comic Code'; + src: url('https://fonts.doughmination.co.uk/ComicCode-Bold_2022-05-24-152309_zqkm.woff2') format('woff2'), + url('https://fonts.doughmination.co.uk/ComicCode-Bold_2022-05-24-152309_zqkm.woff') format('woff'); + font-weight: 700; + font-style: normal; +} + +:root { + /* https://catppuccin.com/palette/ */ + /* Catppuccin Mocha */ + /* accents */ + --rosewater: #f5e0dc; + --flamingo: #f2cdcd; + --pink: #f5c2e7; + --mauve: #cba6f7; + --red: #f38ba8; + --maroon: #eba0ac; + --peach: #fab387; + --yellow: #f9e2af; + --green: #a6e3a1; + --teal: #94e2d5; + --sky: #89dceb; + --saphire: #74c7ec; + --blue: #89b4fa; + /* a lovely new blue passport */ + --lavender: #b4befe; + /* Text */ + --text: #cdd6f4; + --subtext-0: #a6adc8; + --subtext-1: #bac2de; + --overlay-0: #6c7086; + --overlay-1: #7f849c; + --overlay-2: #9399b2; + --surface-0: #313244; + --surface-1: #45475a; + --surface-2: #585b70; + /* Backgrounds */ + --base: #1e1e2e; + --mantle: #181825; + --crust: #11111b; + /* Is this the crusty crab? */ +} + +* { + box-sizing: border-box; +} + +/* Smooth cross-fade between pages (Link Center <-> Tech Stack) */ +@view-transition { + navigation: auto; +} + +::view-transition-old(root), +::view-transition-new(root) { + animation-duration: 0.35s; + animation-timing-function: ease; +} + +@media (prefers-reduced-motion: reduce) { + ::view-transition-old(root), + ::view-transition-new(root) { + animation: none; + } +} + +html, +body { + height: 100%; + overflow: hidden; +} + +body { + font-family: 'Comic Code', sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + height: 100dvh; + margin: 0; + padding: 1.5rem 1rem; + background: linear-gradient(135deg, var(--base) 0%, var(--mantle) 60%, var(--crust) 100%); + color: var(--text); +} + +/* Estrogen watermark blended into the background */ +body::before { + content: ""; + position: fixed; + inset: 0; + background: url(/images/estrogen.svg) center / cover no-repeat; + filter: invert(86%) sepia(8%) saturate(900%) hue-rotate(190deg) brightness(105%); + opacity: 0.05; + pointer-events: none; + z-index: 0; +} + +.hub { + position: relative; + z-index: 1; + width: 100%; + max-width: 460px; +} + +.pfp { + width: 96px; + height: 96px; + border-radius: 50%; + object-fit: cover; + border: 3px solid var(--yellow); + box-shadow: 0 4px 18px rgba(245, 194, 231, 0.25); + margin-bottom: 0.75rem; +} + +.hub-header { + text-align: center; + margin-bottom: 2.25rem; +} + +.hub-header h1 { + margin: 0; + font-size: 2rem; + font-weight: 700; + color: var(--pink); +} + +.tagline { + margin: 0.35rem 0 0; + color: var(--subtext-0); + font-size: 0.95rem; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.pronouns { + margin: 0.35rem 0 0; + color: var(--mauve); + font-size: 0.95rem; + letter-spacing: 0.04em; + text-transform: lowercase; +} + +.links { + display: grid; + grid-template-columns: repeat(auto-fit, 72px); + gap: 0.9rem; + justify-content: center; +} + +.link-card { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 72px; + height: 72px; + border-radius: 16px; + background: var(--surface-0); + border: 1px solid var(--surface-1); + color: var(--text); + text-decoration: none; + transition: transform 0.15s ease, border-color 0.15s ease, + background 0.15s ease, box-shadow 0.15s ease; +} + +.link-card:hover { + transform: translateY(-3px); + background: var(--surface-1); + border-color: var(--pink); + box-shadow: 0 6px 20px rgba(245, 194, 231, 0.22); +} + +.icon { + width: 30px; + height: 30px; + flex-shrink: 0; + filter: invert(86%) sepia(8%) saturate(900%) hue-rotate(190deg) brightness(105%); + transition: filter 0.15s ease; +} + +.link-card:hover .icon { + filter: invert(78%) sepia(36%) saturate(640%) hue-rotate(280deg) brightness(105%); +} + +/* git.gay: white by default, true pride colors on hover */ +.icon.raw { + filter: brightness(0) invert(1); +} + +.link-card:hover .icon.raw { + filter: none; +} + +/* Details revealed on hover as a tooltip */ +.link-text { + position: absolute; + bottom: calc(100% + 10px); + left: 50%; + transform: translateX(-50%) translateY(4px); + display: flex; + flex-direction: column; + align-items: center; + gap: 0.1rem; + padding: 0.5rem 0.75rem; + border-radius: 10px; + background: var(--crust); + border: 1px solid var(--pink); + box-shadow: 0 6px 18px rgba(17, 17, 27, 0.55); + white-space: nowrap; + line-height: 1.3; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease, transform 0.15s ease; + z-index: 10; +} + +/* Tooltip arrow */ +.link-text::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 6px solid transparent; + border-top-color: var(--pink); +} + +.link-card:hover .link-text, +.link-card:focus-visible .link-text { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +.link-title { + font-weight: 500; + font-size: 0.95rem; + color: var(--text); +} + +.link-sub { + font-size: 0.78rem; + color: var(--subtext-0); +} + +.link-card.arch .link-title { + color: var(--pink); +} + +/* Page nav, pinned bottom-left, selected item with a pointer triangle */ +.nav { + position: fixed; + left: 1rem; + bottom: 1rem; + z-index: 6; +} + +.nav-links { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.4rem; +} + +.nav-link { + position: relative; + display: inline-flex; + align-items: center; + padding: 0.3rem 0.7rem; + border-radius: 999px; + background: var(--surface-0); + border: 1px solid var(--surface-1); + color: var(--subtext-1); + font-size: 0.8rem; + text-decoration: none; + transition: transform 0.15s ease, border-color 0.15s ease, + background 0.15s ease, color 0.15s ease; +} + +.nav-link:hover { + border-color: var(--pink); + color: var(--text); + transform: translateX(2px); +} + +.nav-link.selected { + background: var(--pink); + border-color: var(--pink); + color: var(--crust); + font-weight: 700; + margin-left: 14px; +} + +/* Rotated triangle pointing at the selected item */ +.nav-link.selected::before { + content: ""; + position: absolute; + left: -14px; + top: 50%; + transform: translateY(-50%); + border: 6px solid transparent; + border-left-color: var(--pink); +} + +/* Cosmetic system badges, pinned bottom-right */ +.badges { + position: fixed; + right: 1rem; + bottom: 1rem; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.4rem; + pointer-events: none; + z-index: 5; +} + +.badge { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.3rem 0.6rem; + border-radius: 999px; + background: var(--surface-0); + border: 1px solid var(--surface-1); + color: var(--subtext-1); + font-size: 0.75rem; + white-space: nowrap; +} + +.badge-icon { + width: 15px; + height: 15px; + filter: invert(78%) sepia(36%) saturate(640%) hue-rotate(280deg) brightness(105%); +} + +/* Shrink to fit narrow / short screens so the hub never scrolls */ +@media (max-width: 420px), +(max-height: 640px) { + .hub-header { + margin-bottom: 1.25rem; + } + + .hub-header h1 { + font-size: 1.6rem; + } + + .pfp { + width: 72px; + height: 72px; + margin-bottom: 0.5rem; + } + + .links { + grid-template-columns: repeat(auto-fit, 60px); + gap: 0.6rem; + } + + .link-card { + width: 60px; + height: 60px; + border-radius: 13px; + } + + .icon { + width: 26px; + height: 26px; + } +} +/* ===== Tech stack page ===== */ +/* Let only the tech-stack page scroll; link hub stays locked */ +html:has(.tech-stack), +body:has(.tech-stack) { + height: auto; + min-height: 100dvh; + overflow-y: auto; +} + +body:has(.tech-stack) { + align-items: flex-start; +} + +body:has(.tech-stack) .hub { + max-width: 860px; +} + +.tech-stack { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; + margin: 0 auto; + padding-bottom: 4.5rem; +} + +.tech-badge { + height: 28px; + border-radius: 5px; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.tech-badge:hover { + transform: translateY(-2px) scale(1.04); + box-shadow: 0 4px 12px rgba(245, 194, 231, 0.25); +} diff --git a/images/amd.svg b/images/amd.svg new file mode 100644 index 0000000..ef0757c --- /dev/null +++ b/images/amd.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/images/apple.svg b/images/apple.svg new file mode 100644 index 0000000..58235c3 --- /dev/null +++ b/images/apple.svg @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/images/arch.svg b/images/arch.svg new file mode 100644 index 0000000..c622c5c --- /dev/null +++ b/images/arch.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/images/bluesky.svg b/images/bluesky.svg new file mode 100644 index 0000000..a070aea --- /dev/null +++ b/images/bluesky.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/images/debian.svg b/images/debian.svg new file mode 100644 index 0000000..55b6dd4 --- /dev/null +++ b/images/debian.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/images/discord.svg b/images/discord.svg new file mode 100644 index 0000000..9905364 --- /dev/null +++ b/images/discord.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/images/email.svg b/images/email.svg new file mode 100644 index 0000000..966ef94 --- /dev/null +++ b/images/email.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/images/estrogen.svg b/images/estrogen.svg new file mode 100644 index 0000000..71c5d51 --- /dev/null +++ b/images/estrogen.svg @@ -0,0 +1,55 @@ + + \ No newline at end of file diff --git a/images/favicon.svg b/images/favicon.svg new file mode 100644 index 0000000..f21a1a9 --- /dev/null +++ b/images/favicon.svg @@ -0,0 +1,20 @@ + diff --git a/images/git-gay.svg b/images/git-gay.svg new file mode 100644 index 0000000..cc12cd9 --- /dev/null +++ b/images/git-gay.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/github.svg b/images/github.svg new file mode 100644 index 0000000..013e025 --- /dev/null +++ b/images/github.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/images/linkedin.svg b/images/linkedin.svg new file mode 100644 index 0000000..30fc0e3 --- /dev/null +++ b/images/linkedin.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/images/mastodon.svg b/images/mastodon.svg new file mode 100644 index 0000000..a8c2a26 --- /dev/null +++ b/images/mastodon.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/images/oneko.gif b/images/oneko.gif new file mode 100644 index 0000000..a009c2c Binary files /dev/null and b/images/oneko.gif differ diff --git a/images/reddit.svg b/images/reddit.svg new file mode 100644 index 0000000..777aead --- /dev/null +++ b/images/reddit.svg @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/images/site.svg b/images/site.svg new file mode 100644 index 0000000..b302063 --- /dev/null +++ b/images/site.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/images/spotify.svg b/images/spotify.svg new file mode 100644 index 0000000..09d0e9f --- /dev/null +++ b/images/spotify.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/images/twitch.svg b/images/twitch.svg new file mode 100644 index 0000000..b2c8ff5 --- /dev/null +++ b/images/twitch.svg @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/images/twitter.svg b/images/twitter.svg new file mode 100644 index 0000000..3001352 --- /dev/null +++ b/images/twitter.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/images/youtube.svg b/images/youtube.svg new file mode 100644 index 0000000..3c9c0be --- /dev/null +++ b/images/youtube.svg @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/js/cat.js b/js/cat.js new file mode 100644 index 0000000..0c4d9c6 --- /dev/null +++ b/js/cat.js @@ -0,0 +1,279 @@ +// oneko.js: https://github.com/adryd325/oneko.js + +(function oneko() { + const isReducedMotion = + window.matchMedia(`(prefers-reduced-motion: reduce)`) === true || + window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true; + + if (isReducedMotion) return; + + const nekoEl = document.createElement("div"); + let persistPosition = true; + + let nekoPosX = 32; + let nekoPosY = 32; + + let mousePosX = 0; + let mousePosY = 0; + + let frameCount = 0; + let idleTime = 0; + let idleAnimation = null; + let idleAnimationFrame = 0; + + const nekoSpeed = 10; + const spriteSets = { + idle: [[-3, -3]], + alert: [[-7, -3]], + scratchSelf: [ + [-5, 0], + [-6, 0], + [-7, 0], + ], + scratchWallN: [ + [0, 0], + [0, -1], + ], + scratchWallS: [ + [-7, -1], + [-6, -2], + ], + scratchWallE: [ + [-2, -2], + [-2, -3], + ], + scratchWallW: [ + [-4, 0], + [-4, -1], + ], + tired: [[-3, -2]], + sleeping: [ + [-2, 0], + [-2, -1], + ], + N: [ + [-1, -2], + [-1, -3], + ], + NE: [ + [0, -2], + [0, -3], + ], + E: [ + [-3, 0], + [-3, -1], + ], + SE: [ + [-5, -1], + [-5, -2], + ], + S: [ + [-6, -3], + [-7, -2], + ], + SW: [ + [-5, -3], + [-6, -1], + ], + W: [ + [-4, -2], + [-4, -3], + ], + NW: [ + [-1, 0], + [-1, -1], + ], + }; + + function init() { + let nekoFile = "./oneko.gif" + const curScript = document.currentScript + if (curScript && curScript.dataset.cat) { + nekoFile = curScript.dataset.cat + } + if (curScript && curScript.dataset.persistPosition) { + if (curScript.dataset.persistPosition === "") { + persistPosition = true; + } else { + persistPosition = JSON.parse(curScript.dataset.persistPosition.toLowerCase()); + } + } + + if (persistPosition) { + let storedNeko = JSON.parse(window.localStorage.getItem("oneko")); + if (storedNeko !== null) { + nekoPosX = storedNeko.nekoPosX; + nekoPosY = storedNeko.nekoPosY; + mousePosX = storedNeko.mousePosX; + mousePosY = storedNeko.mousePosY; + frameCount = storedNeko.frameCount; + idleTime = storedNeko.idleTime; + idleAnimation = storedNeko.idleAnimation; + idleAnimationFrame = storedNeko.idleAnimationFrame; + nekoEl.style.backgroundPosition = storedNeko.bgPos; + } + } + + nekoEl.id = "oneko"; + nekoEl.ariaHidden = true; + nekoEl.style.width = "32px"; + nekoEl.style.height = "32px"; + nekoEl.style.position = "fixed"; + nekoEl.style.pointerEvents = "none"; + nekoEl.style.imageRendering = "pixelated"; + nekoEl.style.left = `${nekoPosX - 16}px`; + nekoEl.style.top = `${nekoPosY - 16}px`; + nekoEl.style.zIndex = 2147483647; + + nekoEl.style.backgroundImage = `url(${nekoFile})`; + + document.body.appendChild(nekoEl); + + document.addEventListener("mousemove", function (event) { + mousePosX = event.clientX; + mousePosY = event.clientY; + }); + + if (persistPosition) { + window.addEventListener("beforeunload", function (event) { + window.localStorage.setItem("oneko", JSON.stringify({ + nekoPosX: nekoPosX, + nekoPosY: nekoPosY, + mousePosX: mousePosX, + mousePosY: mousePosY, + frameCount: frameCount, + idleTime: idleTime, + idleAnimation: idleAnimation, + idleAnimationFrame: idleAnimationFrame, + bgPos: nekoEl.style.backgroundPosition + })); + }); + } + + window.requestAnimationFrame(onAnimationFrame); + } + + let lastFrameTimestamp; + + function onAnimationFrame(timestamp) { + // Stops execution if the neko element is removed from DOM + if (!nekoEl.isConnected) { + return; + } + if (!lastFrameTimestamp) { + lastFrameTimestamp = timestamp; + } + if (timestamp - lastFrameTimestamp > 100) { + lastFrameTimestamp = timestamp; + frame(); + } + window.requestAnimationFrame(onAnimationFrame); + } + + function setSprite(name, frame) { + const sprite = spriteSets[name][frame % spriteSets[name].length]; + nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`; + } + + function resetIdleAnimation() { + idleAnimation = null; + idleAnimationFrame = 0; + } + + function idle() { + idleTime += 1; + + // every ~ 20 seconds + if ( + idleTime > 10 && + Math.floor(Math.random() * 200) == 0 && + idleAnimation == null + ) { + let avalibleIdleAnimations = ["sleeping", "scratchSelf"]; + if (nekoPosX < 32) { + avalibleIdleAnimations.push("scratchWallW"); + } + if (nekoPosY < 32) { + avalibleIdleAnimations.push("scratchWallN"); + } + if (nekoPosX > window.innerWidth - 32) { + avalibleIdleAnimations.push("scratchWallE"); + } + if (nekoPosY > window.innerHeight - 32) { + avalibleIdleAnimations.push("scratchWallS"); + } + idleAnimation = + avalibleIdleAnimations[ + Math.floor(Math.random() * avalibleIdleAnimations.length) + ]; + } + + switch (idleAnimation) { + case "sleeping": + if (idleAnimationFrame < 8) { + setSprite("tired", 0); + break; + } + setSprite("sleeping", Math.floor(idleAnimationFrame / 4)); + if (idleAnimationFrame > 192) { + resetIdleAnimation(); + } + break; + case "scratchWallN": + case "scratchWallS": + case "scratchWallE": + case "scratchWallW": + case "scratchSelf": + setSprite(idleAnimation, idleAnimationFrame); + if (idleAnimationFrame > 9) { + resetIdleAnimation(); + } + break; + default: + setSprite("idle", 0); + return; + } + idleAnimationFrame += 1; + } + + function frame() { + frameCount += 1; + const diffX = nekoPosX - mousePosX; + const diffY = nekoPosY - mousePosY; + const distance = Math.sqrt(diffX ** 2 + diffY ** 2); + + if (distance < nekoSpeed || distance < 48) { + idle(); + return; + } + + idleAnimation = null; + idleAnimationFrame = 0; + + if (idleTime > 1) { + setSprite("alert", 0); + // count down after being alerted before moving + idleTime = Math.min(idleTime, 7); + idleTime -= 1; + return; + } + + let direction; + direction = diffY / distance > 0.5 ? "N" : ""; + direction += diffY / distance < -0.5 ? "S" : ""; + direction += diffX / distance > 0.5 ? "W" : ""; + direction += diffX / distance < -0.5 ? "E" : ""; + setSprite(direction, frameCount); + + nekoPosX -= (diffX / distance) * nekoSpeed; + nekoPosY -= (diffY / distance) * nekoSpeed; + + nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16); + nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16); + + nekoEl.style.left = `${nekoPosX - 16}px`; + nekoEl.style.top = `${nekoPosY - 16}px`; + } + + init(); +})(); diff --git a/js/nav.js b/js/nav.js new file mode 100644 index 0000000..55969f6 --- /dev/null +++ b/js/nav.js @@ -0,0 +1,22 @@ +// Navigate via data-href so the destination URL never shows in the +// browser status bar on hover. Pairs with the CSS View Transitions +// (@view-transition) for a smooth cross-fade between pages. +document.querySelectorAll("[data-href]").forEach((el) => { + el.style.cursor = "pointer"; + if (!el.hasAttribute("role")) el.setAttribute("role", "link"); + if (!el.hasAttribute("tabindex")) el.setAttribute("tabindex", "0"); + + const go = () => { + const url = el.dataset.href; + if (!url) return; + location.href = url; + }; + + el.addEventListener("click", go); + el.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + go(); + } + }); +}); diff --git a/tech-stack/index.html b/tech-stack/index.html new file mode 100644 index 0000000..a64ef77 --- /dev/null +++ b/tech-stack/index.html @@ -0,0 +1,150 @@ + + + +
+ + +Tech Stack
+