diff --git a/corpus/corpus/decorators.py b/corpus/corpus/decorators.py
index c2d99ba2..2a2c2b00 100644
--- a/corpus/corpus/decorators.py
+++ b/corpus/corpus/decorators.py
@@ -1,7 +1,13 @@
+from datetime import datetime
+from functools import wraps
+from zoneinfo import ZoneInfo
+
from accounts.models import ExecutiveMember
from config.models import ModuleConfiguration
from django.contrib import messages
from django.shortcuts import redirect
+from django.shortcuts import render
+from django.utils import timezone
def module_enabled(module_name):
@@ -77,11 +83,14 @@ def wrapper(request, *args, **kwargs):
return decorator
+
def ensure_view_current_envision():
def decorator(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
- config = ModuleConfiguration.objects.get(module_name="virtual_expo").module_config
+ config = ModuleConfiguration.objects.get(
+ module_name="virtual_expo"
+ ).module_config
try:
ExecutiveMember.objects.get(user=request.user.id)
@@ -90,8 +99,40 @@ def wrapper(request, *args, **kwargs):
exec_member = False
can_view_current_envision = exec_member or config.get(
- "view_current_envision")
- kwargs['can_view_current_envision '] = can_view_current_envision
+ "view_current_envision"
+ )
+ kwargs["can_view_current_envision"] = can_view_current_envision
return view_func(request, *args, **kwargs)
+
return wrapper
+
+ return decorator
+
+
+def event_time_gate(
+ start_time,
+ end_time=None,
+ pre_template="tlm/403.html",
+ post_template="tlm/event_ended.html",
+ context=None,
+):
+ def decorator(view_func):
+ @wraps(view_func)
+ def wrapper(request, *args, **kwargs):
+ now = timezone.now()
+ ctx = context or {}
+
+ # Before launch
+ if now < start_time:
+ return render(request, pre_template, ctx, status=403)
+
+ # After event end
+ if end_time and now > end_time:
+ return render(request, post_template, ctx, status=403)
+
+ # During event
+ return view_func(request, *args, **kwargs)
+
+ return wrapper
+
return decorator
diff --git a/corpus/corpus/settings.py b/corpus/corpus/settings.py
index a61b776e..39da39a9 100644
--- a/corpus/corpus/settings.py
+++ b/corpus/corpus/settings.py
@@ -201,14 +201,14 @@
# Celery Settings
-CELERY_BROKER_URL = 'redis://redis:6379/0'
+CELERY_BROKER_URL = "redis://redis:6379/0"
# CELERY_RESULT_BACKEND = 'redis://redis:6379/0'
# CELERY_BROKER_URL="redis://default:AZZ-AAIjcDExZTljY2M1NTI5MmU0OGMxOGUzMDYwNmRlZjhkZGRjZXAxMA@divine-puma-38526.upstash.io:6379"
# CELERY_RESULT_BACKEND = "redis://default:AZZ-AAIjcDExZTljY2M1NTI5MmU0OGMxOGUzMDYwNmRlZjhkZGRjZXAxMA@divine-puma-38526.upstash.io:6379"
-CELERY_TIMEZONE='UTC'
-CELERY_ACCEPT_CONTENT=['json']
-CELERY_TASK_SERIALIZER = 'json'
+CELERY_TIMEZONE = "UTC"
+CELERY_ACCEPT_CONTENT = ["json"]
+CELERY_TASK_SERIALIZER = "json"
USE_TAILWIND_CDN_LINK = os.getenv("LIVECYCLE") is not None
diff --git a/corpus/templates/static/css/tlm.css b/corpus/templates/static/css/tlm.css
index 96ccc7b6..5b31e272 100644
--- a/corpus/templates/static/css/tlm.css
+++ b/corpus/templates/static/css/tlm.css
@@ -513,3 +513,107 @@ body {
animation: none !important;
}
}
+
+
+/* ═══════════════════════════════════════════════════════════════════
+ TRACK CARDS (landing page)
+ All selectors are scoped to .tlm-track-card so these rules only
+ apply inside a card and never leak to the hero or other sections.
+ ═══════════════════════════════════════════════════════════════════ */
+
+/* Card shell — portrait 3:4, clips everything inside */
+.tlm-track-card {
+ position: relative;
+ overflow: hidden; /* clips img zoom AND any text that would escape */
+ cursor: pointer;
+ border: 2px solid #a07830;
+ box-shadow: 4px 4px 0 #3a2d10;
+ flex-shrink: 0;
+ /* Default size used by home.html — landing.html overrides with aspect-ratio */
+ width: 180px;
+ height: 240px;
+}
+
+/* Image — sepia tint at rest, smooth transition for hover */
+.tlm-track-card img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+ filter: sepia(55%) brightness(0.85) contrast(1.05);
+ /* Smooth zoom + desaturate on hover */
+ transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94),
+ filter 0.5s ease;
+ transform-origin: center center;
+}
+
+/* Hover: smooth zoom-in + full greyscale */
+.tlm-track-card:hover img {
+ transform: scale(1.1);
+ filter: grayscale(100%) brightness(0.72) contrast(1.15);
+}
+
+/* Dark vignette overlay — left strip + bottom fade.
+ Scoped to .tlm-track-card so it never affects other elements. */
+.tlm-track-card .tlm-track-overlay {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ z-index: 1;
+ background:
+ linear-gradient(to right, rgba(10, 8, 2, 0.55) 0%, transparent 38%),
+ linear-gradient(to top, rgba(10, 8, 2, 0.60) 0%, transparent 38%);
+}
+
+/* Name strip — a 22 px column flush to the left edge, full card height.
+ Uses writing-mode so text flows vertically without transform hacks,
+ which means the text is always contained within the strip's box and
+ overflow:hidden on .tlm-track-card clips it safely. */
+.tlm-track-card .tlm-track-name-strip {
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 22px;
+ z-index: 2;
+ pointer-events: none;
+ /* Stack children bottom-to-top, centred in the strip */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ writing-mode: vertical-rl;
+ transform: rotate(180deg); /* flip: text now reads bottom → top (CCW) */
+ padding: 0.5rem 0;
+}
+
+/* The name text itself — static inside the strip, no absolute positioning */
+.tlm-track-card .tlm-track-name {
+ position: static;
+ transform: none;
+ left: auto;
+ top: auto;
+ margin-left: 0;
+ font-family: 'JetBrains Mono', 'Courier New', monospace;
+ font-size: 0.72rem;
+ font-weight: 700;
+ color: #ffffff;
+ letter-spacing: 0.1em;
+ text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.9);
+ white-space: nowrap;
+}
+
+/* Points — bottom-right corner, always inside the card */
+.tlm-track-card .tlm-track-pts {
+ position: absolute;
+ bottom: 0.45rem;
+ right: 0.55rem;
+ left: auto;
+ top: auto;
+ z-index: 2;
+ font-family: 'JetBrains Mono', 'Courier New', monospace;
+ font-size: 0.72rem;
+ font-weight: 700;
+ color: #ffffff;
+ letter-spacing: 0.08em;
+ text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.95);
+}
\ No newline at end of file
diff --git a/corpus/templates/static/img/tlm/track1.jpeg b/corpus/templates/static/img/tlm/track1.jpeg
new file mode 100644
index 00000000..c061c69e
Binary files /dev/null and b/corpus/templates/static/img/tlm/track1.jpeg differ
diff --git a/corpus/templates/static/img/tlm/track2.jpeg b/corpus/templates/static/img/tlm/track2.jpeg
new file mode 100644
index 00000000..df7af540
Binary files /dev/null and b/corpus/templates/static/img/tlm/track2.jpeg differ
diff --git a/corpus/templates/static/img/tlm/track3.png b/corpus/templates/static/img/tlm/track3.png
new file mode 100644
index 00000000..d5240730
Binary files /dev/null and b/corpus/templates/static/img/tlm/track3.png differ
diff --git a/corpus/templates/static/img/tlm/track4.jpeg b/corpus/templates/static/img/tlm/track4.jpeg
new file mode 100644
index 00000000..602a33a7
Binary files /dev/null and b/corpus/templates/static/img/tlm/track4.jpeg differ
diff --git a/corpus/templates/tlm/403.html b/corpus/templates/tlm/403.html
new file mode 100644
index 00000000..3294d507
--- /dev/null
+++ b/corpus/templates/tlm/403.html
@@ -0,0 +1,46 @@
+{% extends 'tlm/base.html' %}
+{% load static %}
+
+{% block title %}Access Denied{% endblock %}
+
+{% block style %}
+
+{# Set theme synchronously — same pattern as landing.html #}
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+
+
+{% block content %}
+
+ Access Denied The mission has not begun yet. Return on Mar 20, 2026 at 18:00 IST. Access Denied The mission has ended.403
+ 403
+
+ Join our Discord channel +
+ + + +- Doomsday Timer -
- - {# Inline monospace countdown. #} - {# JS updates the four number spans (tlm-days, tlm-hours, #} - {# tlm-minutes, tlm-seconds) - the same IDs used previously, #} - {# so the countdown script requires zero changes. #} -- The missions have begun. -
- -Nuclear Winter
+ +
+
+
+
+
+
+
+
+ Doomsday Timer
+ ++ The missions have begun. +
+ +