This commit is contained in:
Clove 2026-06-20 21:55:36 +01:00
parent 95869d08ae
commit ab5fd681b6
15 changed files with 652 additions and 61 deletions

View File

@ -4,14 +4,14 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>88x31 Buttons | Clove Twilight</title>
<title>Clove Twilight</title>
<link rel="stylesheet" href="/css/main.css">
<script>try { var f = localStorage.getItem('ctpFlavor'); document.documentElement.setAttribute('data-flavor', ['mocha', 'macchiato', 'frappe', 'latte'].indexOf(f) >= 0 ? f : 'mocha'); } catch (e) { document.documentElement.setAttribute('data-flavor', 'mocha'); }</script>
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
<!-- SEO Meta Tags -->
<meta name="description" content="The 88x31 buttons featured across c.stupid.cat">
<meta name="keywords" content="Portfolio, Personal, Developer, 88x31, buttons">
<meta name="description" content="A collection of the 88x31 pixel buttons featured across Clove Twilight's site — grab one and link back.">
<meta name="keywords" content="Clove Twilight, c.stupid.cat, 88x31, buttons, web buttons, personal">
<meta name="author" content="doughmination">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
@ -19,7 +19,7 @@
<link rel="canonical" href="https://c.stupid.cat/88x31">
<!-- Alternate for mobile -->
<link rel="alternate" media="only screen and (max-width: 640px)" href="https://c.stupid.cat">
<link rel="alternate" media="only screen and (max-width: 640px)" href="https://c.stupid.cat/88x31">
<!-- Theme Color -->
<meta name="theme-color" content="#f5c2e7">
@ -27,8 +27,8 @@
<!-- Open Graph / Discord / Facebook -->
<meta property="og:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta property="og:site_name" content="c.stupid.cat">
<meta property="og:title" content="88x31 Buttons | Clove Twilight">
<meta property="og:description" content="The 88x31 buttons featured across c.stupid.cat">
<meta property="og:title" content="Clove Twilight">
<meta property="og:description" content="A collection of the 88x31 pixel buttons featured across Clove Twilight's site — grab one and link back.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://c.stupid.cat/88x31">
<meta property="og:locale" content="en_GB">
@ -36,8 +36,8 @@
<!-- Twitter Card -->
<meta name="twitter:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="88x31 Buttons | Clove Twilight">
<meta name="twitter:description" content="The 88x31 buttons featured across c.stupid.cat">
<meta name="twitter:title" content="Clove Twilight">
<meta name="twitter:description" content="A collection of the 88x31 pixel buttons featured across Clove Twilight's site — grab one and link back.">
<!-- Prefetch other pages for faster navigation -->
<link rel="prefetch" href="/">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

49
assets/selfies/README.md Normal file
View File

@ -0,0 +1,49 @@
# /assets/selfies
Drop your selfie image files in this folder, then list them in `selfies.json`.
The gallery at `/selfies` is rendered from that manifest by `/js/selfies.js`.
## Adding a selfie
1. Put the image file in this folder, e.g. `assets/selfies/2026-06-clove.jpg`.
2. Add an entry to `selfies.json`. **The list is shown newest-first — put the
newest selfie at the top.**
`selfies.json` is a plain JSON array. Each entry can be either:
- **a filename string** (no caption, alt text auto-generated):
```json
[
"2026-06-clove.jpg",
"2026-05-night-out.png"
]
```
- **or an object** with optional `caption` and `alt`:
```json
[
{ "src": "2026-06-clove.jpg", "caption": "golden hour ☀️", "alt": "Clove smiling in a sunlit park" },
{ "src": "2026-05-night-out.png", "caption": "night out 💃" }
]
```
You can mix both styles in the same list.
## Fields
- **`src`** (required) — the image. A bare filename resolves to
`/assets/selfies/<filename>`. You can also give a full path (`/assets/...`)
or an absolute URL (`https://...`).
- **`caption`** (optional) — short text shown **under the thumbnail and under
the enlarged photo in the lightbox**. Leave it out for no caption.
- **`alt`** (optional) — accessibility text for screen readers only (not shown
on screen). If omitted, it falls back to the caption, then to a generic
"Selfie N of Clove Twilight".
## Notes
- Common web formats work: `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.avif`.
- Until you add at least one entry, the page shows a friendly "no selfies yet"
message.

