Inktober 2025
by Kofod
HTML
<div class="calendar-area" id="calendarArea">
<img class="title-img" id="titleImg" alt="Title" src="https://i.imgur.com/WJ6qmxl.png" />
<a href="https://en.virtualpopstar.com/social/forum?category=11&topic=368677"
target="_blank" rel="noopener"
class="discussion-link">
<img src="https://i.imgur.com/7WDF94f.png" alt="Inktober Discussion Thread"
class="discussion-img">
</a>
<!-- Shine -->
<div id="discussionShine" class="discussion-shine" aria-hidden="true"></div>
<aside class="info-box" id="infoBox" aria-label="Information">
<h2>Inktober 2025</h2>
<p>Here is the event you all have been waiting for!<br>
Inktober is here! As you might know BloggerTeam is inactive, which means that the all the teams of VirtualPopstar have stuck their heads together to host Inktober this year!<br><br>To discover the prompts, scratch the doors to reveal what is hidden behind!
</p>
</aside>
<div class="grid-wrap">
<div class="doors-grid" id="doorsGrid" aria-label="October Calendar"></div>
</div>
</div>
<!-- Modal -->
<div class="modal-backdrop" id="modalBackdrop" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<div class="modal-box" id="modalBox">
<header class="modal-header">
<h3 id="modalTitle">Day</h3>
<button class="modal-close" id="modalClose" aria-label="Close">×</button>
</header>
<div class="modal-content" id="modalContent"></div>
</div>
</div>
CSS
body{
margin:0;
background:#0b0b0b;
display:flex;
justify-content:center;
align-items:flex-start;
font-family: Arial, Helvetica, sans-serif;
color:#fff;
}
.calendar-area{
position:relative;
width:960px;
height:1100px;
overflow:hidden;
background:#000 center/cover no-repeat;
background-image:url('https://i.ibb.co/bgrb516h/eeKLUGo.png');
border-radius:8px;
box-shadow:0 10px 30px rgba(0,0,0,.5);
}
/* --- Header elements --- */
.title-img{
position:absolute;
top:50px;
left:80px;
max-width:450px;
max-height:200px;
object-fit:contain;
user-select:none;
pointer-events:none;
filter: drop-shadow(3px 5px 2px #0d3064);
}
.discussion-img{
position:absolute;
width:98px;
height:98px;
top:60px;
left:310px;
cursor:pointer;
transition: transform .15s ease;
z-index: 20;
filter: drop-shadow(3px 5px 2px #0d3064)
}
.discussion-img:hover{
transform: scale(1.03);
filter: drop-shadow(5px 7px 4px #0d3064)
}
.info-box{
position:absolute;
top:16px;
right:16px;
width:425px;
height:140px;
padding:14px 16px;
background:rgba(0,0,0,.6);
border:1px solid rgba(255,255,255,.15);
border-radius:10px;
box-shadow:0 6px 20px rgba(0,0,0,.35);
overflow:auto;
z-index: 15;
}
.info-box h2{ margin:0 0 6px 0; font-size:18px; }
.info-box p{ margin:0; font-size:14px; line-height:1.35; }
/* --- Grid area --- */
.grid-wrap{
position:absolute;
left:16px;
right:16px;
top:16px;
bottom:16px;
padding-top:180px; /* space for title + info */
}
.doors-grid{
width:auto;
height:100%;
display:grid;
grid-template-columns: repeat(7, 1fr);
grid-gap:8px;
grid-auto-rows: 171px;
justify-content:center;
align-content:start;
max-width:900px;
margin:0 auto;
...
JavaScript
(function($){
/* ---------- CONFIG ---------- */
var TARGET_MONTH = 10;
var TARGET_YEAR = 2025;
var debugAll = /[?&]debug=1\b/.test(location.search);
// ---- Scratch look & feel ----
var BRUSH_RADIUS = 15;
var HARD_EDGE_BRUSH = true;
var FADE_THRESHOLD = 0.60;
var TEXTURE_OPACITY = 0.35;
/* ---------- TOP-LEVEL IMAGE FALLBACKS ---------- */
// Replace the second URLs with your mirrors
var ASSETS = {
title: { urls: ['https://i.ibb.co/ycnhm2cC/WJ6qmxl.png', 'https://i.imgur.com/WJ6qmxl.png'] },
discussion: { urls: ['https://i.ibb.co/VpjsWF9B/ink-button.png', 'https://i.imgur.com/7WDF94f.png'] },
bg: { urls: ['https://i.ibb.co/bgrb516h/eeKLUGo.png', 'https://i.imgur.com/eeKLUGo.png'] }
};
/* ---------- TIMEZONE: CET/CEST ---------- */
function getCESTDate(){
var now = new Date();
var parts = new Intl.DateTimeFormat('en-GB', {
timeZone: 'Europe/Copenhagen',
year: 'numeric', month: 'numeric', day: 'numeric'
}).formatToParts(now);
var get = function(type){ return parseInt(parts.find(p => p.type === type).value, 10); };
return { year: get('year'), month: get('month'), day: get('day') };
}
/* ---------- HELPERS ---------- */
function firstWorkingUrl(urls, timeoutMs, cacheBust){
urls = (urls || []).filter(Boolean);
var to = (typeof timeoutMs === 'number') ? timeoutMs : 2500;
var bust = !!cacheBust;
function testOnce(u){
return new Promise(function(resolve){
var img = new Image();
var done = false;
var timer = setTimeout(function(){
if (done) return;
done = true;
// Timeout: consider this URL failed
resolve(null);
}, to);
img.onload = function(){
if (done) return;
done = true;
clearTimeout(timer);
resolve(u);
};
img.onerror = function(){
if (done) return;
done = true;
...