diff --git a/assets/js/button-generator.js b/assets/js/button-generator.js
new file mode 100644
index 0000000..62acb20
--- /dev/null
+++ b/assets/js/button-generator.js
@@ -0,0 +1,594 @@
+(function () {
+ const canvas = document.getElementById("button-canvas");
+ const ctx = canvas.getContext("2d");
+
+ // Preload all web fonts for canvas rendering
+ const fonts = [
+ "Lato",
+ "Roboto",
+ "Open Sans",
+ "Montserrat",
+ "Oswald",
+ "Bebas Neue",
+ "Roboto Mono",
+ "VT323",
+ "Press Start 2P",
+ "DSEG7-Classic",
+ ];
+
+ // Load fonts using CSS Font Loading API
+ const fontPromises = fonts.flatMap((font) => [
+ document.fonts.load(`400 12px "${font}"`),
+ document.fonts.load(`700 12px "${font}"`),
+ document.fonts.load(`italic 400 12px "${font}"`),
+ ]);
+
+ Promise.all(fontPromises).then(() => {
+ console.log("All fonts loaded for canvas");
+ drawButton();
+ });
+
+ // Collapsible sections functionality
+ document.querySelectorAll(".control-group-header").forEach((header) => {
+ header.addEventListener("click", () => {
+ const controlGroup = header.closest(".control-group");
+ controlGroup.classList.toggle("collapsed");
+ });
+ });
+
+ // Get all controls
+ const controls = {
+ text: document.getElementById("button-text"),
+ textEnabled: document.getElementById("text-enabled"),
+ fontSize: document.getElementById("font-size"),
+ textX: document.getElementById("text-x"),
+ textY: document.getElementById("text-y"),
+ textColorType: document.getElementById("text-color-type"),
+ textColor: document.getElementById("text-color"),
+ textGradientColor1: document.getElementById("text-gradient-color1"),
+ textGradientColor2: document.getElementById("text-gradient-color2"),
+ textGradientAngle: document.getElementById("text-gradient-angle"),
+ textOutline: document.getElementById("text-outline"),
+ outlineColor: document.getElementById("outline-color"),
+ fontFamily: document.getElementById("font-family"),
+ fontBold: document.getElementById("font-bold"),
+ fontItalic: document.getElementById("font-italic"),
+ text2: document.getElementById("button-text2"),
+ text2Enabled: document.getElementById("text2-enabled"),
+ fontSize2: document.getElementById("font-size2"),
+ text2X: document.getElementById("text2-x"),
+ text2Y: document.getElementById("text2-y"),
+ text2ColorType: document.getElementById("text2-color-type"),
+ text2Color: document.getElementById("text2-color"),
+ text2GradientColor1: document.getElementById("text2-gradient-color1"),
+ text2GradientColor2: document.getElementById("text2-gradient-color2"),
+ text2GradientAngle: document.getElementById("text2-gradient-angle"),
+ text2Outline: document.getElementById("text2-outline"),
+ outline2Color: document.getElementById("outline2-color"),
+ fontFamily2: document.getElementById("font-family2"),
+ fontBold2: document.getElementById("font-bold2"),
+ fontItalic2: document.getElementById("font-italic2"),
+ bgType: document.getElementById("bg-type"),
+ bgColor: document.getElementById("bg-color"),
+ gradientColor1: document.getElementById("gradient-color1"),
+ gradientColor2: document.getElementById("gradient-color2"),
+ gradientAngle: document.getElementById("gradient-angle"),
+ textureType: document.getElementById("texture-type"),
+ textureColor1: document.getElementById("texture-color1"),
+ textureColor2: document.getElementById("texture-color2"),
+ textureScale: document.getElementById("texture-scale"),
+ borderWidth: document.getElementById("border-width"),
+ borderColor: document.getElementById("border-color"),
+ borderStyle: document.getElementById("border-style"),
+ };
+
+ // Update value displays
+ const updateValueDisplay = (id, value) => {
+ const display = document.getElementById(id + "-value");
+ if (display) display.textContent = value;
+ };
+
+ // Show/hide controls based on background type
+ controls.bgType.addEventListener("change", () => {
+ const solidControls = document.getElementById("solid-controls");
+ const gradientControls = document.getElementById("gradient-controls");
+ const textureControls = document.getElementById("texture-controls");
+
+ solidControls.style.display = "none";
+ gradientControls.style.display = "none";
+ textureControls.style.display = "none";
+
+ if (controls.bgType.value === "solid") {
+ solidControls.style.display = "block";
+ } else if (controls.bgType.value === "gradient") {
+ gradientControls.style.display = "block";
+ } else if (controls.bgType.value === "texture") {
+ textureControls.style.display = "block";
+ }
+
+ drawButton();
+ });
+
+ // Show/hide text color controls
+ controls.textColorType.addEventListener("change", () => {
+ const textSolidColor = document.getElementById("text-solid-color");
+ const textGradientColor = document.getElementById("text-gradient-color");
+
+ if (controls.textColorType.value === "solid") {
+ textSolidColor.style.display = "block";
+ textGradientColor.style.display = "none";
+ } else {
+ textSolidColor.style.display = "none";
+ textGradientColor.style.display = "block";
+ }
+ drawButton();
+ });
+
+ controls.text2ColorType.addEventListener("change", () => {
+ const text2SolidColor = document.getElementById("text2-solid-color");
+ const text2GradientColor = document.getElementById("text2-gradient-color");
+
+ if (controls.text2ColorType.value === "solid") {
+ text2SolidColor.style.display = "block";
+ text2GradientColor.style.display = "none";
+ } else {
+ text2SolidColor.style.display = "none";
+ text2GradientColor.style.display = "block";
+ }
+ drawButton();
+ });
+
+ // Show/hide outline color
+ controls.textOutline.addEventListener("change", () => {
+ controls.outlineColor.style.display = controls.textOutline.checked
+ ? "block"
+ : "none";
+ drawButton();
+ });
+
+ controls.text2Outline.addEventListener("change", () => {
+ controls.outline2Color.style.display = controls.text2Outline.checked
+ ? "block"
+ : "none";
+ drawButton();
+ });
+
+ // Draw texture patterns
+ function drawTexture(type, color1, color2, scale) {
+ const tempCanvas = document.createElement("canvas");
+ tempCanvas.width = 88;
+ tempCanvas.height = 31;
+ const tempCtx = tempCanvas.getContext("2d");
+
+ tempCtx.fillStyle = color1;
+ tempCtx.fillRect(0, 0, 88, 31);
+
+ tempCtx.fillStyle = color2;
+ const size = Math.max(2, Math.floor(scale / 10));
+
+ switch (type) {
+ case "dots":
+ for (let y = 0; y < 31; y += size * 2) {
+ for (let x = 0; x < 88; x += size * 2) {
+ tempCtx.beginPath();
+ tempCtx.arc(x, y, size / 2, 0, Math.PI * 2);
+ tempCtx.fill();
+ }
+ }
+ break;
+ case "grid":
+ for (let x = 0; x < 88; x += size) {
+ tempCtx.fillRect(x, 0, 1, 31);
+ }
+ for (let y = 0; y < 31; y += size) {
+ tempCtx.fillRect(0, y, 88, 1);
+ }
+ break;
+ case "diagonal":
+ for (let i = -31; i < 88; i += size) {
+ tempCtx.fillRect(i, 0, 2, 31);
+ tempCtx.save();
+ tempCtx.translate(i + 1, 0);
+ tempCtx.rotate(Math.PI / 4);
+ tempCtx.fillRect(0, 0, 2, 100);
+ tempCtx.restore();
+ }
+ break;
+ case "checkerboard":
+ for (let y = 0; y < 31; y += size) {
+ for (let x = 0; x < 88; x += size) {
+ if ((x / size + y / size) % 2 === 0) {
+ tempCtx.fillRect(x, y, size, size);
+ }
+ }
+ }
+ break;
+ case "noise":
+ for (let y = 0; y < 31; y++) {
+ for (let x = 0; x < 88; x++) {
+ if (Math.random() > 0.5) {
+ tempCtx.fillRect(x, y, 1, 1);
+ }
+ }
+ }
+ break;
+ case "stars":
+ for (let i = 0; i < scale; i++) {
+ const x = Math.floor(Math.random() * 88);
+ const y = Math.floor(Math.random() * 31);
+ tempCtx.fillRect(x, y, 1, 1);
+ tempCtx.fillRect(x - 1, y, 1, 1);
+ tempCtx.fillRect(x + 1, y, 1, 1);
+ tempCtx.fillRect(x, y - 1, 1, 1);
+ tempCtx.fillRect(x, y + 1, 1, 1);
+ }
+ break;
+ }
+
+ return tempCanvas;
+ }
+
+ // Helper function to draw to a single context
+ function drawToContext(context) {
+ context.clearRect(0, 0, 88, 31);
+
+ // Draw background
+ if (controls.bgType.value === "solid") {
+ context.fillStyle = controls.bgColor.value;
+ context.fillRect(0, 0, 88, 31);
+ } else if (controls.bgType.value === "gradient") {
+ const angle = parseFloat(controls.gradientAngle.value) * (Math.PI / 180);
+ const x1 = 44 + Math.cos(angle) * 44;
+ const y1 = 15.5 + Math.sin(angle) * 15.5;
+ const x2 = 44 - Math.cos(angle) * 44;
+ const y2 = 15.5 - Math.sin(angle) * 15.5;
+
+ const gradient = context.createLinearGradient(x1, y1, x2, y2);
+ gradient.addColorStop(0, controls.gradientColor1.value);
+ gradient.addColorStop(1, controls.gradientColor2.value);
+ context.fillStyle = gradient;
+ context.fillRect(0, 0, 88, 31);
+ } else if (controls.bgType.value === "texture") {
+ const texture = drawTexture(
+ controls.textureType.value,
+ controls.textureColor1.value,
+ controls.textureColor2.value,
+ parseFloat(controls.textureScale.value),
+ );
+ context.drawImage(texture, 0, 0);
+ }
+
+ // Draw border
+ const borderWidth = parseFloat(controls.borderWidth.value);
+ if (borderWidth > 0) {
+ const style = controls.borderStyle.value;
+
+ if (style === "solid") {
+ context.strokeStyle = controls.borderColor.value;
+ context.lineWidth = borderWidth;
+ context.strokeRect(
+ borderWidth / 2,
+ borderWidth / 2,
+ 88 - borderWidth,
+ 31 - borderWidth,
+ );
+ } else if (style === "inset" || style === "outset") {
+ const light = style === "outset";
+ context.strokeStyle = light ? "#ffffff" : "#000000";
+ context.lineWidth = borderWidth;
+ context.beginPath();
+ context.moveTo(0, 31);
+ context.lineTo(0, 0);
+ context.lineTo(88, 0);
+ context.stroke();
+
+ context.strokeStyle = light ? "#000000" : "#ffffff";
+ context.beginPath();
+ context.moveTo(88, 0);
+ context.lineTo(88, 31);
+ context.lineTo(0, 31);
+ context.stroke();
+ } else if (style === "ridge") {
+ context.strokeStyle = "#ffffff";
+ context.lineWidth = borderWidth / 2;
+ context.strokeRect(
+ borderWidth / 4,
+ borderWidth / 4,
+ 88 - borderWidth / 2,
+ 31 - borderWidth / 2,
+ );
+
+ context.strokeStyle = "#000000";
+ context.strokeRect(
+ (borderWidth * 3) / 4,
+ (borderWidth * 3) / 4,
+ 88 - borderWidth * 1.5,
+ 31 - borderWidth * 1.5,
+ );
+ }
+ }
+
+ // Draw text line 1
+ const text = controls.text.value;
+ if (text && controls.textEnabled.checked) {
+ const fontSize = parseFloat(controls.fontSize.value);
+ const fontWeight = controls.fontBold.checked ? "bold" : "normal";
+ const fontStyle = controls.fontItalic.checked ? "italic" : "normal";
+ const fontFamily = controls.fontFamily.value;
+
+ context.font = `${fontStyle} ${fontWeight} ${fontSize}px "${fontFamily}"`;
+ context.textAlign = "center";
+ context.textBaseline = "middle";
+
+ const x = (parseFloat(controls.textX.value) / 100) * 88;
+ const y = (parseFloat(controls.textY.value) / 100) * 31;
+
+ if (controls.textOutline.checked) {
+ context.strokeStyle = controls.outlineColor.value;
+ context.lineWidth = 2;
+ context.strokeText(text, x, y);
+ }
+
+ // Apply text color or gradient
+ if (controls.textColorType.value === "solid") {
+ context.fillStyle = controls.textColor.value;
+ } else {
+ const angle =
+ parseFloat(controls.textGradientAngle.value) * (Math.PI / 180);
+ const textWidth = context.measureText(text).width;
+ const x1 = x - textWidth / 2 + (Math.cos(angle) * textWidth) / 2;
+ const y1 = y - fontSize / 2 + (Math.sin(angle) * fontSize) / 2;
+ const x2 = x + textWidth / 2 - (Math.cos(angle) * textWidth) / 2;
+ const y2 = y + fontSize / 2 - (Math.sin(angle) * fontSize) / 2;
+
+ const textGradient = context.createLinearGradient(x1, y1, x2, y2);
+ textGradient.addColorStop(0, controls.textGradientColor1.value);
+ textGradient.addColorStop(1, controls.textGradientColor2.value);
+ context.fillStyle = textGradient;
+ }
+
+ context.fillText(text, x, y);
+ }
+
+ // Draw text line 2
+ const text2 = controls.text2.value;
+ if (text2 && controls.text2Enabled.checked) {
+ const fontSize2 = parseFloat(controls.fontSize2.value);
+ const fontWeight2 = controls.fontBold2.checked ? "bold" : "normal";
+ const fontStyle2 = controls.fontItalic2.checked ? "italic" : "normal";
+ const fontFamily2 = controls.fontFamily2.value;
+
+ context.font = `${fontStyle2} ${fontWeight2} ${fontSize2}px "${fontFamily2}"`;
+ context.textAlign = "center";
+ context.textBaseline = "middle";
+
+ const x2 = (parseFloat(controls.text2X.value) / 100) * 88;
+ const y2 = (parseFloat(controls.text2Y.value) / 100) * 31;
+
+ if (controls.text2Outline.checked) {
+ context.strokeStyle = controls.outline2Color.value;
+ context.lineWidth = 2;
+ context.strokeText(text2, x2, y2);
+ }
+
+ // Apply text color or gradient
+ if (controls.text2ColorType.value === "solid") {
+ context.fillStyle = controls.text2Color.value;
+ } else {
+ const angle2 =
+ parseFloat(controls.text2GradientAngle.value) * (Math.PI / 180);
+ const text2Width = context.measureText(text2).width;
+ const x1_2 = x2 - text2Width / 2 + (Math.cos(angle2) * text2Width) / 2;
+ const y1_2 = y2 - fontSize2 / 2 + (Math.sin(angle2) * fontSize2) / 2;
+ const x2_2 = x2 + text2Width / 2 - (Math.cos(angle2) * text2Width) / 2;
+ const y2_2 = y2 + fontSize2 / 2 - (Math.sin(angle2) * fontSize2) / 2;
+
+ const text2Gradient = context.createLinearGradient(
+ x1_2,
+ y1_2,
+ x2_2,
+ y2_2,
+ );
+ text2Gradient.addColorStop(0, controls.text2GradientColor1.value);
+ text2Gradient.addColorStop(1, controls.text2GradientColor2.value);
+ context.fillStyle = text2Gradient;
+ }
+
+ context.fillText(text2, x2, y2);
+ }
+ }
+
+ // Main draw function
+ function drawButton() {
+ drawToContext(ctx);
+ }
+
+ // Download function
+ document.getElementById("download-button").addEventListener("click", () => {
+ const link = document.createElement("a");
+ link.download = "button-88x31.png";
+ link.href = canvas.toDataURL();
+ link.click();
+ });
+
+ // Preset buttons
+ document.getElementById("preset-random").addEventListener("click", () => {
+ const randomColor = () =>
+ "#" +
+ Math.floor(Math.random() * 16777215)
+ .toString(16)
+ .padStart(6, "0");
+
+ // Random background
+ controls.bgType.value = ["solid", "gradient", "texture"][
+ Math.floor(Math.random() * 3)
+ ];
+ controls.bgColor.value = randomColor();
+ controls.gradientColor1.value = randomColor();
+ controls.gradientColor2.value = randomColor();
+ controls.gradientAngle.value = Math.floor(Math.random() * 360);
+ controls.textureColor1.value = randomColor();
+ controls.textureColor2.value = randomColor();
+ controls.textureType.value = [
+ "dots",
+ "grid",
+ "diagonal",
+ "checkerboard",
+ "noise",
+ "stars",
+ ][Math.floor(Math.random() * 6)];
+
+ // Random text 1 color (50% chance of gradient)
+ controls.textColorType.value = Math.random() > 0.5 ? "gradient" : "solid";
+ controls.textColor.value = randomColor();
+ controls.textGradientColor1.value = randomColor();
+ controls.textGradientColor2.value = randomColor();
+ controls.textGradientAngle.value = Math.floor(Math.random() * 360);
+
+ // Random text 2 color (50% chance of gradient)
+ controls.text2ColorType.value = Math.random() > 0.5 ? "gradient" : "solid";
+ controls.text2Color.value = randomColor();
+ controls.text2GradientColor1.value = randomColor();
+ controls.text2GradientColor2.value = randomColor();
+ controls.text2GradientAngle.value = Math.floor(Math.random() * 360);
+
+ // Random border
+ controls.borderColor.value = randomColor();
+ controls.borderWidth.value = Math.floor(Math.random() * 6);
+ controls.borderStyle.value = ["solid", "inset", "outset", "ridge"][
+ Math.floor(Math.random() * 4)
+ ];
+
+ // Random text styles
+ controls.fontBold.checked = Math.random() > 0.5;
+ controls.fontItalic.checked = Math.random() > 0.5;
+ controls.fontBold2.checked = Math.random() > 0.5;
+ controls.fontItalic2.checked = Math.random() > 0.5;
+
+ // Update displays
+ updateValueDisplay("gradient-angle", controls.gradientAngle.value);
+ updateValueDisplay("text-gradient-angle", controls.textGradientAngle.value);
+ updateValueDisplay(
+ "text2-gradient-angle",
+ controls.text2GradientAngle.value,
+ );
+ updateValueDisplay("border-width", controls.borderWidth.value);
+
+ controls.bgType.dispatchEvent(new Event("change"));
+ controls.textColorType.dispatchEvent(new Event("change"));
+ controls.text2ColorType.dispatchEvent(new Event("change"));
+ drawButton();
+ });
+
+ document.getElementById("preset-classic").addEventListener("click", () => {
+ // Classic 90s web button style
+ controls.bgType.value = "gradient";
+ controls.gradientColor1.value = "#0066cc";
+ controls.gradientColor2.value = "#0099ff";
+ controls.gradientAngle.value = 90;
+
+ controls.textColorType.value = "solid";
+ controls.textColor.value = "#ffffff";
+ controls.text2ColorType.value = "solid";
+ controls.text2Color.value = "#ffffff";
+
+ controls.borderWidth.value = 2;
+ controls.borderColor.value = "#000000";
+ controls.borderStyle.value = "outset";
+
+ controls.fontFamily.value = "Oswald";
+ controls.fontFamily2.value = "Lato";
+ controls.fontBold.checked = true;
+ controls.fontBold2.checked = false;
+ controls.fontItalic.checked = false;
+ controls.fontItalic2.checked = false;
+
+ controls.text.value = "RITUAL.SH";
+ controls.text2.value = "FREE THE WEB";
+ controls.textEnabled.checked = true;
+ controls.text2Enabled.checked = true;
+ controls.fontSize.value = 12;
+ controls.fontSize2.value = 8;
+ controls.textY.value = 35;
+ controls.text2Y.value = 65;
+
+ updateValueDisplay("font-size", 12);
+ updateValueDisplay("font-size2", 8);
+ updateValueDisplay("gradient-angle", 90);
+ updateValueDisplay("text-y", 35);
+ updateValueDisplay("text2-y", 65);
+
+ controls.bgType.dispatchEvent(new Event("change"));
+ controls.textColorType.dispatchEvent(new Event("change"));
+ controls.text2ColorType.dispatchEvent(new Event("change"));
+ drawButton();
+ });
+
+ document.getElementById("preset-modern").addEventListener("click", () => {
+ // Modern cyberpunk style with gradient text
+ controls.bgType.value = "gradient";
+ controls.gradientColor1.value = "#0a0a0a";
+ controls.gradientColor2.value = "#1a0a2e";
+ controls.gradientAngle.value = 135;
+
+ controls.textColorType.value = "gradient";
+ controls.textGradientColor1.value = "#00ffaa";
+ controls.textGradientColor2.value = "#00ffff";
+ controls.textGradientAngle.value = 90;
+
+ controls.text2ColorType.value = "gradient";
+ controls.text2GradientColor1.value = "#ff00ff";
+ controls.text2GradientColor2.value = "#ff6600";
+ controls.text2GradientAngle.value = 0;
+
+ controls.borderWidth.value = 1;
+ controls.borderColor.value = "#00ffaa";
+ controls.borderStyle.value = "solid";
+
+ controls.fontFamily.value = "Roboto Mono";
+ controls.fontFamily2.value = "Roboto Mono";
+ controls.fontBold.checked = true;
+ controls.fontBold2.checked = false;
+ controls.fontItalic.checked = false;
+ controls.fontItalic2.checked = false;
+
+ controls.text.value = "RITUAL.SH";
+ controls.text2.value = "EST. 2024";
+ controls.textEnabled.checked = true;
+ controls.text2Enabled.checked = true;
+ controls.fontSize.value = 11;
+ controls.fontSize2.value = 9;
+ controls.textY.value = 35;
+ controls.text2Y.value = 65;
+
+ updateValueDisplay("font-size", 11);
+ updateValueDisplay("font-size2", 9);
+ updateValueDisplay("gradient-angle", 135);
+ updateValueDisplay("text-gradient-angle", 90);
+ updateValueDisplay("text2-gradient-angle", 0);
+ updateValueDisplay("text-y", 35);
+ updateValueDisplay("text2-y", 65);
+
+ controls.textColorType.dispatchEvent(new Event("change"));
+ controls.text2ColorType.dispatchEvent(new Event("change"));
+ controls.bgType.dispatchEvent(new Event("change"));
+ drawButton();
+ });
+
+ // Add event listeners to all controls
+ Object.values(controls).forEach((control) => {
+ if (control) {
+ control.addEventListener("input", drawButton);
+ control.addEventListener("change", drawButton);
+
+ if (control.type === "range") {
+ control.addEventListener("input", (e) => {
+ updateValueDisplay(e.target.id, e.target.value);
+ });
+ }
+ }
+ });
+
+ // Initial draw
+ drawButton();
+})();
diff --git a/assets/sass/pages/button-generator.scss b/assets/sass/pages/button-generator.scss
new file mode 100644
index 0000000..a6a17ec
--- /dev/null
+++ b/assets/sass/pages/button-generator.scss
@@ -0,0 +1,503 @@
+@import url(https://fonts.bunny.net/css?family=bebas-neue:400|lato:400,400i,700,700i|montserrat:400,400i,700,700i|open-sans:400,400i,700,700i|oswald:400,700|press-start-2p:400|roboto:400,400i,700,700i|roboto-mono:400,400i,700,700i|vt323:400);
+
+#button-generator-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;
+ overflow: visible;
+
+ &::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
+ );
+ }
+
+ .generator-container {
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ gap: 2rem;
+ margin-top: 1rem;
+ position: relative;
+ z-index: 1;
+
+ @media (max-width: 768px) {
+ grid-template-columns: 1fr;
+ }
+ }
+
+ .preview-section {
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 100, 180, 0.15) 0%,
+ rgba(0, 80, 150, 0.1) 100%
+ );
+ padding: 1.5rem;
+ border-radius: 6px;
+ border: 1px solid rgba(0, 150, 255, 0.3);
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ position: sticky;
+ top: 1rem;
+ align-self: flex-start;
+ max-height: calc(100vh - 2rem);
+ overflow-y: auto;
+ box-shadow:
+ 0 0 20px rgba(0, 150, 255, 0.1),
+ inset 0 0 40px rgba(0, 100, 200, 0.05);
+
+ &::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;
+ }
+
+ h3 {
+ margin-top: 0;
+ color: #0096ff;
+ font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ text-shadow: 0 0 15px rgba(0, 150, 255, 0.5);
+ font-size: 1.2rem;
+ }
+ }
+
+ .presets-container {
+ margin-top: 1.5rem;
+ padding-top: 1.5rem;
+ border-top: 2px solid;
+ border-image: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(0, 150, 255, 0.5),
+ transparent
+ )
+ 1;
+
+ h3 {
+ margin-top: 0;
+ margin-bottom: 1rem;
+ color: rgba(100, 180, 255, 0.9);
+ font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ text-shadow: 0 0 10px rgba(0, 150, 255, 0.4);
+ font-size: 1rem;
+ }
+ }
+
+ .preview-container {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .preview-wrapper {
+ background: repeating-conic-gradient(
+ rgba(20, 30, 50, 0.8) 0% 25%,
+ rgba(10, 20, 40, 0.9) 0% 50%
+ )
+ 50% / 20px 20px;
+ padding: 2rem;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 4px;
+ border: 1px solid rgba(0, 150, 255, 0.2);
+ }
+
+ #button-canvas {
+ image-rendering: pixelated;
+ image-rendering: -moz-crisp-edges;
+ image-rendering: crisp-edges;
+ border: 1px solid rgba(0, 150, 255, 0.4);
+ box-shadow: 0 0 15px rgba(0, 150, 255, 0.2);
+ }
+
+ .controls-section {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ }
+
+ .control-group {
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 100, 180, 0.15) 0%,
+ rgba(0, 80, 150, 0.1) 100%
+ );
+ padding: 1.5rem;
+ 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;
+
+ &::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::before {
+ opacity: 1;
+ }
+
+ h3 {
+ margin-top: 0;
+ margin-bottom: 1rem;
+ }
+
+ label {
+ display: block;
+ margin-top: 0.75rem;
+ margin-bottom: 0.25rem;
+ font-weight: 500;
+ font-size: 0.85rem;
+ color: rgba(100, 180, 255, 0.8);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ }
+
+ input[type="text"],
+ select {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ border: 1px solid rgba(0, 150, 255, 0.4);
+ border-radius: 4px;
+ font-size: 0.9rem;
+ 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);
+ }
+ }
+
+ select {
+ cursor: pointer;
+
+ option {
+ background: rgba(5, 15, 30, 0.95);
+ color: rgba(200, 220, 255, 0.95);
+ }
+ }
+
+ // Font preview in dropdowns
+ select#font-family,
+ select#font-family2 {
+ option[value="Lato"] {
+ font-family: "Lato", sans-serif;
+ }
+ option[value="Roboto"] {
+ font-family: "Roboto", sans-serif;
+ }
+ option[value="Open Sans"] {
+ font-family: "Open Sans", sans-serif;
+ }
+ option[value="Montserrat"] {
+ font-family: "Montserrat", sans-serif;
+ }
+ option[value="Oswald"] {
+ font-family: "Oswald", sans-serif;
+ }
+ option[value="Bebas Neue"] {
+ font-family: "Bebas Neue", display;
+ }
+ option[value="Roboto Mono"] {
+ font-family: "Roboto Mono", monospace;
+ }
+ option[value="VT323"] {
+ font-family: "VT323", monospace;
+ }
+ option[value="Press Start 2P"] {
+ font-family: "Press Start 2P", display;
+ }
+ }
+
+ input[type="range"] {
+ width: 100%;
+ margin-top: 0.25rem;
+ -webkit-appearance: none;
+ appearance: none;
+ background: rgba(0, 100, 180, 0.2);
+ border-radius: 4px;
+ height: 6px;
+ outline: none;
+
+ &::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 18px;
+ height: 18px;
+ background: linear-gradient(135deg, #0096ff, #66ccff);
+ cursor: pointer;
+ border-radius: 50%;
+ box-shadow: 0 0 10px rgba(0, 150, 255, 0.6);
+ border: 2px solid rgba(0, 150, 255, 0.8);
+ }
+
+ &::-moz-range-thumb {
+ width: 18px;
+ height: 18px;
+ background: linear-gradient(135deg, #0096ff, #66ccff);
+ cursor: pointer;
+ border-radius: 50%;
+ box-shadow: 0 0 10px rgba(0, 150, 255, 0.6);
+ border: 2px solid rgba(0, 150, 255, 0.8);
+ }
+ }
+
+ input[type="color"] {
+ width: 100%;
+ height: 50px;
+ border: 1px solid rgba(0, 150, 255, 0.4);
+ border-radius: 4px;
+ cursor: pointer;
+ background: rgba(5, 15, 30, 0.7);
+ padding: 4px;
+ transition: all 0.3s ease;
+
+ &:hover {
+ border-color: rgba(0, 150, 255, 0.7);
+ box-shadow: 0 0 15px rgba(0, 150, 255, 0.3);
+ }
+ }
+
+ input[type="checkbox"] {
+ width: auto;
+ margin-left: 0.5rem;
+ cursor: pointer;
+ accent-color: #0096ff;
+ }
+ }
+
+ .control-group-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ cursor: pointer;
+ user-select: none;
+ margin: 0;
+ padding: 0.5rem 0;
+ border-bottom: 2px solid;
+ border-image: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(0, 150, 255, 0.5),
+ transparent
+ )
+ 1;
+ margin-bottom: 1rem;
+ color: rgba(100, 180, 255, 0.9);
+ font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ transition: all 0.3s ease;
+
+ &:hover {
+ color: #66ccff;
+ text-shadow: 0 0 10px rgba(0, 150, 255, 0.5);
+ }
+ }
+
+ .toggle-icon {
+ font-size: 1.5rem;
+ font-weight: bold;
+ transition: transform 0.3s ease;
+ color: rgba(0, 150, 255, 0.8);
+
+ .control-group.collapsed & {
+ transform: rotate(180deg);
+ }
+ }
+
+ .control-group-content {
+ overflow: hidden;
+ transition:
+ max-height 0.3s ease,
+ opacity 0.3s ease;
+
+ .control-group.collapsed & {
+ max-height: 0;
+ opacity: 0;
+ pointer-events: none;
+ }
+ }
+
+ .checkbox-row {
+ display: flex;
+ gap: 1.5rem;
+ margin-top: 0.75rem;
+ }
+
+ .checkbox-label {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 500;
+ font-size: 0.85rem;
+ cursor: pointer;
+ margin: 0;
+ color: rgba(100, 180, 255, 0.8);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ transition: color 0.3s ease;
+
+ &:hover {
+ color: #66ccff;
+ }
+
+ input[type="checkbox"] {
+ margin: 0;
+ cursor: pointer;
+ }
+
+ span {
+ user-select: none;
+ }
+ }
+
+ .btn-primary,
+ .btn-secondary {
+ width: 100%;
+ padding: 0.75rem 1.5rem;
+ font-size: 0.95rem;
+ font-weight: 600;
+ border: 1px solid rgba(0, 150, 255, 0.5);
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ 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::before {
+ left: 100%;
+ }
+ }
+
+ .btn-primary {
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 120, 200, 0.3) 0%,
+ rgba(0, 100, 180, 0.2) 100%
+ );
+ color: #66ccff;
+
+ &: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);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+ }
+
+ .btn-secondary {
+ background: linear-gradient(
+ 135deg,
+ rgba(0, 100, 180, 0.2) 0%,
+ rgba(0, 80, 150, 0.15) 100%
+ );
+ color: #66ccff;
+ margin-top: 0.5rem;
+
+ &: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);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+ }
+}
diff --git a/assets/sass/pages/homepage.scss b/assets/sass/pages/homepage.scss
index 0aa1a23..e06bfd3 100644
--- a/assets/sass/pages/homepage.scss
+++ b/assets/sass/pages/homepage.scss
@@ -244,6 +244,7 @@
inset 0 1px 2px rgba(255, 255, 255, 0.1);
border-radius: 1em;
cursor: pointer;
+ position: relative;
&::after {
content: "Interests and Tools";
@@ -267,7 +268,7 @@
@include media-down(lg) {
margin-left: -0.5em;
opacity: 1;
- bottom: 0;
+ bottom: -40px;
font-size: 20px;
}
}
diff --git a/assets/sass/pages/resources.scss b/assets/sass/pages/resources.scss
index b7aa8a7..f70ee2c 100644
--- a/assets/sass/pages/resources.scss
+++ b/assets/sass/pages/resources.scss
@@ -84,7 +84,7 @@
max-width: 1400px;
margin: 0 auto;
position: relative;
- z-index: 1;
+ z-index: 10;
}
// Portal Header with portals on either side
@@ -97,7 +97,7 @@
position: relative;
.portal-title {
- font-size: 3.5rem;
+ font-size: 2.5rem;
font-family: Verdana, "Trebuchet MS", Tahoma, sans-serif;
font-weight: 700;
text-transform: uppercase;
@@ -108,11 +108,13 @@
0 0 40px rgba(255, 255, 255, 0.4),
0 2px 4px rgba(0, 0, 0, 0.3);
margin: 0;
+ margin-top: 50px;
position: relative;
z-index: 2;
@include media-up(md) {
font-size: 4rem;
+ margin-top: 0;
}
}
@@ -192,7 +194,7 @@
left: 5%;
bottom: 5%;
width: 150px;
- z-index: -1;
+ z-index: 1;
.heart-icon {
font-size: 2em;
@@ -257,6 +259,7 @@
.portal-sign {
position: relative;
padding: 3rem 2rem;
+ z-index: 10;
border: 10px solid black;
background: white;
@@ -579,7 +582,8 @@
inset 0 0 100px rgba(0, 100, 200, 0.05),
0 8px 32px rgba(0, 0, 0, 0.4);
position: relative;
- overflow: hidden;
+ overflow: visible;
+ z-index: 10;
// Subtle tech panel grid pattern
&::before {
diff --git a/assets/sass/style.scss b/assets/sass/style.scss
index e165389..e5591e9 100644
--- a/assets/sass/style.scss
+++ b/assets/sass/style.scss
@@ -26,6 +26,7 @@
@import "pages/blog";
@import "pages/media";
@import "pages/resources";
+@import "pages/button-generator";
@import url(https://fonts.bunny.net/css?family=abel:400|barlow-condensed:400,500|caveat:400|lato:300,300i,400,400i|neonderthaw:400);
diff --git a/content/blog/2026-01-06-week-3-bit-chilly-out/index.md b/content/blog/2026-01-06-week-3-bit-chilly-out/index.md
index 0106157..f0dd967 100644
--- a/content/blog/2026-01-06-week-3-bit-chilly-out/index.md
+++ b/content/blog/2026-01-06-week-3-bit-chilly-out/index.md
@@ -9,6 +9,8 @@ draft: true
- ❄️ It's been pretty cold and we've had the lightest sprinkling of snow.
- 📱 I did a mobile pass on some of the pages on this website, it works a bit better on phones now.
+- 🟠 Added the [resources](/resources) section with a script to pull weekly last.fm stats (see example output below!)
+- 🆒 Started working on an [88x31 button creator](/resources/button-generator/), it's got a decent amount of functionality so far.
## Links I Found Interesting
diff --git a/content/resources/button-generator/index.md b/content/resources/button-generator/index.md
new file mode 100644
index 0000000..0dbad1b
--- /dev/null
+++ b/content/resources/button-generator/index.md
@@ -0,0 +1,23 @@
+---
+title: "88x31 Button Creator"
+date: 2026-01-08
+description: "Make custom 88x31 pixel buttons with text, colors, gradients, and textures"
+icon: "button"
+demo_url: ""
+source_url: ""
+draft: false
+---
+
+Welcome to my 88x31 button creator, this is a pretty rough and ready implementation so it could be buggy, please let me know if you find any issues.
+
+Currently this only supports static images and exports as png due to the basic `canvas` tag limitations. I have approximate plans for how to make this export gifs and potentially make animated buttons, please look forward to it.
+
+Big thanks to [neonaut's 88x31 archive](https://neonaut.neocities.org/cyber/88x31) and everyone who made the buttons that appear there. You should check it out if you need inspiration for your button!
+
+{{< button-generator >}}
+
+---
+
+### Changelog
+
+- 08/01/2025 - Initial release.
diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html
index 5546f2c..e3146ce 100755
--- a/layouts/_default/baseof.html
+++ b/layouts/_default/baseof.html
@@ -33,5 +33,11 @@
{{ block "main" . }}{{ end }}
{{ block "footer" . }}{{ partial "site-footer.html" . }}{{ end }} {{ block
"scripts" . }}{{ partial "site-scripts.html" . }}{{ end }}
+
+
+ {{ if or (findRE "{{<\\s*button-generator" .RawContent) (findRE "{{%\\s*button-generator" .RawContent) }}
+ {{ $buttonGenerator := resources.Get "js/button-generator.js" | resources.Minify | resources.Fingerprint }}
+
+ {{ end }}