View File

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -0,0 +1,3 @@
[
{ "src": "image.png", "alt": "Selfie of Clove Twilight", "caption": "Selfie taken after I fully moved into uni ✨" }
]

View File

@ -13,14 +13,14 @@
<link rel="preconnect" href="https://cdn.discordapp.com">
<link rel="dns-prefetch" href="https://cdn.discordapp.com">
<title>Cool People</title>
<title>Clove Twilight</title>
<link rel="stylesheet" href="/css/main.css">
<script>try { var f = localStorage.getItem('ctpFlavor'); document.documentElement.setAttribute('data-flavor', ['mocha', 'macchiato', 'frappe', 'latte'].indexOf(f) >= 0 ? f : 'mocha'); } catch (e) { document.documentElement.setAttribute('data-flavor', 'mocha'); }</script>
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
<!-- SEO Meta Tags -->
<meta name="description" content="Home for Clove Twilight">
<meta name="keywords" content="Portfolio, Personal, Developer">
<meta name="description" content="Cool people Clove Twilight knows — friends, mutuals, and creators worth checking out, with links to their sites.">
<meta name="keywords" content="Clove Twilight, c.stupid.cat, friends, cool people, mutuals, links">
<meta name="author" content="doughmination">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
@ -28,7 +28,7 @@
<link rel="canonical" href="https://c.stupid.cat/cool-people">
<!-- Alternate for mobile -->
<link rel="alternate" media="only screen and (max-width: 640px)" href="https://c.stupid.cat">
<link rel="alternate" media="only screen and (max-width: 640px)" href="https://c.stupid.cat/cool-people">
<!-- Theme Color -->
<meta name="theme-color" content="#f5c2e7">
@ -36,8 +36,8 @@
<!-- Open Graph / Discord / Facebook -->
<meta property="og:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta property="og:site_name" content="c.stupid.cat">
<meta property="og:title" content="Cool People | Clove Twilight">
<meta property="og:description" content="Cool people Clove knows!">
<meta property="og:title" content="Clove Twilight">
<meta property="og:description" content="Cool people Clove Twilight knows — friends, mutuals, and creators worth checking out, with links to their sites.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://c.stupid.cat/cool-people">
<meta property="og:locale" content="en_GB">
@ -45,8 +45,9 @@
<!-- Twitter Card -->
<meta name="twitter:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Cool People | Clove Twilight">
<meta name="twitter:description" content="Cool people Clove knows!">
<meta name="twitter:title" content="Clove Twilight">
<meta name="twitter:description" content="Cool people Clove Twilight knows — friends, mutuals, and creators worth checking out, with links to their sites.">
<style>
.friends-disclaimer {
margin: 2.5rem auto 0;

View File

@ -3838,3 +3838,245 @@ body:has(.presence-stage) {
color: var(--subtext-0);
font-style: italic;
}
/* ============================================================
14. Selfies page
============================================================ */
/* Let the gallery scroll (the default layout locks the viewport). Keyed on
the wrapper class so it scrolls even before selfies.js injects the grid
mirrors the .friends-wrap rule used by the cool-people page. */
html:has(.selfies-wrap),
body:has(.selfies-wrap) {
height: auto;
min-height: 100dvh;
overflow-y: auto;
}
body:has(.selfies-wrap) {
align-items: flex-start;
}
body:has(.selfies-wrap) .hub {
max-width: 960px;
}
body:has(.selfies-wrap) .hub-header {
position: relative;
z-index: 1;
margin-bottom: 2rem;
}
.selfie-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 0.9rem;
width: 100%;
margin-bottom: 1.5rem;
padding-bottom: 4.5rem;
}
.selfie-thumb {
margin: 0;
padding: 0;
border: 1px solid var(--surface-1);
border-radius: 14px;
overflow: hidden;
background: var(--surface-0);
aspect-ratio: 1 / 1;
display: block;
transition: transform 0.15s ease, border-color 0.15s ease,
box-shadow 0.15s ease;
}
.selfie-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.selfie-thumb:hover,
.selfie-thumb:focus-visible {
transform: translateY(-3px);
border-color: rgb(var(--accent-rgb));
box-shadow: 0 6px 20px rgba(var(--accent-rgb), 0.22);
outline: none;
}
.selfie-empty {
grid-column: 1 / -1;
text-align: center;
color: var(--subtext-0);
font-style: italic;
padding: 3rem 1rem;
}
/* ---- Lightbox ---- */
.lightbox {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
background: color-mix(in srgb, var(--crust) 86%, transparent);
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
}
.lightbox[hidden] {
display: none;
}
.lightbox.is-open {
animation: lightbox-fade 0.18s ease;
}
@keyframes lightbox-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.lightbox-img {
max-width: min(92vw, 1100px);
max-height: 86vh;
object-fit: contain;
border-radius: 12px;
border: 2px solid rgba(var(--accent-rgb), 0.55);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
.lightbox-close,
.lightbox-nav {
position: fixed;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--surface-1);
background: color-mix(in srgb, var(--surface-0) 85%, transparent);
color: var(--text);
border-radius: 999px;
line-height: 1;
transition: background 0.15s ease, border-color 0.15s ease,
transform 0.15s ease;
}
.lightbox-close:hover,
.lightbox-nav:hover,
.lightbox-close:focus-visible,
.lightbox-nav:focus-visible {
background: var(--surface-1);
border-color: rgb(var(--accent-rgb));
outline: none;
}
.lightbox-close {
top: 1rem;
right: 1rem;
width: 2.6rem;
height: 2.6rem;
font-size: 1.8rem;
}
.lightbox-nav {
top: 50%;
transform: translateY(-50%);
width: 3rem;
height: 3rem;
font-size: 2rem;
}
.lightbox-nav:hover {
transform: translateY(-50%) scale(1.06);
}
.lightbox-prev {
left: 1rem;
}
.lightbox-next {
right: 1rem;
}
.lightbox-nav[hidden] {
display: none;
}
/* Freeze the page behind the lightbox while it's open */
body.lightbox-open {
overflow: hidden;
}
@media (prefers-reduced-motion: reduce) {
.selfie-thumb,
.lightbox-close,
.lightbox-nav {
transition: none;
}
.lightbox.is-open {
animation: none;
}
.lightbox-nav:hover {
transform: translateY(-50%);
}
}
@media (max-width: 560px) {
.selfie-grid {
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 0.6rem;
}
.lightbox-nav {
width: 2.6rem;
height: 2.6rem;
font-size: 1.6rem;
}
}
/* ---- Captions (thumbnails + lightbox) ---- */
.selfie-item {
margin: 0;
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.selfie-caption {
text-align: center;
font-size: 0.8rem;
line-height: 1.35;
color: var(--subtext-0);
overflow-wrap: anywhere;
}
.lightbox-figure {
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.8rem;
max-width: min(92vw, 1100px);
}
/* leave room beneath the image for the caption line */
.lightbox-img {
max-height: 80vh;
}
.lightbox-caption {
margin: 0;
text-align: center;
color: var(--text);
font-size: 0.95rem;
line-height: 1.4;
max-width: min(92vw, 1100px);
overflow-wrap: anywhere;
}
.lightbox-caption[hidden] {
display: none;
}

View File

@ -11,14 +11,14 @@
<link rel="preconnect" href="https://wakatime.com">
<link rel="dns-prefetch" href="https://wakatime.com">
<title>Clove Twilight - Dev Info</title>
<title>Clove Twilight</title>
<link rel="stylesheet" href="/css/main.css">
<script>try { var f = localStorage.getItem('ctpFlavor'); document.documentElement.setAttribute('data-flavor', ['mocha', 'macchiato', 'frappe', 'latte'].indexOf(f) >= 0 ? f : 'mocha'); } catch (e) { document.documentElement.setAttribute('data-flavor', 'mocha'); }</script>
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
<!-- SEO Meta Tags -->
<meta name="description" content="What Clove has been coding lately, tracked by dev-info">
<meta name="keywords" content="Portfolio, Personal, Developer, dev-info, Dev Info">
<meta name="description" content="What Clove Twilight has been coding lately — a live contribution heatmap and WakaTime coding stats.">
<meta name="keywords" content="Clove Twilight, c.stupid.cat, dev info, coding stats, WakaTime, contributions, developer">
<meta name="author" content="doughmination">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
@ -34,8 +34,8 @@
<!-- Open Graph / Discord / Facebook -->
<meta property="og:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta property="og:site_name" content="c.stupid.cat">
<meta property="og:title" content="Clove Twilight | Dev Info">
<meta property="og:description" content="What Clove has been coding lately, tracked by dev-info">
<meta property="og:title" content="Clove Twilight">
<meta property="og:description" content="What Clove Twilight has been coding lately — a live contribution heatmap and WakaTime coding stats.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://c.stupid.cat/dev-info">
<meta property="og:locale" content="en_GB">
@ -43,8 +43,8 @@
<!-- Twitter Card -->
<meta name="twitter:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Clove Twilight | Dev Info">
<meta name="twitter:description" content="What Clove has been coding lately, tracked by dev-info">
<meta name="twitter:title" content="Clove Twilight">
<meta name="twitter:description" content="What Clove Twilight has been coding lately — a live contribution heatmap and WakaTime coding stats.">
<!-- Prefetch other pages for faster navigation -->
<link rel="prefetch" href="/">

View File

@ -13,14 +13,14 @@
<link rel="preconnect" href="https://cdn.discordapp.com">
<link rel="dns-prefetch" href="https://cdn.discordapp.com">
<title>Clove Twilight - Discord</title>
<title>Clove Twilight</title>
<link rel="stylesheet" href="/css/main.css">
<script>try { var f = localStorage.getItem('ctpFlavor'); document.documentElement.setAttribute('data-flavor', ['mocha', 'macchiato', 'frappe', 'latte'].indexOf(f) >= 0 ? f : 'mocha'); } catch (e) { document.documentElement.setAttribute('data-flavor', 'mocha'); }</script>
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
<!-- SEO Meta Tags -->
<meta name="description" content="Clove Twilight's live Discord presence — status, activity, and what fae is up to right now.">
<meta name="keywords" content="Portfolio, Personal, Developer, Discord, presence, Lanyard">
<meta name="description" content="Clove Twilight's live Discord presence — current status, activity, and what fae is up to right now.">
<meta name="keywords" content="Clove Twilight, c.stupid.cat, Discord, presence, status, Lanyard">
<meta name="author" content="doughmination">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
@ -36,8 +36,8 @@
<!-- Open Graph / Discord / Facebook -->
<meta property="og:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta property="og:site_name" content="c.stupid.cat">
<meta property="og:title" content="Clove Twilight | Discord">
<meta property="og:description" content="Clove Twilight's live Discord presence — status, activity, and what fae is up to right now.">
<meta property="og:title" content="Clove Twilight">
<meta property="og:description" content="Clove Twilight's live Discord presence — current status, activity, and what fae is up to right now.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://c.stupid.cat/discord">
<meta property="og:locale" content="en_GB">
@ -45,8 +45,8 @@
<!-- Twitter Card -->
<meta name="twitter:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Clove Twilight | Discord">
<meta name="twitter:description" content="Clove Twilight's live Discord presence — status, activity, and what fae is up to right now.">
<meta name="twitter:title" content="Clove Twilight">
<meta name="twitter:description" content="Clove Twilight's live Discord presence — current status, activity, and what fae is up to right now.">
<!-- Prefetch other pages for faster navigation -->
<link rel="prefetch" href="/">

View File

@ -11,14 +11,14 @@
<link rel="preconnect" href="https://challenges.cloudflare.com">
<link rel="dns-prefetch" href="https://challenges.cloudflare.com">
<title>Guestbook</title>
<title>Clove Twilight</title>
<link rel="stylesheet" href="/css/main.css">
<script>try { var f = localStorage.getItem('ctpFlavor'); document.documentElement.setAttribute('data-flavor', ['mocha', 'macchiato', 'frappe', 'latte'].indexOf(f) >= 0 ? f : 'mocha'); } catch (e) { document.documentElement.setAttribute('data-flavor', 'mocha'); }</script>
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
<!-- SEO Meta Tags -->
<meta name="description" content="Sign Clove's guestbook">
<meta name="keywords" content="Guestbook, Personal, Developer">
<meta name="description" content="Sign Clove Twilight's guestbook — leave a message and say hello.">
<meta name="keywords" content="Clove Twilight, c.stupid.cat, guestbook, messages, sign">
<meta name="author" content="doughmination">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
@ -26,7 +26,7 @@
<link rel="canonical" href="https://c.stupid.cat/guestbook">
<!-- Alternate for mobile -->
<link rel="alternate" media="only screen and (max-width: 640px)" href="https://c.stupid.cat">
<link rel="alternate" media="only screen and (max-width: 640px)" href="https://c.stupid.cat/guestbook">
<!-- Theme Color -->
<meta name="theme-color" content="#f5c2e7">
@ -34,8 +34,8 @@
<!-- Open Graph / Discord / Facebook -->
<meta property="og:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta property="og:site_name" content="c.stupid.cat">
<meta property="og:title" content="Guestbook | Clove Twilight">
<meta property="og:description" content="Leave a message in Clove's guestbook!">
<meta property="og:title" content="Clove Twilight">
<meta property="og:description" content="Sign Clove Twilight's guestbook — leave a message and say hello.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://c.stupid.cat/guestbook">
<meta property="og:locale" content="en_GB">
@ -43,8 +43,8 @@
<!-- Twitter Card -->
<meta name="twitter:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Guestbook | Clove Twilight">
<meta name="twitter:description" content="Leave a message in Clove's guestbook!">
<meta name="twitter:title" content="Clove Twilight">
<meta name="twitter:description" content="Sign Clove Twilight's guestbook — leave a message and say hello.">
<!-- Prefetch other pages for faster navigation -->
<link rel="prefetch" href="/">

View File

@ -11,14 +11,14 @@
<link rel="preconnect" href="https://abacus.jasoncameron.dev" crossorigin>
<link rel="dns-prefetch" href="https://abacus.jasoncameron.dev">
<title>Clove Twilight - Home</title>
<title>Clove Twilight</title>
<link rel="stylesheet" href="/css/main.css">
<script>try { var f = localStorage.getItem('ctpFlavor'); document.documentElement.setAttribute('data-flavor', ['mocha', 'macchiato', 'frappe', 'latte'].indexOf(f) >= 0 ? f : 'mocha'); } catch (e) { document.documentElement.setAttribute('data-flavor', 'mocha'); }</script>
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
<!-- SEO Meta Tags -->
<meta name="description" content="Home for Clove Twilight">
<meta name="keywords" content="Portfolio, Personal, Developer">
<meta name="description" content="The homepage and hub for everything Clove Twilight — projects, music, Discord presence, dev stats, and more.">
<meta name="keywords" content="Clove Twilight, c.stupid.cat, portfolio, personal, developer, homepage">
<meta name="author" content="doughmination">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
@ -34,8 +34,8 @@
<!-- Open Graph / Discord / Facebook -->
<meta property="og:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta property="og:site_name" content="c.stupid.cat">
<meta property="og:title" content="Clove Twilight | Home">
<meta property="og:description" content="Home for Clove Twilight">
<meta property="og:title" content="Clove Twilight">
<meta property="og:description" content="The homepage and hub for everything Clove Twilight — projects, music, Discord presence, dev stats, and more.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://c.stupid.cat">
<meta property="og:locale" content="en_GB">
@ -43,8 +43,8 @@
<!-- Twitter Card -->
<meta name="twitter:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Clove Twilight | Home">
<meta name="twitter:description" content="Home for Clove Twilight">
<meta name="twitter:title" content="Clove Twilight">
<meta name="twitter:description" content="The homepage and hub for everything Clove Twilight — projects, music, Discord presence, dev stats, and more.">
<!-- Prefetch other pages for faster navigation -->
<link rel="prefetch" href="/cool-people">

203
js/selfies.js Normal file
View File

@ -0,0 +1,203 @@
/* ============================================================
selfies.js renders the /selfies gallery from a manifest
Manifest: /assets/selfies/selfies.json
- array of filename strings, or { "src", "alt", "caption" } objects
- shown in list order (newest first = top of the list)
"alt" is for screen readers; "caption" (optional) is shown on the page.
Click any thumbnail to open it full-size in a lightbox.
============================================================ */
(function selfies() {
"use strict";
const MANIFEST = "/assets/selfies/selfies.json";
const FOLDER = "/assets/selfies/";
const root = document.getElementById("selfies-root");
if (!root) return;
const reduceMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
/* ---------- helpers ---------- */
// Resolve a manifest src to a usable URL.
function resolveSrc(s) {
if (typeof s !== "string") return "";
s = s.trim();
if (/^https?:\/\//i.test(s) || s.startsWith("/")) return s;
return FOLDER + s.replace(/^\.?\//, "");
}
// Normalise a manifest entry into { src, alt, caption } or null if unusable.
function normalize(entry, i) {
let raw = "";
let alt = "";
let caption = "";
if (typeof entry === "string") {
raw = entry;
} else if (entry && typeof entry === "object" && entry.src) {
raw = entry.src;
alt = typeof entry.alt === "string" ? entry.alt : "";
caption = typeof entry.caption === "string" ? entry.caption.trim() : "";
}
if (typeof raw !== "string" || !raw.trim()) return null;
const src = resolveSrc(raw);
if (!src) return null;
if (!alt) alt = caption || "Selfie " + (i + 1) + " of Clove Twilight";
return { src, alt, caption };
}
function showMessage(text) {
root.innerHTML = "";
const p = document.createElement("p");
p.className = "selfie-empty";
p.textContent = text;
root.appendChild(p);
}
/* ---------- lightbox ---------- */
let items = [];
let current = 0;
let lastFocus = null;
const lb = document.createElement("div");
lb.className = "lightbox";
lb.hidden = true;
lb.setAttribute("role", "dialog");
lb.setAttribute("aria-modal", "true");
lb.setAttribute("aria-label", "Selfie viewer");
lb.innerHTML =
'<button class="lightbox-close" type="button" aria-label="Close (Esc)">&times;</button>' +
'<button class="lightbox-nav lightbox-prev" type="button" aria-label="Previous selfie">&#8249;</button>' +
'<figure class="lightbox-figure">' +
'<img class="lightbox-img" alt="">' +
'<figcaption class="lightbox-caption" hidden></figcaption>' +
"</figure>" +
'<button class="lightbox-nav lightbox-next" type="button" aria-label="Next selfie">&#8250;</button>';
document.body.appendChild(lb);
const lbImg = lb.querySelector(".lightbox-img");
const lbCap = lb.querySelector(".lightbox-caption");
const btnClose = lb.querySelector(".lightbox-close");
const btnPrev = lb.querySelector(".lightbox-prev");
const btnNext = lb.querySelector(".lightbox-next");
function preload(i) {
if (i < 0 || i >= items.length) return;
const img = new Image();
img.src = items[i].src;
}
function render(i) {
current = (i + items.length) % items.length; // wrap around
const it = items[current];
lbImg.src = it.src;
lbImg.alt = it.alt;
if (it.caption) {
lbCap.textContent = it.caption;
lbCap.hidden = false;
} else {
lbCap.textContent = "";
lbCap.hidden = true;
}
const multiple = items.length > 1;
btnPrev.hidden = !multiple;
btnNext.hidden = !multiple;
if (multiple) {
preload(current + 1);
preload(current - 1);
}
}
function open(i) {
lastFocus = document.activeElement;
render(i);
lb.hidden = false;
if (!reduceMotion) lb.classList.add("is-open");
document.body.classList.add("lightbox-open");
btnClose.focus();
}
function close() {
lb.hidden = true;
lb.classList.remove("is-open");
document.body.classList.remove("lightbox-open");
lbImg.removeAttribute("src");
if (lastFocus && typeof lastFocus.focus === "function") lastFocus.focus();
}
const next = () => render(current + 1);
const prev = () => render(current - 1);
btnClose.addEventListener("click", close);
btnNext.addEventListener("click", next);
btnPrev.addEventListener("click", prev);
// Click on the dim backdrop (but not the image/caption or buttons) closes.
lb.addEventListener("click", (e) => {
if (e.target === lb) close();
});
document.addEventListener("keydown", (e) => {
if (lb.hidden) return;
if (e.key === "Escape") close();
else if (e.key === "ArrowRight") next();
else if (e.key === "ArrowLeft") prev();
});
/* ---------- grid ---------- */
function buildGrid(list) {
root.innerHTML = "";
const frag = document.createDocumentFragment();
list.forEach((it, i) => {
const fig = document.createElement("figure");
fig.className = "selfie-item";
const btn = document.createElement("button");
btn.type = "button";
btn.className = "selfie-thumb";
btn.setAttribute("aria-label", "Open " + (it.caption || it.alt));
const img = document.createElement("img");
img.src = it.src;
img.alt = it.alt;
img.loading = i < 4 ? "eager" : "lazy";
img.decoding = "async";
// If an image fails to load, drop its tile so the grid stays clean.
img.addEventListener("error", () => fig.remove());
btn.appendChild(img);
btn.addEventListener("click", () => open(i));
fig.appendChild(btn);
if (it.caption) {
const cap = document.createElement("figcaption");
cap.className = "selfie-caption";
cap.textContent = it.caption;
fig.appendChild(cap);
}
frag.appendChild(fig);
});
root.appendChild(frag);
}
/* ---------- load ---------- */
root.setAttribute("aria-busy", "true");
fetch(MANIFEST, { cache: "no-cache" })
.then((r) => {
if (!r.ok) throw new Error("manifest " + r.status);
return r.json();
})
.then((data) => {
if (!Array.isArray(data)) throw new Error("manifest is not an array");
items = data.map(normalize).filter(Boolean);
if (!items.length) {
showMessage("No selfies yet — check back soon! 📸");
return;
}
buildGrid(items);
})
.catch((err) => {
console.error("Could not load selfies:", err);
showMessage("Couldn't load the selfies right now.");
})
.finally(() => root.removeAttribute("aria-busy"));
})();

View File

@ -11,27 +11,41 @@
<link rel="preconnect" href="https://lyrics.lanyard.cafe" crossorigin>
<link rel="dns-prefetch" href="https://lyrics.lanyard.cafe">
<title>Music · Clove Twilight</title>
<title>Clove Twilight</title>
<link rel="stylesheet" href="/css/main.css">
<script>try { var f = localStorage.getItem('ctpFlavor'); document.documentElement.setAttribute('data-flavor', ['mocha', 'macchiato', 'frappe', 'latte'].indexOf(f) >= 0 ? f : 'mocha'); } catch (e) { document.documentElement.setAttribute('data-flavor', 'mocha'); }</script>
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
<!-- SEO Meta Tags -->
<meta name="description" content="What Clove Twilight is listening to — live now-playing, synced lyrics, and recent plays.">
<meta name="description" content="What Clove Twilight is listening to — live now-playing track, synced lyrics, and recent plays from Last.fm.">
<meta name="keywords" content="Clove Twilight, c.stupid.cat, music, now playing, Last.fm, lyrics, scrobbles">
<meta name="author" content="doughmination">
<meta name="robots" content="index, follow">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
<!-- Canonical URL -->
<link rel="canonical" href="https://c.stupid.cat/music">
<!-- Alternate for mobile -->
<link rel="alternate" media="only screen and (max-width: 640px)" href="https://c.stupid.cat/music">
<!-- Theme Color -->
<meta name="theme-color" content="#f5c2e7">
<!-- Open Graph -->
<!-- Open Graph / Discord / Facebook -->
<meta property="og:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta property="og:site_name" content="c.stupid.cat">
<meta property="og:title" content="Music · Clove Twilight">
<meta property="og:description" content="Live now-playing, synced lyrics, and recent plays.">
<meta property="og:title" content="Clove Twilight">
<meta property="og:description" content="What Clove Twilight is listening to — live now-playing track, synced lyrics, and recent plays from Last.fm.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://c.stupid.cat/music">
<meta property="og:locale" content="en_GB">
<!-- Twitter Card -->
<meta name="twitter:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Clove Twilight">
<meta name="twitter:description" content="What Clove Twilight is listening to — live now-playing track, synced lyrics, and recent plays from Last.fm.">
<!-- Prefetch other pages for faster navigation -->
<link rel="prefetch" href="/">
<link rel="prefetch" href="/cool-people">

View File

@ -4,22 +4,22 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Clove Twilight - Projects</title>
<title>Clove Twilight</title>
<link rel="stylesheet" href="/css/main.css">
<script>try { var f = localStorage.getItem('ctpFlavor'); document.documentElement.setAttribute('data-flavor', ['mocha', 'macchiato', 'frappe', 'latte'].indexOf(f) >= 0 ? f : 'mocha'); } catch (e) { document.documentElement.setAttribute('data-flavor', 'mocha'); }</script>
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
<!-- SEO Meta Tags -->
<meta name="description" content="Find all the Projects Clove contributes on actively">
<meta name="keywords" content="Portfolio, Personal, Developer">
<meta name="description" content="Explore the projects Clove Twilight actively builds and contributes to, from web apps to Discord bots.">
<meta name="keywords" content="Clove Twilight, c.stupid.cat, projects, portfolio, developer, open source">
<meta name="author" content="doughmination">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
<!-- Canonical URL -->
<link rel="canonical" href="https://c.stupid.cat">
<link rel="canonical" href="https://c.stupid.cat/projects">
<!-- Alternate for mobile -->
<link rel="alternate" media="only screen and (max-width: 640px)" href="https://c.stupid.cat">
<link rel="alternate" media="only screen and (max-width: 640px)" href="https://c.stupid.cat/projects">
<!-- Theme Color -->
<meta name="theme-color" content="#f5c2e7">
@ -27,17 +27,17 @@
<!-- Open Graph / Discord / Facebook -->
<meta property="og:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta property="og:site_name" content="c.stupid.cat">
<meta property="og:title" content="Clove Twilight | Projects">
<meta property="og:description" content="Find all the Projects Clove contributes on actively">
<meta property="og:title" content="Clove Twilight">
<meta property="og:description" content="Explore the projects Clove Twilight actively builds and contributes to, from web apps to Discord bots.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://c.stupid.cat">
<meta property="og:url" content="https://c.stupid.cat/projects">
<meta property="og:locale" content="en_GB">
<!-- Twitter Card -->
<meta name="twitter:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Clove Twilight | Discord Bot">
<meta name="twitter:description" content="Find all the Projects Clove contributes on actively">
<meta name="twitter:title" content="Clove Twilight">
<meta name="twitter:description" content="Explore the projects Clove Twilight actively builds and contributes to, from web apps to Discord bots.">
<!-- Prefetch other pages for faster navigation -->
<link rel="prefetch" href="/">

79
selfies/index.html Normal file
View File

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Clove Twilight</title>
<link rel="stylesheet" href="/css/main.css">
<script>try { var f = localStorage.getItem('ctpFlavor'); document.documentElement.setAttribute('data-flavor', ['mocha', 'macchiato', 'frappe', 'latte'].indexOf(f) >= 0 ? f : 'mocha'); } catch (e) { document.documentElement.setAttribute('data-flavor', 'mocha'); }</script>
<link rel="icon" type="image/svg+xml" href="/assets/favicon/favicon.svg">
<!-- SEO Meta Tags -->
<meta name="description" content="Browse a gallery of selfies from Clove Twilight.">
<meta name="keywords" content="Clove Twilight, c.stupid.cat, selfies, photos, gallery, personal">
<meta name="author" content="doughmination">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
<!-- Canonical URL -->
<link rel="canonical" href="https://c.stupid.cat/selfies">
<!-- Alternate for mobile -->
<link rel="alternate" media="only screen and (max-width: 640px)" href="https://c.stupid.cat/selfies">
<!-- Theme Color -->
<meta name="theme-color" content="#f5c2e7">
<!-- Open Graph / Discord / Facebook -->
<meta property="og:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta property="og:site_name" content="c.stupid.cat">
<meta property="og:title" content="Clove Twilight">
<meta property="og:description" content="Browse a gallery of selfies from Clove Twilight.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://c.stupid.cat/selfies">
<meta property="og:locale" content="en_GB">
<!-- Twitter Card -->
<meta name="twitter:image" content="https://c.stupid.cat/assets/favicon/favicon.png">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Clove Twilight">
<meta name="twitter:description" content="Browse a gallery of selfies from Clove Twilight.">
<!-- Prefetch other pages for faster navigation -->
<link rel="prefetch" href="/">
<link rel="prefetch" href="/cool-people">
<link rel="prefetch" href="/dev-info">
<link rel="prefetch" href="/discord">
<link rel="prefetch" href="/projects">
<link rel="prefetch" href="/music">
<link rel="prefetch" href="/88x31">
</head>
<body>
<header class="nav">
<nav class="nav-links">
<a class="nav-link" data-href="/">Home</a>
<a class="nav-link" data-href="/cool-people">Cool People</a>
<a class="nav-link" data-href="/dev-info">Dev Info</a>
<a class="nav-link" data-href="/discord">Discord</a>
<a class="nav-link" data-href="/projects">Projects</a>
<a class="nav-link" data-href="/music">Music</a>
<a class="nav-link" data-href="/88x31">88x31</a>
<a class="nav-link" data-href="/guestbook">Guestbook</a>
</nav>
</header>
<div class="hub selfies-wrap">
<header class="hub-header">
<h1>Selfies</h1>
<p class="tagline">A gallery of my selfies &mdash; tap any photo to view it full size</p>
</header>
<!-- Thumbnails are rendered by selfies.js from /assets/selfies/selfies.json -->
<div id="selfies-root" class="selfie-grid" aria-label="Selfies gallery"></div>
</div>
<script src="/js/core.js" data-cat="/assets/oneko/classics/classic.png"></script>
<script src="/js/selfies.js"></script>
</body>
</html>