diff --git a/assets/js/code-copy.js b/assets/js/code-copy.js new file mode 100644 index 0000000..f55f123 --- /dev/null +++ b/assets/js/code-copy.js @@ -0,0 +1,70 @@ +// Add copy buttons to all code blocks +document.addEventListener("DOMContentLoaded", function () { + // Find all
elements that contain
+ const codeBlocks = document.querySelectorAll("pre code");
+
+ codeBlocks.forEach((codeBlock) => {
+ const pre = codeBlock.parentElement;
+
+ // Create wrapper for positioning
+ const wrapper = document.createElement("div");
+ wrapper.style.position = "relative";
+
+ // Wrap the pre element
+ pre.parentNode.insertBefore(wrapper, pre);
+ wrapper.appendChild(pre);
+
+ // Create copy button
+ const copyButton = document.createElement("button");
+ copyButton.className = "code-copy-btn";
+ copyButton.innerHTML = `
+
+ `;
+ copyButton.setAttribute("aria-label", "Copy code to clipboard");
+
+ // Add click handler
+ copyButton.addEventListener("click", async () => {
+ const code = codeBlock.textContent;
+
+ try {
+ await navigator.clipboard.writeText(code);
+
+ // Show success feedback
+ copyButton.classList.add("copied");
+
+ // Reset after 2 seconds
+ setTimeout(() => {
+ copyButton.classList.remove("copied");
+ }, 2000);
+ } catch (err) {
+ console.error("Failed to copy code:", err);
+
+ // Fallback for older browsers
+ const textArea = document.createElement("textarea");
+ textArea.value = code;
+ textArea.style.position = "fixed";
+ textArea.style.opacity = "0";
+ document.body.appendChild(textArea);
+ textArea.select();
+
+ try {
+ document.execCommand("copy");
+ copyButton.classList.add("copied");
+
+ setTimeout(() => {
+ copyButton.classList.remove("copied");
+ }, 2000);
+ } catch (err2) {
+ console.error("Fallback copy failed:", err2);
+ }
+
+ document.body.removeChild(textArea);
+ }
+ });
+
+ wrapper.appendChild(copyButton);
+ });
+});
diff --git a/assets/js/lastfm-stats.js b/assets/js/lastfm-stats.js
new file mode 100644
index 0000000..92b9192
--- /dev/null
+++ b/assets/js/lastfm-stats.js
@@ -0,0 +1,260 @@
+// Last.fm Stats Interactive Module
+(function () {
+ "use strict";
+
+ const LASTFM_API_URL = "https://ws.audioscrobbler.com/2.0/";
+ const LASTFM_API_KEY = "3a4fef48fecc593d25e0f9a40df1fefe";
+
+ // Store current stats for export
+ let currentStats = {
+ artists: [],
+ totalTracks: 0,
+ period: "",
+ username: "",
+ };
+
+ // Calculate timestamps based on period
+ function getTimestamps(period) {
+ const now = Math.floor(Date.now() / 1000);
+ let from;
+
+ if (period === "7day") {
+ from = now - 7 * 24 * 60 * 60; // 7 days
+ } else if (period === "1month") {
+ from = now - 30 * 24 * 60 * 60; // 30 days
+ }
+
+ return { from, to: now };
+ }
+
+ // Fetch top artists for the specified period
+ async function fetchTopArtists(username, period) {
+ const url = `${LASTFM_API_URL}?method=user.gettopartists&user=${username}&api_key=${LASTFM_API_KEY}&format=json&period=${period}&limit=5`;
+
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch top artists: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+
+ // Check for Last.fm API errors
+ if (data.error) {
+ throw new Error(data.message || "Last.fm API error");
+ }
+
+ return data.topartists?.artist || [];
+ }
+
+ // Fetch recent tracks to count total scrobbles in period
+ async function fetchTrackCount(username, period) {
+ const { from, to } = getTimestamps(period);
+ const url = `${LASTFM_API_URL}?method=user.getrecenttracks&user=${username}&api_key=${LASTFM_API_KEY}&format=json&from=${from}&to=${to}&limit=1`;
+
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch track count: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+
+ // Check for Last.fm API errors
+ if (data.error) {
+ throw new Error(data.message || "Last.fm API error");
+ }
+
+ return data.recenttracks?.["@attr"]?.total || 0;
+ }
+
+ // Generate markdown format
+ function generateMarkdown() {
+ const periodText =
+ currentStats.period === "7day" ? "Past Week" : "Past Month";
+ let markdown = `## Last.fm Stats - ${periodText}\n\n`;
+ markdown += `**Total Tracks:** ${currentStats.totalTracks}\n\n`;
+ markdown += `**Top 5 Artists:**\n\n`;
+
+ currentStats.artists.forEach((artist) => {
+ markdown += `- [${artist.name}](${artist.url}) - ${artist.playcount} plays\n`;
+ });
+
+ return markdown;
+ }
+
+ // Generate plain text format
+ function generatePlainText() {
+ const periodText =
+ currentStats.period === "7day" ? "Past Week" : "Past Month";
+ let text = `Last.fm Stats - ${periodText}\n\n`;
+ text += `Total Tracks: ${currentStats.totalTracks}\n\n`;
+ text += `Top 5 Artists:\n\n`;
+
+ currentStats.artists.forEach((artist) => {
+ text += `- ${artist.name} - ${artist.playcount} plays\n`;
+ });
+
+ return text;
+ }
+
+ // Copy to clipboard
+ async function copyToClipboard(text, button) {
+ try {
+ await navigator.clipboard.writeText(text);
+ const originalText = button.textContent;
+ button.textContent = "Copied!";
+ button.classList.add("copied");
+
+ setTimeout(() => {
+ button.textContent = originalText;
+ button.classList.remove("copied");
+ }, 2000);
+ } catch (err) {
+ console.error("Failed to copy:", err);
+ alert("Failed to copy to clipboard");
+ }
+ }
+
+ // Display the stats
+ function displayStats(artists, totalTracks, period, username) {
+ const artistsList = document.getElementById("top-artists");
+ const totalTracksEl = document.getElementById("total-tracks");
+
+ // Store current stats for export
+ currentStats = { artists, totalTracks, period, username };
+
+ // Update total tracks
+ totalTracksEl.textContent = totalTracks;
+
+ // Clear and populate artists list
+ artistsList.innerHTML = "";
+
+ if (artists.length === 0) {
+ artistsList.innerHTML = "No artists found for this period ";
+ return;
+ }
+
+ artists.forEach((artist) => {
+ const li = document.createElement("li");
+ li.innerHTML = `${artist.name} - ${artist.playcount} plays`;
+ artistsList.appendChild(li);
+ });
+
+ // Show export buttons
+ const exportButtons = document.getElementById("export-buttons");
+ if (exportButtons) {
+ exportButtons.style.display = "flex";
+ }
+ }
+
+ // Show/hide UI elements
+ function setLoadingState(isLoading) {
+ const loading = document.getElementById("stats-loading");
+ const content = document.getElementById("stats-content");
+ const error = document.getElementById("stats-error");
+ const results = document.getElementById("stats-results");
+
+ results.style.display = "block";
+
+ if (isLoading) {
+ loading.style.display = "block";
+ content.style.display = "none";
+ error.style.display = "none";
+ } else {
+ loading.style.display = "none";
+ }
+ }
+
+ function showError(message) {
+ const error = document.getElementById("stats-error");
+ const errorMessage = document.getElementById("error-message");
+ const content = document.getElementById("stats-content");
+
+ error.style.display = "block";
+ content.style.display = "none";
+ errorMessage.textContent = message;
+ }
+
+ function showContent() {
+ const error = document.getElementById("stats-error");
+ const content = document.getElementById("stats-content");
+
+ error.style.display = "none";
+ content.style.display = "block";
+ }
+
+ // Main fetch function
+ async function fetchStats() {
+ const username = document.getElementById("lastfm-username").value.trim();
+ const period = document.getElementById("time-period").value;
+
+ if (!username) {
+ showError("Please enter a Last.fm username");
+ return;
+ }
+
+ setLoadingState(true);
+
+ try {
+ // Fetch both stats in parallel
+ const [artists, totalTracks] = await Promise.all([
+ fetchTopArtists(username, period),
+ fetchTrackCount(username, period),
+ ]);
+
+ displayStats(artists, totalTracks, period, username);
+ showContent();
+ } catch (error) {
+ console.error("Error fetching Last.fm stats:", error);
+ showError(
+ error.message ||
+ "Failed to fetch stats. Please check the username and try again.",
+ );
+ } finally {
+ setLoadingState(false);
+ }
+ }
+
+ // Initialize when DOM is ready
+ function init() {
+ const fetchButton = document.getElementById("fetch-stats");
+ const usernameInput = document.getElementById("lastfm-username");
+ const copyMarkdownBtn = document.getElementById("copy-markdown");
+ const copyPlainTextBtn = document.getElementById("copy-plaintext");
+
+ if (!fetchButton || !usernameInput) {
+ return; // Not on the stats page
+ }
+
+ // Fetch stats on button click
+ fetchButton.addEventListener("click", fetchStats);
+
+ // Also fetch on Enter key in username input
+ usernameInput.addEventListener("keypress", (e) => {
+ if (e.key === "Enter") {
+ fetchStats();
+ }
+ });
+
+ // Copy buttons
+ if (copyMarkdownBtn) {
+ copyMarkdownBtn.addEventListener("click", () => {
+ const markdown = generateMarkdown();
+ copyToClipboard(markdown, copyMarkdownBtn);
+ });
+ }
+
+ if (copyPlainTextBtn) {
+ copyPlainTextBtn.addEventListener("click", () => {
+ const plainText = generatePlainText();
+ copyToClipboard(plainText, copyPlainTextBtn);
+ });
+ }
+ }
+
+ // Run init when DOM is loaded
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", init);
+ } else {
+ init();
+ }
+})();
diff --git a/assets/js/pgp-copy.js b/assets/js/pgp-copy.js
new file mode 100644
index 0000000..843662f
--- /dev/null
+++ b/assets/js/pgp-copy.js
@@ -0,0 +1,33 @@
+// PGP Key copy functionality
+document.addEventListener('DOMContentLoaded', function() {
+ const copyButtons = document.querySelectorAll('.pgp-copy-trigger');
+
+ copyButtons.forEach(button => {
+ button.addEventListener('click', async function() {
+ const feedback = button.closest('.contact-pgp').querySelector('.pgp-copy-feedback');
+
+ try {
+ const response = await fetch('/publickey.asc');
+ const pgpKey = await response.text();
+
+ await navigator.clipboard.writeText(pgpKey);
+
+ feedback.textContent = 'PGP key copied to clipboard!';
+ feedback.classList.add('show', 'success');
+
+ setTimeout(() => {
+ feedback.classList.remove('show');
+ }, 3000);
+ } catch (err) {
+ console.error('Failed to copy PGP key:', err);
+
+ feedback.textContent = 'Failed to copy key';
+ feedback.classList.add('show', 'error');
+
+ setTimeout(() => {
+ feedback.classList.remove('show');
+ }, 3000);
+ }
+ });
+ });
+});
diff --git a/assets/sass/pages/blog.scss b/assets/sass/pages/blog.scss
index 4ed1a18..0d2d9a2 100644
--- a/assets/sass/pages/blog.scss
+++ b/assets/sass/pages/blog.scss
@@ -559,7 +559,7 @@
}
// Contact section styling
- .blog-contact-section {
+ .contact-section {
margin-top: 40px;
padding: 20px;
background: rgba(0, 255, 0, 0.05);
diff --git a/assets/sass/pages/resources.scss b/assets/sass/pages/resources.scss
index d615573..06ec85d 100644
--- a/assets/sass/pages/resources.scss
+++ b/assets/sass/pages/resources.scss
@@ -1,8 +1,80 @@
-// Resources Whiteboard Page
-.resources-page {
+.test-chamber-grid {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+
+ background-image:
+ linear-gradient(rgba(58, 58, 58, 0.6) 2px, transparent 2px),
+ linear-gradient(90deg, rgba(58, 58, 58, 0.6) 2px, transparent 2px),
+ radial-gradient(
+ ellipse 80% 20% at 50% 100%,
+ rgba(200, 220, 255, 0.25) 0%,
+ transparent 50%
+ ),
+ linear-gradient(180deg, #6a6d75 0%, #7a7d85 50%, #3f4146 100%);
+ background-size:
+ 150px 200px,
+ 150px 200px,
+ 100% 100%,
+ 100% 100%;
+ background-position:
+ 0 0,
+ 0 0,
+ 0 0,
+ 0 0;
+
+ &::before {
+ content: "TEST 06";
+ position: absolute;
+ top: 55%;
+ right: 8%;
+ transform: translateY(-50%);
+ font-family: "Barlow Condensed", sans-serif;
+ font-size: 6rem;
+ font-weight: 700;
+ color: rgba(255, 255, 255, 0.3);
+ letter-spacing: 0.2em;
+ text-transform: uppercase;
+ pointer-events: none;
+ z-index: 1;
+ writing-mode: vertical-rl;
+ text-orientation: mixed;
+
+ @include media-up(md) {
+ font-size: 7rem;
+ }
+ }
+
+ &::after {
+ position: absolute;
+ z-index: 12;
+ top: -180px;
+ left: -200px;
+ width: 600px;
+ height: 600px;
+ background: radial-gradient(
+ ellipse 60% 70% at 40% 45%,
+ rgba(0, 0, 0, 0.65) 0%,
+ rgba(0, 0, 0, 0.4) 30%,
+ rgba(0, 0, 0, 0.3) 50%,
+ rgba(0, 0, 0, 0.15) 70%,
+ transparent 100%
+ );
+ content: " ";
+ filter: blur(8px);
+ }
+}
+
+.resources-page,
+.resouce-single {
min-height: 100vh;
padding: 2rem 1rem;
+}
+// Resources Whiteboard Page
+.resources-page {
@include media-up(md) {
padding: 3rem 2rem;
}
@@ -11,46 +83,342 @@
.resources-container {
max-width: 1400px;
margin: 0 auto;
+ position: relative;
+ z-index: 1;
}
-.whiteboard {
- background: linear-gradient(135deg, #f5f5f0 0%, #e8e8dd 100%);
- border-radius: 8px;
-
- box-shadow:
- 0 10px 40px rgba(0, 0, 0, 0.1),
- inset 0 0 100px rgba(0, 0, 0, 0.02);
+// Portal Header with portals on either side
+.portal-header {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 3rem;
+ margin-bottom: 3rem;
position: relative;
- // Subtle texture overlay
- &::before {
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-image:
- repeating-linear-gradient(
- 0deg,
- transparent,
- transparent 2px,
- rgba(0, 0, 0, 0.01) 2px,
- rgba(0, 0, 0, 0.01) 4px
- ),
- repeating-linear-gradient(
- 90deg,
- transparent,
- transparent 2px,
- rgba(0, 0, 0, 0.01) 2px,
- rgba(0, 0, 0, 0.01) 4px
+ .portal-title {
+ font-size: 3.5rem;
+ font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.15em;
+ color: #ffffff;
+ text-shadow:
+ 0 0 20px rgba(255, 255, 255, 0.8),
+ 0 0 40px rgba(255, 255, 255, 0.4),
+ 0 2px 4px rgba(0, 0, 0, 0.3);
+ margin: 0;
+ position: relative;
+ z-index: 2;
+
+ @include media-up(md) {
+ font-size: 4rem;
+ }
+ }
+
+ .portal-icon {
+ width: 100px;
+ height: 160px;
+ position: relative;
+ overflow: visible;
+ z-index: 1;
+
+ // Portal ring (the colored edge)
+ &::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ border: 8px solid;
+ z-index: -1;
+ animation: portal-pulse 2s ease-in-out infinite;
+ }
+
+ // Black center void
+ &::after {
+ content: "";
+ position: absolute;
+ inset: 16px;
+ border-radius: 50%;
+ background: radial-gradient(
+ ellipse at center,
+ rgba(0, 0, 0, 0.95) 0%,
+ rgba(0, 0, 0, 0.85) 60%,
+ rgba(0, 0, 0, 0.6) 100%
);
- pointer-events: none;
- border-radius: 8px;
+ z-index: 1;
+ }
+
+ &.blue-portal {
+ left: 100px;
+
+ &::before {
+ border-color: #0096ff;
+ box-shadow:
+ 0 0 30px 8px rgba(0, 150, 255, 0.8),
+ 0 0 60px 12px rgba(0, 150, 255, 0.5),
+ inset 0 0 30px rgba(0, 150, 255, 0.6);
+ }
+
+ &::after {
+ box-shadow:
+ inset 0 0 40px rgba(0, 150, 255, 0.3),
+ inset 0 0 20px rgba(0, 150, 255, 0.5);
+ }
+ }
+
+ &.orange-portal {
+ left: -100px;
+ &::before {
+ border-color: #ff7800;
+ box-shadow:
+ 0 0 30px 8px rgba(255, 120, 0, 0.8),
+ 0 0 60px 12px rgba(255, 120, 0, 0.5),
+ inset 0 0 30px rgba(255, 120, 0, 0.6);
+ }
+
+ &::after {
+ box-shadow:
+ inset 0 0 40px rgba(255, 120, 0, 0.3),
+ inset 0 0 20px rgba(255, 120, 0, 0.5);
+ }
+ }
}
}
-.whiteboard-header {
+// Companion Cube
+.companion-cube {
+ position: fixed;
+ bottom: 40px;
+ right: 40px;
+ width: 80px;
+ height: 80px;
+ background: linear-gradient(
+ 135deg,
+ rgba(200, 200, 200, 0.2) 0%,
+ rgba(150, 150, 150, 0.3) 100%
+ );
+ border: 2px solid rgba(200, 200, 200, 0.4);
+ border-radius: 8px;
+ box-shadow:
+ 0 10px 30px rgba(0, 0, 0, 0.5),
+ inset 0 0 20px rgba(255, 255, 255, 0.1);
+ transform: rotateX(15deg) rotateY(-15deg);
+ transform-style: preserve-3d;
+ animation: cube-float 4s ease-in-out infinite;
+ position: relative;
+
+ // Heart symbol in center
+ &::before {
+ content: "♥";
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 32px;
+ color: rgba(255, 100, 150, 0.6);
+ text-shadow: 0 0 10px rgba(255, 100, 150, 0.8);
+ }
+
+ // Corner circles
+ &::after {
+ content: "";
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: rgba(200, 200, 200, 0.4);
+ top: 8px;
+ left: 8px;
+ box-shadow:
+ 60px 0 0 rgba(200, 200, 200, 0.4),
+ 0 60px 0 rgba(200, 200, 200, 0.4),
+ 60px 60px 0 rgba(200, 200, 200, 0.4);
+ }
+
+ @include media-up(md) {
+ width: 100px;
+ height: 100px;
+
+ &::before {
+ font-size: 40px;
+ }
+ }
+}
+
+@keyframes portal-pulse {
+ 0%,
+ 100% {
+ transform: scaleX(0.75) scaleY(0.85);
+ opacity: 1;
+ }
+ 50% {
+ transform: scaleX(0.8) scaleY(0.9);
+ opacity: 0.9;
+ }
+}
+
+@keyframes portal-ring-pulse {
+ 0%,
+ 100% {
+ transform: scale(1);
+ opacity: 0.3;
+ }
+ 50% {
+ transform: scale(1.2);
+ opacity: 0.1;
+ }
+}
+
+@keyframes cube-float {
+ 0%,
+ 100% {
+ transform: rotateX(15deg) rotateY(-15deg) translateY(0);
+ }
+ 50% {
+ transform: rotateX(15deg) rotateY(-15deg) translateY(-10px);
+ }
+}
+
+@keyframes portal-spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes portal-vortex {
+ 0% {
+ transform: rotate(0deg) scale(1);
+ }
+ 50% {
+ transform: rotate(180deg) scale(1.1);
+ }
+ 100% {
+ transform: rotate(360deg) scale(1);
+ }
+}
+
+.portal-sign {
+ position: relative;
+ padding: 3rem 2rem;
+
+ border: 10px solid black;
+ background: white;
+
+ max-width: 900px;
+ margin: auto;
+
+ box-shadow:
+ 0 8px 24px rgba(0, 0, 0, 0.6),
+ 0 0 40px rgba(0, 0, 0, 0.6);
+
+ @include media-up(md) {
+ padding: 4rem 3rem;
+ }
+
+ .portal-sign-decor {
+ border-top: 10px solid black;
+ border-bottom: 10px solid black;
+ margin-left: 6rem;
+ position: relative;
+
+ @include media-down(lg) {
+ margin-left: auto;
+ }
+
+ .portal-sign-text {
+ p {
+ margin-bottom: 1rem;
+ }
+
+ font-family: "caveat", cursive;
+ font-size: 1.5rem;
+ position: absolute;
+ text-align: right;
+ right: 0;
+ top: 50px;
+ width: 50%;
+ transform: rotate(10deg);
+
+ @include media-down(md) {
+ position: relative;
+ transform: none;
+ top: auto;
+ left: auto;
+ width: 100%;
+ text-align: left;
+ margin-top: 1em;
+ }
+ }
+
+ .portal-sign-number {
+ position: relative;
+ font-family: "Barlow Condensed", sans-serif;
+ font-size: 30rem;
+
+ @include media-down(md) {
+ display: none;
+ }
+ }
+
+ .portal-sign-sub {
+ font-family: "Barlow Condensed", sans-serif;
+ font-size: 3rem;
+ font-weight: bold;
+
+ @include media-down(md) {
+ display: none;
+ }
+ }
+ }
+
+ .portal-sign-lines {
+ margin-left: 6rem;
+ margin-top: 1rem;
+ padding-bottom: 5rem;
+ display: block;
+ position: relative;
+ border-bottom: 10px solid black;
+
+ @include media-down(md) {
+ display: none;
+ }
+
+ @include media-down(lg) {
+ margin-left: auto;
+ }
+
+ &::before {
+ content: "";
+ height: 50px;
+ width: 10px;
+ background-color: black;
+ display: block;
+
+ /* repeat the line 5 more times */
+ box-shadow:
+ 20px 0 0 black,
+ 40px 0 0 black,
+ 60px 0 0 black,
+ 80px 0 0 black,
+ 100px 0 0 black;
+ }
+ }
+
+ .portal-sign-content {
+ margin-left: 6rem;
+ margin-top: 1rem;
+
+ @include media-down(lg) {
+ margin-left: auto;
+ }
+ }
+}
+
+.portal-header {
text-align: center;
margin-bottom: 4rem;
position: relative;
@@ -58,129 +426,63 @@
h1 {
font-size: 3rem;
- color: #2c3e50;
+ color: #0096ff;
margin-bottom: 1rem;
font-weight: 700;
- }
-
- .whiteboard-description {
- font-family: "Caveat", cursive;
- font-size: 28px;
- font-weight: bold;
- color: #2c3e50;
- max-width: 600px;
- margin: 0 auto;
+ text-shadow: 0 0 20px rgba(0, 150, 255, 0.6);
}
}
-.whiteboard-pins {
+.portal-sign-items {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
- gap: 3rem;
- position: relative;
- z-index: 1;
-
- @include media-up(lg) {
- grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
- }
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 1em;
}
-// Resource Pin (Post-it note style)
-.resource-pin {
+.resource-pin a {
+ color: #000;
+}
+
+.resource-card {
+ padding: 2rem 1.5rem 1.5rem;
+ aspect-ratio: 1/1;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ border: 3px solid;
position: relative;
- transform: rotate(0deg);
- transition:
- transform 0.3s ease,
- box-shadow 0.3s ease;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
- // Random slight rotations for pins
- &:nth-child(3n + 1) {
- transform: rotate(-1deg);
- }
+ .resource-info {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
- &:nth-child(3n + 2) {
- transform: rotate(1deg);
- }
+ background: rgba(255, 255, 255, 0.6);
+ font-size: 2rem;
+ text-align: center;
+ font-family: "caveat", cursive;
- &:nth-child(3n + 3) {
- transform: rotate(-0.5deg);
- }
+ opacity: 0;
- &:hover {
- transform: rotate(0deg) translateY(-5px) !important;
- box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2);
- z-index: 10;
-
- .pin-tack {
- transform: translateY(-2px);
+ &:hover {
+ opacity: 1;
}
}
}
-// Pin tack at the top
-.pin-tack {
- position: absolute;
- top: -15px;
- left: 50%;
- transform: translateX(-50%);
- width: 20px;
- height: 20px;
- background: radial-gradient(circle at 30% 30%, #ff6b6b, #c92a2a);
- border-radius: 50%;
- box-shadow:
- 0 2px 5px rgba(0, 0, 0, 0.3),
- inset -2px -2px 4px rgba(0, 0, 0, 0.2);
- z-index: 2;
- transition: transform 0.3s ease;
-
- &::after {
- content: "";
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: 6px;
- height: 6px;
- background: #fff;
- border-radius: 50%;
- opacity: 0.5;
- }
-}
-
-// Resource card (the note itself)
-.resource-card {
- background: linear-gradient(135deg, #fef9c3 0%, #fde68a 100%);
- padding: 2rem 1.5rem 1.5rem;
- border-radius: 4px;
- box-shadow:
- 0 4px 8px rgba(0, 0, 0, 0.1),
- inset 0 -40px 40px -20px rgba(0, 0, 0, 0.05);
- min-height: 280px;
- display: flex;
- flex-direction: column;
- gap: 1rem;
-
- // Vary note colors
- .resource-pin:nth-child(4n + 1) & {
- background: linear-gradient(135deg, #fef9c3 0%, #fde68a 100%); // Yellow
- }
-
- .resource-pin:nth-child(4n + 2) & {
- background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%); // Blue
- }
-
- .resource-pin:nth-child(4n + 3) & {
- background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%); // Green
- }
-
- .resource-pin:nth-child(4n + 4) & {
- background: linear-gradient(135deg, #fce7f3 0%, #fbcfe8 100%); // Pink
- }
-}
-
.resource-icon {
- width: 80px;
- height: 80px;
+ width: 100px;
+ height: 100px;
margin: 0 auto 1rem;
position: relative;
@@ -190,13 +492,12 @@
180deg,
transparent 0%,
transparent 20%,
- #9333ea 20%,
- #7c3aed 100%
+ #000 20%,
+ #000 100%
);
border-radius: 10px 10px 40px 40px;
- border: 3px solid #6b21a8;
+ border: 3px solid #000;
position: relative;
- box-shadow: inset 0 0 20px rgba(147, 51, 234, 0.3);
// Lamp base
&::before {
@@ -205,7 +506,7 @@
bottom: -8px;
left: 50%;
transform: translateX(-50%);
- width: 90%;
+ width: 70%;
height: 12px;
background: #1f2937;
border-radius: 4px;
@@ -219,16 +520,15 @@
left: 30%;
width: 25px;
height: 30px;
- background: radial-gradient(circle, #ec4899 0%, #db2777 100%);
+ background: #fff;
border-radius: 50% 50% 40% 60%;
- opacity: 0.9;
animation: float-blob 4s ease-in-out infinite;
}
}
// Last.fm stats icon
&.lastfm-stats {
- background: linear-gradient(135deg, #d32323 0%, #b71c1c 100%);
+ background: black;
border-radius: 8px;
display: flex;
align-items: center;
@@ -261,44 +561,6 @@
}
}
-// Additional lava blob for lavalamp
-.resource-icon.lavalamp {
- & > span {
- position: absolute;
- top: 50%;
- right: 25%;
- width: 20px;
- height: 25px;
- background: radial-gradient(circle, #f97316 0%, #ea580c 100%);
- border-radius: 60% 40% 50% 50%;
- opacity: 0.85;
- animation: float-blob-2 3.5s ease-in-out infinite;
- }
-
- // Create the blob with a pseudo-element instead
- &:not(:empty)::before {
- // Override for when we add content
- animation: float-blob 4s ease-in-out infinite;
- }
-}
-
-// Last.fm additional bars
-.resource-icon.lastfm-stats {
- & > span {
- position: absolute;
- bottom: 15px;
- width: 8px;
- background: #fff;
- border-radius: 2px;
-
- &:nth-child(1) {
- left: 50px;
- height: 30px;
- animation: equalizer-3 0.8s ease-in-out infinite 0.4s;
- }
- }
-}
-
@keyframes float-blob {
0%,
100% {
@@ -309,16 +571,6 @@
}
}
-@keyframes float-blob-2 {
- 0%,
- 100% {
- transform: translateY(0) scale(1);
- }
- 50% {
- transform: translateY(-20px) scale(0.9);
- }
-}
-
@keyframes equalizer-1 {
0%,
100% {
@@ -339,112 +591,9 @@
}
}
-@keyframes equalizer-3 {
- 0%,
- 100% {
- height: 30px;
- }
- 50% {
- height: 15px;
- }
-}
-
-.resource-info {
- flex: 1;
- display: flex;
- flex-direction: column;
-
- h2 {
- font-size: 1.5rem;
- margin: 0 0 0.5rem 0;
- color: #1f2937;
- font-weight: 600;
-
- a {
- color: inherit;
- text-decoration: none;
- transition: color 0.2s ease;
-
- &:hover {
- color: #3b82f6;
- }
- }
- }
-}
-
-.resource-description {
- color: #4b5563;
- font-size: 0.95rem;
- line-height: 1.5;
- margin: 0 0 1rem 0;
- flex: 1;
-}
-
-.resource-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- margin-bottom: 1rem;
-
- .tag {
- background: rgba(0, 0, 0, 0.1);
- padding: 0.25rem 0.75rem;
- border-radius: 12px;
- font-size: 0.8rem;
- color: #374151;
- text-decoration: none;
- transition: background 0.2s ease;
-
- &:hover {
- background: rgba(0, 0, 0, 0.2);
- }
- }
-}
-
-.resource-links {
- display: flex;
- gap: 0.75rem;
- margin-top: auto;
-
- .resource-link {
- flex: 1;
- padding: 0.5rem 1rem;
- text-align: center;
- text-decoration: none;
- border-radius: 4px;
- font-weight: 500;
- font-size: 0.9rem;
- transition: all 0.2s ease;
-
- &.demo {
- background: #3b82f6;
- color: #fff;
-
- &:hover {
- background: #2563eb;
- transform: translateY(-2px);
- box-shadow: 0 4px 8px rgba(59, 130, 246, 0.4);
- }
- }
-
- &.source {
- background: #1f2937;
- color: #fff;
-
- &:hover {
- background: #111827;
- transform: translateY(-2px);
- box-shadow: 0 4px 8px rgba(31, 41, 55, 0.4);
- }
- }
- }
-}
-
// Single Resource Page
.resource-single {
- max-width: 900px;
margin: 0 auto;
- padding: 2rem 1rem;
@include media-up(md) {
padding: 3rem 2rem;
@@ -452,10 +601,113 @@
}
.resource-content {
- background: #fff;
+ max-width: 900px;
+ margin: auto;
+ background:
+ linear-gradient(
+ 145deg,
+ rgba(10, 20, 40, 0.95) 0%,
+ rgba(15, 25, 45, 0.98) 100%
+ )
+ padding-box,
+ linear-gradient(
+ 135deg,
+ rgba(0, 150, 255, 0.5),
+ rgba(255, 120, 0, 0.3),
+ rgba(0, 150, 255, 0.5)
+ )
+ border-box;
border-radius: 8px;
+ border: 2px solid transparent;
padding: 3rem 2rem;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ box-shadow:
+ 0 0 40px rgba(0, 150, 255, 0.2),
+ 0 0 80px rgba(255, 120, 0, 0.1),
+ inset 0 0 100px rgba(0, 100, 200, 0.05),
+ 0 8px 32px rgba(0, 0, 0, 0.4);
+ position: relative;
+ overflow: hidden;
+
+ // Subtle tech panel grid pattern
+ &::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-image:
+ linear-gradient(rgba(0, 150, 255, 0.03) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(0, 150, 255, 0.03) 1px, transparent 1px);
+ background-size: 50px 50px;
+ pointer-events: none;
+ opacity: 0.3;
+ border-radius: 8px;
+ }
+
+ // Corner accents (Aperture Science style)
+ &::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background:
+ radial-gradient(
+ circle at top left,
+ rgba(0, 150, 255, 0.15) 0%,
+ transparent 30%
+ ),
+ radial-gradient(
+ circle at bottom right,
+ rgba(255, 120, 0, 0.15) 0%,
+ transparent 30%
+ );
+ pointer-events: none;
+ }
+
+ // Animated energy particles on border
+ .energy-particles {
+ position: absolute;
+ inset: -2px;
+ border-radius: 8px;
+ pointer-events: none;
+ z-index: 1;
+ overflow: hidden;
+
+ &::before,
+ &::after {
+ content: "";
+ position: absolute;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ filter: blur(4px);
+ }
+
+ // Blue particle
+ &::before {
+ background: radial-gradient(
+ circle,
+ rgba(0, 150, 255, 0.8) 0%,
+ transparent 70%
+ );
+ box-shadow: 0 0 15px rgba(0, 150, 255, 0.8);
+ animation: particle-travel-blue 8s linear infinite;
+ }
+
+ // Orange particle
+ &::after {
+ background: radial-gradient(
+ circle,
+ rgba(255, 120, 0, 0.8) 0%,
+ transparent 70%
+ );
+ box-shadow: 0 0 15px rgba(255, 120, 0, 0.8);
+ animation: particle-travel-orange 8s linear infinite 4s;
+ }
+ }
@include media-up(md) {
padding: 4rem 3rem;
@@ -466,70 +718,294 @@
text-align: center;
margin-bottom: 3rem;
padding-bottom: 2rem;
- border-bottom: 2px solid #e5e7eb;
+ border-bottom: 2px solid;
+ border-image: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(0, 150, 255, 0.6),
+ rgba(255, 120, 0, 0.6),
+ transparent
+ )
+ 1;
+ position: relative;
+ z-index: 1;
h1 {
font-size: 2.5rem;
- color: #1f2937;
+ color: rgba(240, 245, 250, 0.95);
margin: 1rem 0;
+ font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.15em;
+ text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
+ position: relative;
+ display: inline-block;
+
+ // Blue line on the left
+ &::before {
+ content: "";
+ position: absolute;
+ left: -4rem;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 3rem;
+ height: 3px;
+ background: linear-gradient(90deg, transparent, #0096ff);
+ box-shadow: 0 0 10px rgba(0, 150, 255, 0.8);
+ }
+
+ // Orange line on the right
+ &::after {
+ content: "";
+ position: absolute;
+ right: -4rem;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 3rem;
+ height: 3px;
+ background: linear-gradient(90deg, #ff7800, transparent);
+ box-shadow: 0 0 10px rgba(255, 120, 0, 0.8);
+ }
@include media-up(md) {
font-size: 3rem;
+
+ &::before,
+ &::after {
+ width: 5rem;
+ }
+
+ &::before {
+ left: -6rem;
+ }
+
+ &::after {
+ right: -6rem;
+ }
}
}
.lead {
font-size: 1.25rem;
- color: #6b7280;
+ color: rgba(150, 200, 255, 0.9);
margin: 1rem 0;
- }
-}
-
-.resource-icon-large {
- width: 120px;
- height: 120px;
- margin: 0 auto;
-
- &.lavalamp,
- &.lastfm-stats {
- // Inherit styles from smaller icons
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ font-weight: 300;
+ text-shadow: 0 0 10px rgba(0, 150, 255, 0.3);
}
}
.resource-body {
- color: #374151;
+ color: rgba(200, 220, 255, 0.95);
line-height: 1.8;
font-size: 1.1rem;
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ position: relative;
+ z-index: 1;
h2 {
- color: #1f2937;
+ color: #0096ff;
margin-top: 2rem;
margin-bottom: 1rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ font-weight: 600;
+ text-shadow: 0 0 15px rgba(0, 150, 255, 0.5);
+ border-bottom: 2px solid;
+ border-image: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(0, 150, 255, 0.5),
+ transparent
+ )
+ 1;
+ padding-bottom: 0.5rem;
+ padding-left: 1rem;
+ position: relative;
+
+ &::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 4px;
+ height: 70%;
+ background: linear-gradient(
+ 180deg,
+ rgba(0, 150, 255, 0.8),
+ rgba(255, 120, 0, 0.6)
+ );
+ box-shadow: 0 0 10px rgba(0, 150, 255, 0.6);
+ }
+ }
+
+ h3 {
+ color: rgba(255, 140, 50, 0.9);
+ margin-top: 1.5rem;
+ margin-bottom: 0.75rem;
+ text-transform: uppercase;
+ font-size: 1rem;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-shadow: 0 0 10px rgba(255, 120, 0, 0.4);
}
p {
margin-bottom: 1rem;
}
+ a {
+ color: #0096ff;
+ text-decoration: none;
+ border-bottom: 1px solid rgba(0, 150, 255, 0.3);
+ transition: all 0.3s ease;
+ position: relative;
+
+ &:hover {
+ color: #ff7800;
+ border-bottom-color: rgba(255, 120, 0, 0.6);
+ text-shadow: 0 0 8px rgba(0, 150, 255, 0.5);
+ }
+ }
+
+ ul,
+ ol {
+ margin-bottom: 1rem;
+ padding-left: 2rem;
+
+ li {
+ margin-bottom: 0.5rem;
+ position: relative;
+
+ &::marker {
+ color: rgba(0, 150, 255, 0.6);
+ }
+ }
+ }
+
+ strong {
+ color: #fff;
+ font-weight: 600;
+ text-shadow: 0 0 5px rgba(0, 150, 255, 0.3);
+ }
+
code {
- background: #f3f4f6;
- padding: 0.2rem 0.4rem;
+ background: rgba(0, 100, 180, 0.15);
+ padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.9em;
+ color: #66ccff;
+ border: 1px solid rgba(0, 150, 255, 0.3);
+ font-family: "Consolas", "Monaco", monospace;
+ box-shadow: 0 0 10px rgba(0, 150, 255, 0.1);
}
pre {
- background: #1f2937;
- color: #e5e7eb;
+ background: linear-gradient(
+ 145deg,
+ rgba(5, 15, 30, 0.9) 0%,
+ rgba(10, 20, 40, 0.95) 100%
+ );
+ color: #66ccff;
padding: 1.5rem;
- border-radius: 8px;
+ border-radius: 6px;
overflow-x: auto;
margin: 1.5rem 0;
+ border: 1px solid rgba(0, 150, 255, 0.3);
+ box-shadow:
+ inset 0 0 20px rgba(0, 100, 200, 0.1),
+ 0 4px 20px rgba(0, 0, 0, 0.3);
+ position: relative;
+
+ &::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 4px;
+ height: 100%;
+ background: linear-gradient(
+ 180deg,
+ rgba(0, 150, 255, 0.6),
+ rgba(255, 120, 0, 0.4)
+ );
+ }
code {
background: none;
padding: 0;
color: inherit;
+ border: none;
+ box-shadow: none;
+ }
+ }
+
+ // Code copy button
+ .code-copy-btn {
+ position: absolute;
+ top: 0.75rem;
+ right: 0.75rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 0.75rem;
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 100, 180, 0.3) 0%,
+ rgba(0, 80, 150, 0.2) 100%
+ );
+ color: #66ccff;
+ border: 1px solid rgba(0, 150, 255, 0.4);
+ border-radius: 4px;
+ font-size: 0.85rem;
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ z-index: 10;
+
+ svg {
+ width: 14px;
+ height: 14px;
+ transition: transform 0.2s ease;
+ }
+
+ &:hover {
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 120, 200, 0.4) 0%,
+ rgba(255, 100, 0, 0.25) 100%
+ );
+ border-color: rgba(0, 150, 255, 0.7);
+ box-shadow: 0 0 15px rgba(0, 150, 255, 0.3);
+ color: #fff;
+ transform: translateY(-1px);
+
+ svg {
+ transform: scale(1.1);
+ }
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+
+ &.copied {
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 200, 150, 0.4) 0%,
+ rgba(0, 150, 100, 0.3) 100%
+ );
+ border-color: rgba(0, 255, 180, 0.6);
+ color: #66ffcc;
+
+ svg {
+ transform: scale(0.9);
+ }
}
}
}
@@ -540,30 +1016,89 @@
align-items: center;
margin-top: 3rem;
padding-top: 2rem;
- border-top: 2px solid #e5e7eb;
+ border-top: 2px solid;
+ border-image: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(0, 150, 255, 0.4),
+ rgba(255, 120, 0, 0.4),
+ transparent
+ )
+ 1;
gap: 1rem;
flex-wrap: wrap;
+ position: relative;
+ z-index: 1;
.nav-link {
padding: 0.75rem 1.5rem;
- background: #f3f4f6;
- color: #374151;
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 100, 180, 0.2) 0%,
+ rgba(0, 80, 150, 0.15) 100%
+ );
+ color: #66ccff;
text-decoration: none;
- border-radius: 6px;
+ border-radius: 4px;
font-weight: 500;
- transition: all 0.2s ease;
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ border: 1px solid rgba(0, 150, 255, 0.4);
+ transition: all 0.3s ease;
+ text-transform: uppercase;
+ font-size: 0.85rem;
+ letter-spacing: 0.08em;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(0, 150, 255, 0.2),
+ transparent
+ );
+ transition: left 0.5s ease;
+ }
&:hover {
- background: #e5e7eb;
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 120, 200, 0.3) 0%,
+ rgba(255, 100, 0, 0.2) 100%
+ );
+ border-color: rgba(0, 150, 255, 0.7);
+ box-shadow:
+ 0 0 20px rgba(0, 150, 255, 0.4),
+ 0 0 30px rgba(255, 120, 0, 0.2);
+ color: #fff;
+ text-shadow: 0 0 10px rgba(0, 150, 255, 0.6);
transform: translateY(-2px);
+
+ &::before {
+ left: 100%;
+ }
}
&.back {
- background: #3b82f6;
- color: #fff;
+ background: linear-gradient(
+ 135deg,
+ rgba(255, 100, 0, 0.2) 0%,
+ rgba(200, 80, 0, 0.15) 100%
+ );
+ border-color: rgba(255, 120, 0, 0.5);
+ color: #ffaa66;
&:hover {
- background: #2563eb;
+ border-color: rgba(255, 120, 0, 0.8);
+ box-shadow:
+ 0 0 20px rgba(255, 120, 0, 0.4),
+ 0 0 30px rgba(0, 150, 255, 0.2);
}
}
@@ -574,3 +1109,756 @@
}
}
}
+
+.resource-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ justify-content: center;
+ margin: 1rem 0;
+ position: relative;
+ z-index: 1;
+
+ .tag {
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 100, 180, 0.2) 0%,
+ rgba(0, 80, 150, 0.1) 100%
+ );
+ padding: 0.35rem 0.85rem;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ color: rgba(100, 200, 255, 0.9);
+ text-decoration: none;
+ transition: all 0.3s ease;
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ border: 1px solid rgba(0, 150, 255, 0.3);
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ font-weight: 500;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 2px;
+ height: 100%;
+ background: linear-gradient(
+ 180deg,
+ rgba(0, 150, 255, 0.6),
+ rgba(255, 120, 0, 0.4)
+ );
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ &:hover {
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 120, 200, 0.3) 0%,
+ rgba(255, 100, 0, 0.2) 100%
+ );
+ border-color: rgba(0, 150, 255, 0.6);
+ color: #fff;
+ box-shadow: 0 0 15px rgba(0, 150, 255, 0.3);
+ transform: translateY(-1px);
+
+ &::before {
+ opacity: 1;
+ }
+ }
+ }
+}
+
+// Contact section styling for resources
+.contact-section {
+ margin: 3rem 0;
+ padding: 2rem;
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 100, 180, 0.15) 0%,
+ rgba(0, 80, 150, 0.1) 100%
+ );
+ border-radius: 6px;
+ border: 1px solid rgba(0, 150, 255, 0.3);
+ box-shadow:
+ 0 0 20px rgba(0, 150, 255, 0.1),
+ inset 0 0 40px rgba(0, 100, 200, 0.05);
+ position: relative;
+ z-index: 1;
+
+ &::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 4px;
+ background: linear-gradient(
+ 180deg,
+ rgba(0, 150, 255, 0.7),
+ rgba(255, 120, 0, 0.5)
+ );
+ border-radius: 6px 0 0 6px;
+ }
+
+ .contact-title {
+ color: #0096ff;
+ font-size: 1.8rem;
+ font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ margin-bottom: 1.5rem;
+ text-shadow: 0 0 15px rgba(0, 150, 255, 0.5);
+ }
+
+ .contact-content {
+ color: rgba(200, 220, 255, 0.95);
+
+ p {
+ margin-bottom: 1.5rem;
+ line-height: 1.6;
+ }
+ }
+
+ .contact-email,
+ .contact-pgp {
+ margin-bottom: 1rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+
+ @include media-up(md) {
+ flex-direction: row;
+ align-items: center;
+ gap: 1rem;
+ }
+ }
+
+ .contact-label {
+ font-weight: 600;
+ color: rgba(100, 180, 255, 0.9);
+ text-transform: uppercase;
+ font-size: 0.85rem;
+ letter-spacing: 0.08em;
+ }
+
+ .contact-email a {
+ color: #66ccff;
+ text-decoration: none;
+ border-bottom: 1px solid rgba(0, 150, 255, 0.3);
+ transition: all 0.3s ease;
+ font-family: "Consolas", "Monaco", monospace;
+
+ &:hover {
+ color: #ff7800;
+ border-bottom-color: rgba(255, 120, 0, 0.6);
+ text-shadow: 0 0 8px rgba(0, 150, 255, 0.5);
+ }
+ }
+
+ .pgp-actions {
+ display: flex;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+ }
+
+ .pgp-button {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 1rem;
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 100, 180, 0.3) 0%,
+ rgba(0, 80, 150, 0.2) 100%
+ );
+ color: #66ccff;
+ border: 1px solid rgba(0, 150, 255, 0.4);
+ border-radius: 4px;
+ font-size: 0.85rem;
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ text-decoration: none;
+
+ .button-icon {
+ font-size: 1.1rem;
+ }
+
+ &:hover {
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 120, 200, 0.4) 0%,
+ rgba(255, 100, 0, 0.25) 100%
+ );
+ border-color: rgba(0, 150, 255, 0.7);
+ box-shadow: 0 0 15px rgba(0, 150, 255, 0.3);
+ color: #fff;
+ transform: translateY(-1px);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+ }
+
+ .copy-feedback {
+ margin-top: 0.5rem;
+ font-size: 0.85rem;
+ font-weight: 500;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+
+ &.show {
+ opacity: 1;
+ }
+
+ &.success {
+ color: #66ffcc;
+ text-shadow: 0 0 10px rgba(0, 255, 180, 0.6);
+ }
+
+ &.error {
+ color: #ff9966;
+ text-shadow: 0 0 10px rgba(255, 120, 0, 0.6);
+ }
+ }
+}
+
+// Last.fm Stats Interactive Form
+#lastfm-stats-app {
+ margin: 2rem 0;
+ padding: 2rem;
+ background: linear-gradient(
+ 145deg,
+ rgba(5, 15, 30, 0.9) 0%,
+ rgba(10, 20, 40, 0.95) 100%
+ );
+ border-radius: 6px;
+ border: 1px solid rgba(0, 150, 255, 0.4);
+ box-shadow:
+ 0 0 30px rgba(0, 150, 255, 0.15),
+ 0 0 50px rgba(255, 120, 0, 0.08),
+ inset 0 0 60px rgba(0, 100, 200, 0.05);
+ position: relative;
+ z-index: 1;
+ overflow: hidden;
+
+ &::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(0, 150, 255, 0.6),
+ rgba(255, 120, 0, 0.6),
+ transparent
+ );
+ }
+}
+
+.stats-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+ position: relative;
+ z-index: 1;
+
+ @include media-up(md) {
+ flex-direction: row;
+ align-items: flex-end;
+ }
+
+ .form-group {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+
+ label {
+ font-weight: 600;
+ color: #66ccff;
+ font-size: 0.8rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ text-shadow: 0 0 8px rgba(0, 150, 255, 0.4);
+ }
+
+ input,
+ select {
+ padding: 0.75rem 1rem;
+ border: 1px solid rgba(0, 150, 255, 0.4);
+ border-radius: 4px;
+ font-size: 1rem;
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ background: rgba(5, 15, 30, 0.7);
+ color: rgba(200, 220, 255, 0.95);
+ transition: all 0.3s ease;
+ box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);
+
+ &:focus {
+ outline: none;
+ border-color: rgba(0, 150, 255, 0.7);
+ box-shadow:
+ 0 0 15px rgba(0, 150, 255, 0.3),
+ inset 0 2px 8px rgba(0, 0, 0, 0.4);
+ background: rgba(5, 15, 30, 0.9);
+ }
+
+ &::placeholder {
+ color: rgba(100, 150, 200, 0.5);
+ }
+ }
+
+ select {
+ cursor: pointer;
+
+ option {
+ background: rgba(5, 15, 30, 0.95);
+ color: rgba(200, 220, 255, 0.95);
+ }
+ }
+ }
+
+ .btn-primary {
+ padding: 0.75rem 2rem;
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 120, 200, 0.3) 0%,
+ rgba(0, 100, 180, 0.2) 100%
+ );
+ color: #66ccff;
+ border: 1px solid rgba(0, 150, 255, 0.5);
+ border-radius: 4px;
+ font-size: 0.95rem;
+ font-weight: 600;
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ align-self: flex-end;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(0, 150, 255, 0.3),
+ transparent
+ );
+ transition: left 0.5s ease;
+ }
+
+ &:hover {
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 150, 255, 0.4) 0%,
+ rgba(255, 100, 0, 0.3) 100%
+ );
+ border-color: rgba(0, 150, 255, 0.8);
+ box-shadow:
+ 0 0 20px rgba(0, 150, 255, 0.4),
+ 0 0 30px rgba(255, 120, 0, 0.2);
+ color: #fff;
+ text-shadow: 0 0 10px rgba(0, 150, 255, 0.6);
+ transform: translateY(-2px);
+
+ &::before {
+ left: 100%;
+ }
+ }
+
+ &:active {
+ transform: translateY(0);
+ box-shadow: 0 0 15px rgba(0, 150, 255, 0.3);
+ }
+ }
+}
+
+.stats-results {
+ position: relative;
+ z-index: 1;
+
+ .loading {
+ text-align: center;
+ padding: 2rem;
+ color: #66ccff;
+ font-size: 1.1rem;
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+
+ p {
+ animation: pulse 1.5s ease-in-out infinite;
+ text-shadow: 0 0 15px rgba(0, 150, 255, 0.6);
+ }
+ }
+
+ #stats-content {
+ h2 {
+ color: #0096ff;
+ margin-bottom: 1.5rem;
+ font-size: 1.8rem;
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ text-shadow: 0 0 15px rgba(0, 150, 255, 0.5);
+ border-bottom: 2px solid;
+ border-image: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(0, 150, 255, 0.6),
+ transparent
+ )
+ 1;
+ padding-bottom: 0.75rem;
+ }
+ }
+
+ .stat-box {
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 100, 180, 0.15) 0%,
+ rgba(0, 80, 150, 0.1) 100%
+ );
+ padding: 1.5rem;
+ border-radius: 6px;
+ margin-bottom: 1.5rem;
+ border: 1px solid rgba(0, 150, 255, 0.3);
+ box-shadow:
+ 0 0 20px rgba(0, 150, 255, 0.1),
+ inset 0 0 40px rgba(0, 100, 200, 0.05);
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 3px;
+ height: 100%;
+ background: linear-gradient(
+ 180deg,
+ rgba(0, 150, 255, 0.7),
+ rgba(255, 120, 0, 0.5)
+ );
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ &:hover {
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 120, 200, 0.25) 0%,
+ rgba(255, 100, 0, 0.15) 100%
+ );
+ border-color: rgba(0, 150, 255, 0.5);
+ box-shadow:
+ 0 0 30px rgba(0, 150, 255, 0.2),
+ 0 0 40px rgba(255, 120, 0, 0.1);
+
+ &::before {
+ opacity: 1;
+ }
+ }
+
+ h3 {
+ color: rgba(100, 180, 255, 0.8);
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ margin-bottom: 0.75rem;
+ font-weight: 600;
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ text-shadow: 0 0 8px rgba(0, 150, 255, 0.4);
+ }
+
+ .stat-number {
+ font-size: 3rem;
+ font-weight: 700;
+ color: #66ccff;
+ margin: 0;
+ font-family: "DSEG7-Classic", "Segoe UI", monospace;
+ text-shadow:
+ 0 0 20px rgba(0, 150, 255, 0.8),
+ 0 0 40px rgba(0, 150, 255, 0.4);
+ letter-spacing: 0.08em;
+ background: linear-gradient(135deg, #66ccff 0%, #0096ff 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ filter: drop-shadow(0 0 15px rgba(0, 150, 255, 0.5));
+ }
+ }
+
+ .artist-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+
+ li {
+ padding: 0.75rem 0;
+ padding-left: 1rem;
+ border-bottom: 1px solid rgba(0, 150, 255, 0.2);
+ font-size: 1.05rem;
+ color: rgba(200, 220, 255, 0.9);
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ position: relative;
+ transition: all 0.3s ease;
+
+ &::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 3px;
+ height: 60%;
+ background: linear-gradient(
+ 180deg,
+ rgba(0, 150, 255, 0.6),
+ rgba(255, 120, 0, 0.4)
+ );
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ &:hover {
+ padding-left: 1.2rem;
+
+ &::before {
+ opacity: 1;
+ }
+ }
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ a {
+ color: #66ccff;
+ text-decoration: none;
+ font-weight: 500;
+ transition: all 0.3s ease;
+ border-bottom: 1px solid rgba(0, 150, 255, 0.2);
+
+ &:hover {
+ color: #ff7800;
+ border-bottom-color: rgba(255, 120, 0, 0.5);
+ text-shadow: 0 0 10px rgba(0, 150, 255, 0.5);
+ }
+ }
+ }
+ }
+
+ .export-buttons {
+ display: flex;
+ gap: 1rem;
+ margin-top: 2rem;
+ padding-top: 1.5rem;
+ border-top: 2px solid;
+ border-image: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(0, 150, 255, 0.3),
+ transparent
+ )
+ 1;
+ flex-wrap: wrap;
+
+ @include media-up(md) {
+ flex-wrap: nowrap;
+ }
+
+ .btn-export {
+ flex: 1;
+ padding: 0.75rem 1.5rem;
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 100, 180, 0.2) 0%,
+ rgba(0, 80, 150, 0.15) 100%
+ );
+ color: #66ccff;
+ border: 1px solid rgba(0, 150, 255, 0.4);
+ border-radius: 4px;
+ font-size: 0.85rem;
+ font-weight: 600;
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ min-width: 150px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(0, 150, 255, 0.2),
+ transparent
+ );
+ transition: left 0.5s ease;
+ }
+
+ &:hover {
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 120, 200, 0.3) 0%,
+ rgba(255, 100, 0, 0.2) 100%
+ );
+ border-color: rgba(0, 150, 255, 0.6);
+ transform: translateY(-2px);
+ box-shadow: 0 0 20px rgba(0, 150, 255, 0.3);
+ color: #fff;
+ text-shadow: 0 0 8px rgba(0, 150, 255, 0.5);
+
+ &::before {
+ left: 100%;
+ }
+ }
+
+ &:active {
+ transform: translateY(0);
+ box-shadow: 0 0 15px rgba(0, 150, 255, 0.2);
+ }
+
+ &.copied {
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 200, 150, 0.3) 0%,
+ rgba(0, 150, 100, 0.2) 100%
+ );
+ border-color: rgba(0, 255, 180, 0.6);
+ color: #66ffcc;
+ text-shadow: 0 0 10px rgba(0, 255, 180, 0.6);
+ box-shadow: 0 0 20px rgba(0, 255, 180, 0.3);
+ }
+ }
+ }
+
+ .error {
+ background: linear-gradient(
+ 135deg,
+ rgba(180, 50, 0, 0.2) 0%,
+ rgba(150, 30, 0, 0.15) 100%
+ );
+ border: 1px solid rgba(255, 80, 0, 0.5);
+ border-radius: 6px;
+ padding: 1.5rem;
+ color: #ff9966;
+ text-align: center;
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ box-shadow:
+ 0 0 20px rgba(255, 80, 0, 0.2),
+ inset 0 0 40px rgba(255, 50, 0, 0.1);
+ position: relative;
+
+ &::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 4px;
+ background: linear-gradient(
+ 180deg,
+ rgba(255, 120, 0, 0.8),
+ rgba(255, 80, 0, 0.6)
+ );
+ }
+
+ p {
+ margin: 0;
+ font-weight: 500;
+ text-shadow: 0 0 10px rgba(255, 120, 0, 0.5);
+ }
+ }
+}
+
+@keyframes pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+// Energy particle animations - travel around the border
+@keyframes particle-travel-blue {
+ 0% {
+ top: -10px;
+ left: 0;
+ }
+ 25% {
+ top: -10px;
+ left: 100%;
+ }
+ 50% {
+ top: 100%;
+ left: 100%;
+ }
+ 75% {
+ top: 100%;
+ left: -10px;
+ }
+ 100% {
+ top: -10px;
+ left: -10px;
+ }
+}
+
+@keyframes particle-travel-orange {
+ 0% {
+ top: -10px;
+ left: 100%;
+ }
+ 25% {
+ top: 100%;
+ left: 100%;
+ }
+ 50% {
+ top: 100%;
+ left: -10px;
+ }
+ 75% {
+ top: -10px;
+ left: -10px;
+ }
+ 100% {
+ top: -10px;
+ left: 100%;
+ }
+}
diff --git a/assets/sass/style.scss b/assets/sass/style.scss
index bdde189..b5a53f8 100644
--- a/assets/sass/style.scss
+++ b/assets/sass/style.scss
@@ -26,7 +26,7 @@
@import "pages/media";
@import "pages/resources";
-@import url("https://fonts.bunny.net/css2?family=Caveat:wght@400..700&family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&family=Neonderthaw&display=swap");
+@import url(https://fonts.bunny.net/css?family=abel:400|barlow-condensed:400,500|caveat:400|lato:300,300i,400,400i|neonderthaw:400);
@font-face {
font-family: "DSEG7-Classic";
diff --git a/content/resources/_index.md b/content/resources/_index.md
index 8a2654c..28c733f 100644
--- a/content/resources/_index.md
+++ b/content/resources/_index.md
@@ -3,6 +3,6 @@ title: "Resources"
draft: false
---
-# Resources
+Welcome to my test chamber, here you'll find various experiements, tests and resources. Take a look around.
-Welcome to my whiteboard of resources. Here you'll find various tools, scripts, and experiments I've built and wanted to share.
+The cake isn't a lie.
diff --git a/content/resources/lastfm-stats/index.md b/content/resources/lastfm-stats/index.md
index b33e843..d4b199b 100644
--- a/content/resources/lastfm-stats/index.md
+++ b/content/resources/lastfm-stats/index.md
@@ -1,7 +1,6 @@
---
-title: "Last.fm Weekly Stats Script"
+title: "Last.fm Weekly Stats"
date: 2026-01-04
-tags: ["javascript", "api", "last.fm"]
description: "Fetch and display your weekly listening stats from Last.fm"
icon: "lastfm-stats"
demo_url: ""
@@ -9,26 +8,115 @@ source_url: ""
draft: false
---
-A handy script for pulling your weekly listening statistics from Last.fm's API. Perfect for tracking your music habits and creating weekly listening reports.
+Get your weekly listening statistics from Last.fm's API. Enter your username below to see your top artists and track counts for different time periods.
-## Features
+I made this so I could easily add in listening stats to my [weekly posts](/tags/weekly-update/), if you find it useful please let me know.
-- Fetches weekly top tracks from Last.fm
-- Displays track count and listening time
-- Formats data in a clean, readable format
-- Easy to integrate into blogs or dashboards
+{{< lastfm-stats-form >}}
-## Setup
+## Download the Shell Script
-1. Get your Last.fm API key from [Last.fm API](https://www.last.fm/api)
-2. Configure your username in the script
-3. Run the script to fetch your weekly stats
+Want to run this locally or integrate it into your own workflows? Here is a bash script I was using to generate this before I decided to make a web version.
-## Output
+```
+#!/bin/bash
-The script returns your top tracks for the week, including:
-- Track name and artist
-- Play count
-- Listening duration
+# Last.fm Weekly Stats Script
+# Fetches your Last.fm listening statistics for the past week
+#
+# Requirements:
+# - curl (for API requests)
+# - jq (for JSON parsing)
+#
+# Usage: ./lastfm-week.sh
+#
+# Setup:
+# Create a .env file with:
+# LASTFM_API_KEY=your_api_key_here
+# LASTFM_USERNAME=your_username_here
+#
+# Output: Markdown-formatted stats with top artists and track counts
+#
+# Download from: https://ritual.sh/resources/lastfm-stats/
-Great for weekly blog posts or personal music tracking!
+# Load environment variables from .env file
+if [ -f .env ]; then
+ export $(cat .env | grep -v '^#' | xargs)
+else
+ echo "Error: .env file not found"
+ exit 1
+fi
+
+# Check required variables
+if [ -z "$LASTFM_API_KEY" ] || [ -z "$LASTFM_USERNAME" ]; then
+ echo "Error: LASTFM_API_KEY and LASTFM_USERNAME must be set in .env file"
+ exit 1
+fi
+
+API_BASE="http://ws.audioscrobbler.com/2.0/"
+
+# Get current timestamp
+NOW=$(date +%s)
+# Get timestamp from 7 days ago
+WEEK_AGO=$((NOW - 604800))
+
+# Fetch top artists for the week
+TOP_ARTISTS=$(curl -s "${API_BASE}?method=user.gettopartists&user=${LASTFM_USERNAME}&api_key=${LASTFM_API_KEY}&format=json&period=7day&limit=5")
+
+# Fetch recent tracks to count this week's scrobbles
+RECENT_TRACKS=$(curl -s "${API_BASE}?method=user.getrecenttracks&user=${LASTFM_USERNAME}&api_key=${LASTFM_API_KEY}&format=json&from=${WEEK_AGO}&to=${NOW}&limit=1")
+
+# Get total track count
+TOTAL_TRACKS=$(echo "$RECENT_TRACKS" | jq -r '.recenttracks["@attr"].total')
+
+# Output in markdown format
+echo "## Last.fm Weekly Stats"
+echo ""
+echo "**Total Tracks:** ${TOTAL_TRACKS}"
+echo ""
+echo "**Top 5 Artists:**"
+echo ""
+
+# Parse and display top 5 artists as markdown links
+echo "$TOP_ARTISTS" | jq -r '.topartists.artist[] | "- [\(.name)](\(.url)) - \(.playcount) plays"'
+```
+
+### Shell Script Usage
+
+The script fetches your Last.fm stats and outputs them in markdown format.
+
+**Requirements:**
+
+- `curl` for API requests
+- `jq` for JSON parsing
+
+**Setup:**
+
+1. Create a `.env` file with your credentials:
+
+```bash
+LASTFM_API_KEY=your_api_key_here
+LASTFM_USERNAME=your_username_here
+```
+
+2. Make the script executable:
+
+```bash
+chmod +x lastfm-week.sh
+```
+
+3. Run it:
+
+```bash
+./lastfm-week.sh
+```
+
+**Output:**
+
+The script prints markdown-formatted stats including:
+
+- Total tracks scrobbled this week
+- Top 5 artists with play counts
+- Direct links to artist pages
+
+Enjoy!
diff --git a/content/resources/lavalamp/index.md b/content/resources/lavalamp/index.md
index 1660430..e18dc73 100644
--- a/content/resources/lavalamp/index.md
+++ b/content/resources/lavalamp/index.md
@@ -1,12 +1,11 @@
---
title: "HTML/CSS Lavalamp"
date: 2026-01-04
-tags: ["css", "html", "animation"]
description: "A pure CSS lavalamp animation with floating blobs"
icon: "lavalamp"
demo_url: ""
source_url: ""
-draft: false
+draft: true
---
A mesmerizing lavalamp effect created entirely with HTML and CSS. Features smooth floating animations and gradient blobs that rise and fall like a classic lava lamp.
diff --git a/lastfm-week.sh b/lastfm-week.sh
index c208320..1d64688 100755
--- a/lastfm-week.sh
+++ b/lastfm-week.sh
@@ -1,7 +1,22 @@
#!/bin/bash
# Last.fm Weekly Stats Script
-# Usage: ./lastfm-weekly.sh
+# Fetches your Last.fm listening statistics for the past week
+#
+# Requirements:
+# - curl (for API requests)
+# - jq (for JSON parsing)
+#
+# Usage: ./lastfm-week.sh
+#
+# Setup:
+# Create a .env file with:
+# LASTFM_API_KEY=your_api_key_here
+# LASTFM_USERNAME=your_username_here
+#
+# Output: Markdown-formatted stats with top artists and track counts
+#
+# Download from: https://ritual.sh/resources/lastfm-stats/
# Load environment variables from .env file
if [ -f .env ]; then
diff --git a/layouts/blog/single.html b/layouts/blog/single.html
index 1a9cec6..b8ea142 100644
--- a/layouts/blog/single.html
+++ b/layouts/blog/single.html
@@ -23,36 +23,7 @@
{{ .Content }}
-
- Contact
-
-
- If you found this interesting, have any comments, questions,
- corrections, or just fancy saying "hello" please feel free to get
- in touch.
-
-
- Email:
- dan@ritual.sh
-
-
- PGP Public Key:
-
-
- Download
-
-
-
-
-
-
-
+ {{ partial "contact-section.html" . }}