diff --git a/hackx/index.html b/hackx/index.html
index 1fa5999c..fd393cce 100644
--- a/hackx/index.html
+++ b/hackx/index.html
@@ -279,32 +279,117 @@
// TIMELINE
diff --git a/hackx/sections.js b/hackx/sections.js
index 66cc5f88..a97c3e92 100644
--- a/hackx/sections.js
+++ b/hackx/sections.js
@@ -18,6 +18,7 @@
initFooterCanvas();
// Core (one-time CSS transitions, no jank)
initScrollFadeIn();
+ initTimelineParallaxReveal();
initPrizeCounters();
// initScrollProgressBar(); // removed — was showing blue line at top
initAnnouncements();
@@ -396,6 +397,152 @@
}
}
+ // ===== TIMELINE PARALLAX REVEAL (timeline-only) =====
+ function initTimelineParallaxReveal() {
+ const timelineItems = Array.from(document.querySelectorAll('.timeline-item'));
+ if (timelineItems.length === 0) return;
+ const itemStates = new Map();
+ let rafId = null;
+
+ function clamp(value, min, max) {
+ return Math.min(Math.max(value, min), max);
+ }
+
+ function smoothstep(t) {
+ return t * t * (3 - 2 * t);
+ }
+
+ function setTimelineHeight(item, state) {
+ if (!state.timetable) return;
+ item.style.setProperty('--tt-height', `${state.timetable.scrollHeight}px`);
+ }
+
+ function initTimelineState() {
+ timelineItems.forEach((item) => {
+ item.classList.remove('timeline-open');
+ item.style.setProperty('--date-scale', '1');
+ item.style.setProperty('--date-glow', '0');
+ item.style.setProperty('--timeline-reveal', '0');
+
+ const timetable = item.querySelector('.timeline-timetable');
+ const events = Array.from(item.querySelectorAll('.timetable-event'));
+
+ events.forEach(eventEl => {
+ eventEl.classList.remove('is-visible');
+ eventEl.style.opacity = '0';
+ eventEl.style.transform = 'translateY(8px)';
+ });
+
+ const state = {
+ target: 0,
+ current: 0,
+ events,
+ timetable,
+ };
+
+ itemStates.set(item, state);
+ setTimelineHeight(item, state);
+ });
+ }
+
+ function updateTimelineHeights() {
+ timelineItems.forEach((item) => {
+ const state = itemStates.get(item);
+ if (!state) return;
+ setTimelineHeight(item, state);
+ });
+ }
+
+ function computeTargetsFromScroll() {
+ const vh = window.innerHeight || document.documentElement.clientHeight;
+ timelineItems.forEach(item => {
+ const state = itemStates.get(item);
+ if (!state) return;
+
+ const rect = item.getBoundingClientRect();
+ // Complete reveal by viewport midpoint (50% height)
+ const start = vh * 0.9;
+ const end = vh * 0.5;
+ const progress = clamp((start - rect.top) / Math.max(start - end, 1), 0, 1);
+ state.target = smoothstep(progress);
+ });
+ }
+
+ function applyRevealProgress(item, revealProgress) {
+ const state = itemStates.get(item);
+ if (!state) return;
+ const eventCount = state.events.length;
+
+ item.classList.toggle('timeline-open', revealProgress > 0.01);
+ item.style.setProperty('--date-scale', (1 + revealProgress * 0.06).toFixed(3));
+ item.style.setProperty('--date-glow', revealProgress.toFixed(3));
+ item.style.setProperty('--timeline-reveal', revealProgress.toFixed(3));
+
+ if (eventCount === 0) return;
+
+ state.events.forEach((eventEl, index) => {
+ // Slight overlap between items so the sequence feels continuous.
+ const stepSpan = 1 / (eventCount + 0.35);
+ const stepStart = index * stepSpan;
+ const localProgress = clamp((revealProgress - stepStart) / Math.max(stepSpan, 0.0001), 0, 1);
+
+ eventEl.style.opacity = localProgress.toFixed(3);
+ eventEl.style.transform = `translateY(${(1 - localProgress) * 8}px)`;
+ eventEl.classList.toggle('is-visible', localProgress > 0.6);
+ });
+ }
+
+ function startRafLoop() {
+ if (rafId !== null) return;
+
+ const tick = () => {
+ let hasActiveMotion = false;
+
+ timelineItems.forEach((item) => {
+ const state = itemStates.get(item);
+ if (!state) return;
+
+ const delta = state.target - state.current;
+ if (Math.abs(delta) > 0.0008) {
+ state.current += delta * 0.16;
+ hasActiveMotion = true;
+ } else {
+ state.current = state.target;
+ }
+
+ applyRevealProgress(item, state.current);
+ });
+
+ if (hasActiveMotion) {
+ rafId = requestAnimationFrame(tick);
+ } else {
+ rafId = null;
+ }
+ };
+
+ rafId = requestAnimationFrame(tick);
+ }
+
+ function updateFromScroll() {
+ computeTargetsFromScroll();
+ startRafLoop();
+ }
+
+ initTimelineState();
+ updateFromScroll();
+ window.addEventListener('scroll', updateFromScroll, { passive: true });
+ window.addEventListener('resize', () => {
+ updateTimelineHeights();
+ updateFromScroll();
+ });
+
+ // Ensure timeline positions are correct after fonts/layout settle.
+ setTimeout(() => {
+ updateTimelineHeights();
+ updateFromScroll();
+ }, 120);
+ }
+
// ===== PRIZE COUNTERS (easeOutExpo for satisfying deceleration) =====
function initPrizeCounters() {
const amounts = document.querySelectorAll('.prize-amount, .pool-amount');
@@ -1322,7 +1469,7 @@
`;
document.head.appendChild(style);
- document.querySelectorAll('.readout-row, .prize-entry, .timeline-item').forEach(el => {
+ document.querySelectorAll('.readout-row, .prize-entry').forEach(el => {
el.classList.add('_line-trace-wrap');
if (el.style.position === '' || el.style.position === 'static') {
el.style.position = 'relative';
diff --git a/hackx/style.css b/hackx/style.css
index aa1adb91..759ba183 100644
--- a/hackx/style.css
+++ b/hackx/style.css
@@ -460,6 +460,7 @@ body {
border-top: 2px solid #FF174420;
}
+
.timeline {
position: relative;
max-width: 800px;
@@ -480,9 +481,9 @@ body {
.timeline-item {
position: relative;
display: flex;
- align-items: baseline;
- gap: 40px;
- padding: 30px 0;
+ align-items: flex-start;
+ gap: 28px;
+ padding: 16px 0;
border-bottom: 1px solid #FF174410;
opacity: 0;
transform: translateY(10px);
@@ -518,13 +519,22 @@ body {
}
.timeline-date {
- font-size: clamp(20px, 3vw, 32px);
- font-weight: bold;
- letter-spacing: 0.06em;
+ font-size: clamp(18px, 2.2vw, 26px);
+ font-weight: 700;
+ letter-spacing: 0.04em;
color: #FF1744B0;
- min-width: 120px;
- text-align: right;
- flex-shrink: 0;
+ width: 190px;
+ height: 56px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ text-align: left;
+ box-sizing: border-box;
+ transform: scale(var(--date-scale, 1));
+ text-shadow: 0 0 calc(16px * var(--date-glow, 0)) rgba(255, 23, 68, 0.45);
+ transition: transform 0.22s ease, text-shadow 0.22s ease;
+ padding-right: 1.25rem;
+ will-change: transform, text-shadow;
}
.timeline-item.active .timeline-date {
@@ -537,12 +547,95 @@ body {
letter-spacing: 3px;
padding-left: 30px;
color: var(--text);
+ opacity: 0;
+ transform: translateY(8px);
+ transition: opacity 0.35s ease, transform 0.35s ease;
}
.timeline-item.active .timeline-event {
color: var(--gold);
}
+.timeline-item.timeline-open .timeline-event {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+/* ===== TIMELINE TIMETABLE ===== */
+.timetable-align {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.timeline-timetable {
+ margin-top: 12px;
+ margin-left: 20px;
+ max-height: calc(var(--tt-height, 0px) * var(--timeline-reveal, 0));
+ overflow: hidden;
+ opacity: calc(0.12 + (var(--timeline-reveal, 0) * 0.88));
+ transition: max-height 0.32s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.28s ease;
+ position: relative;
+ padding-left: 3rem;
+ will-change: max-height, opacity;
+}
+
+.timetable-event {
+ display: flex;
+ gap: 12px;
+ padding: 8px 0;
+ align-items: baseline;
+ font-size: 12px;
+ letter-spacing: 0.5px;
+ position: relative;
+ opacity: 0;
+ transform: translateY(8px);
+ transition: opacity 0.24s linear, transform 0.32s cubic-bezier(0.22, 1, 0.36, 1);
+ will-change: transform, opacity;
+}
+
+.timetable-event.is-visible {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.timetable-event::before {
+ content: '';
+ position: absolute;
+ left: -32px;
+ top: 6px;
+ width: 5px;
+ height: 5px;
+ border: 1px solid #FF174440;
+ background: #0A0008;
+ border-radius: 50%;
+ flex-shrink: 0;
+ transition: all 0.4s ease;
+}
+
+
+.timetable-event.is-visible::before {
+ border-color: #FF1744CC;
+ box-shadow: 0 0 6px #FF174420;
+}
+
+.event-time {
+ color: var(--gold);
+ font-weight: bold;
+ min-width: 65px;
+ letter-spacing: 2px;
+ flex-shrink: 0;
+}
+
+.event-desc {
+ color: var(--text-dim);
+ line-height: 1.3;
+}
+
+.timeline-item.visible .event-desc {
+ color: var(--text);
+}
+
/* ===== FAQ SECTION ===== */
#faq-section {
border-top: 2px solid #FF174420;
@@ -1395,9 +1488,13 @@ body {
}
.timeline-date {
+ justify-content: flex-start;
text-align: left;
- min-width: auto;
+ width: 190px;
+ min-width: 190px;
font-size: clamp(16px, 4vw, 24px);
+ padding-left: 0;
+ padding-right: 1rem;
}
.timeline-event {