From fe6d980549476236ae990a31d1e6062f18f95304 Mon Sep 17 00:00:00 2001 From: Anuj Sharma Date: Sat, 11 Jul 2026 07:59:02 +0530 Subject: [PATCH 1/2] Revert "Refactor: Track Route and navbar removed" This reverts commit 5cc1a580fac7bcb005d01059fbf7ca4db7abeb6d. --- web-app/css/styles.css | 1142 +++++++++++++++++++++++++++++++++++++++- web-app/index.html | 520 ++++++++++++------ web-app/js/main.js | 240 ++++++++- 3 files changed, 1729 insertions(+), 173 deletions(-) diff --git a/web-app/css/styles.css b/web-app/css/styles.css index cd1db4c6..03990cf2 100644 --- a/web-app/css/styles.css +++ b/web-app/css/styles.css @@ -719,8 +719,8 @@ body.sidebar-active .sidebar-dock { overflow: hidden; } -#boardCanvas -{ +#boardCanvas, +#timelineCanvas { position: absolute; inset: 0; width: 100%; @@ -1418,6 +1418,371 @@ body.sidebar-active .sidebar-dock { opacity: 0.6; } +/* ── Hero Timeline ─────────────────────────────────────────── */ +.hero-timeline-section { + position: relative; + padding: 80px 0; + overflow: hidden; +} + +.timeline-container { + position: relative; + max-width: 1280px; + margin: 0 auto; + padding: 0 24px; +} + +.timeline-grid { + display: flex; + flex-direction: column; + gap: 80px; + position: relative; + z-index: 1; +} + +.timeline-item { + display: grid; + grid-template-columns: 1fr 40px 1fr; + gap: 32px; + align-items: center; + opacity: 0; + transform: translateY(40px); + transition: + opacity 0.6s var(--ease-out-expo), + transform 0.6s var(--ease-out-expo); +} + +.timeline-item.visible { + opacity: 1; + transform: translateY(0); +} + +.timeline-item:nth-child(odd) .timeline-content { + grid-column: 1; + text-align: right; +} + +.timeline-item:nth-child(even) .timeline-content { + grid-column: 3; + text-align: left; +} + +.timeline-node { + grid-column: 2; + display: flex; + align-items: center; + justify-content: center; + position: relative; + z-index: 2; +} + +.timeline-dot { + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--bg); + border: 2px solid var(--text-tertiary); + transition: all var(--duration-base) var(--ease-out-expo); + position: relative; +} + +.timeline-item.visible .timeline-dot { + border-color: var(--accent); + background: var(--accent); + box-shadow: 0 0 20px var(--accent-glow); + transform: scale(1.2); +} + +.timeline-item.active .timeline-dot { + transform: scale(1.6); + box-shadow: + 0 0 30px var(--accent-glow), + 0 0 60px rgba(34, 197, 94, 0.15); +} + +.timeline-dot::after { + content: ""; + position: absolute; + inset: -4px; + border-radius: 50%; + border: 2px solid var(--accent); + opacity: 0; + transform: scale(0.8); + transition: all 0.6s var(--ease-out-expo); +} + +.timeline-item.active .timeline-dot::after { + opacity: 1; + transform: scale(1); + animation: timelinePulse 2s ease-in-out infinite; +} + +@keyframes timelinePulse { + + 0%, + 100% { + transform: scale(1); + opacity: 0.6; + } + + 50% { + transform: scale(1.5); + opacity: 0; + } +} + +.timeline-item.active .timeline-content { + border-color: var(--accent); + box-shadow: + 0 0 30px var(--accent-glow), + var(--shadow-lg); + transform: translateY(-4px) scale(1.02); + background: var(--surface-hover); +} + +.timeline-item.active .timeline-title { + background: linear-gradient(135deg, var(--accent), #22c55e); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.timeline-content { + display: flex; + align-items: center; + gap: 16px; + padding: 24px; + border-radius: 16px; + background: var(--surface); + border: 1px solid var(--border); + backdrop-filter: blur(12px); + transition: all var(--duration-base) ease; +} + +.timeline-item:nth-child(odd) .timeline-content { + flex-direction: row-reverse; +} + +.timeline-item:nth-child(even) .timeline-content { + flex-direction: row; +} + +.timeline-icon { + flex-shrink: 0; + width: 28px; + height: 28px; + color: var(--accent); + transition: transform var(--duration-base) var(--ease-out-expo); +} + +.timeline-item:nth-child(odd) .timeline-text { + padding-right: 12px; + border-right: 2px solid var(--accent); + border-right-color: rgba(34, 197, 94, 0.5); +} + +.timeline-item:nth-child(even) .timeline-text { + padding-left: 12px; + border-left: 2px solid var(--accent); + border-left-color: rgba(34, 197, 94, 0.5); +} + +.timeline-item.active .timeline-icon { + transform: scale(1.2); +} + +.timeline-text { + flex: 1; +} + +.timeline-item:nth-child(odd) .timeline-text { + text-align: right; +} + +.timeline-item:nth-child(even) .timeline-text { + text-align: left; +} + +.timeline-item.visible .timeline-content { + border-color: var(--border-accent); + box-shadow: var(--shadow-md); +} + +.timeline-step { + font-family: var(--font-mono); + font-size: 0.7rem; + font-weight: 600; + color: var(--accent); + letter-spacing: 0.08em; + text-transform: uppercase; + margin-bottom: 6px; + display: block; +} + +.timeline-title { + font-family: var(--font-sans); + font-size: 1.15rem; + font-weight: 700; + margin-bottom: 6px; + letter-spacing: -0.02em; +} + +.timeline-desc { + font-size: 0.9rem; + color: var(--text-secondary); + line-height: 1.6; +} + +/* ── Timeline Route Badge ────────────────────────────────── */ +.timeline-route-badge { + position: absolute; + top: -55px; + /* Offset upward to sit beautifully above the first timeline grid row */ + right: 24px; + font-family: var(--font-sans); + font-size: clamp(1.5rem, + 4vw, + 2.75rem); + /* Mathematically half of the hero title clamp(3rem, 8vw, 5.5rem) */ + font-weight: 800; + line-height: 1.05; + letter-spacing: -0.04em; + background: linear-gradient(135deg, var(--accent), #06b6d4); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + z-index: 10; + pointer-events: none; + text-transform: uppercase; +} + +.timeline-route-badge::after { + content: ""; + position: absolute; + bottom: -6px; + /* Position beneath the text */ + left: 0; + width: 100%; + height: 4px; + background: var(--accent); + box-shadow: 0 0 10px var(--accent-glow); + border-radius: 2px; +} + +/* ── Timeline Section Background Grid & Orbs (Matching Hero) ── */ +.timeline-background { + position: absolute; + inset: 0; + z-index: 0; + overflow: hidden; + pointer-events: none; +} + +.timeline-background::before { + content: ""; + position: absolute; + inset: 0; + background-image: + linear-gradient(to right, var(--border) 1px, transparent 1px), + linear-gradient(to bottom, var(--border) 1px, transparent 1px); + background-size: 48px 48px; + opacity: 0.3; + mask-image: radial-gradient(ellipse at center, black 30%, transparent 70%); + -webkit-mask-image: radial-gradient(ellipse at center, + black 30%, + transparent 70%); +} + +.timeline-background::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 800px; + height: 800px; + background: radial-gradient(circle, var(--accent-soft) 0%, transparent 60%); + opacity: 0.5; + pointer-events: none; +} + +.timeline-orb { + position: absolute; + border-radius: 50%; + filter: blur(80px); + opacity: 0.25; + pointer-events: none; + z-index: 0; +} + +.timeline-orb-one { + width: 400px; + height: 400px; + right: -100px; + top: 20%; + background: radial-gradient(circle, rgba(34, 197, 94, 0.25), transparent 70%); + animation: orbFloat 20s ease-in-out infinite; +} + +.timeline-orb-two { + width: 450px; + height: 450px; + left: -150px; + bottom: 15%; + background: radial-gradient(circle, rgba(6, 182, 212, 0.15), transparent 70%); + animation: orbFloat 25s ease-in-out infinite reverse; +} + +/* ── Dynamic Timeline SVG Track ──────────────────────────── */ +.timeline-svg { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 2; + /* Sits over the background overlays but behind content */ +} + +.timeline-svg-track { + stroke: var(--text-tertiary); + stroke-width: 3px; + stroke-linecap: round; + /* dot · gap: tiny dot, generous gap → sparse dotted look */ + stroke-dasharray: 3 20; + opacity: 0.35; + transition: stroke var(--duration-base) ease; +} + +.timeline-svg-fill { + stroke: var(--accent); + stroke-width: 3px; + stroke-linecap: round; + stroke-dasharray: 3 20; + /* dashoffset animated by JS; starts at full length (path not drawn) */ + transition: stroke var(--duration-base) ease; + filter: drop-shadow(0 0 6px var(--accent-glow)); +} + +/* Ensure timeline grid elements overlap properly */ +.timeline-grid { + position: relative; + z-index: 3; + /* Above timeline SVG */ +} + +.timeline-item { + position: relative; + z-index: 4; + /* Card layering above grid root */ +} + +.timeline-node { + position: relative; + z-index: 5; + /* Step nodes sit on top of everything */ +} /* ═══════════════════════════════════════════════════════════════ FILTER SIDEBAR @@ -1619,11 +1984,13 @@ body.sidebar-active .sidebar-dock { } body.sidebar-active:not(.sidebar-collapsed) .hero-section, + body.sidebar-active:not(.sidebar-collapsed) .hero-timeline-section, body.sidebar-active:not(.sidebar-collapsed) .playground-section { padding-left: 296px !important; } body.sidebar-collapsed .hero-section, + body.sidebar-collapsed .hero-timeline-section, body.sidebar-collapsed .playground-section { padding-left: 128px !important; } @@ -3280,6 +3647,11 @@ body.sidebar-collapsed .playground-section { @media (prefers-reduced-motion: reduce) { + .timeline-route-badge, + .timeline-route-badge::after, + .timeline-svg-track, + .timeline-svg-fill, + .timeline-item.visible .timeline-dot, .footer-content>*, .footer-links li, .footer-brand h2, @@ -3437,6 +3809,17 @@ body.sidebar-collapsed .playground-section { /* ═══════════════════════════════════════════════════════════════ RESPONSIVE ═══════════════════════════════════════════════════════════════ */ +@media (max-width: 1100px) { + .projects-grid { + grid-template-columns: repeat(2, 1fr); + } + + .timeline-item { + grid-template-columns: 1fr 32px 1fr; + gap: 20px; + } +} + @media (max-width: 900px) { .footer-content { grid-template-columns: 1fr 1fr; @@ -3445,6 +3828,10 @@ body.sidebar-collapsed .playground-section { .footer-brand { grid-column: 1 / -1; } + + .hero-timeline-section { + display: none; + } } @media (max-width: 768px) { @@ -4255,9 +4642,84 @@ html[data-theme="dark"] body { linear-gradient(180deg, #161211 0%, #0f0c0b 100%) !important; } -.cursor-glow { - position: fixed; - width: 240px; +/* ========================================================================== + ARCADE REDESIGN OVERRIDES + ========================================================================== */ + +:root { + --accent-color: #6abf8d; + --primary-color: #6abf8d; + --secondary-color: #f2a65a; + --success-color: #5fbf93; + --danger-color: #de6d5b; + --warning-color: #f2b85f; + --bg-color: #f8f5ee; + --surface-color: rgba(255, 253, 248, 0.82); + --panel-color: #fffaf1; + --control-color: #fffdf8; + --text-color: #2c241f; + --text-secondary: #75675d; + --border-color: rgba(118, 99, 84, 0.14); + --accent-soft: rgba(106, 191, 141, 0.16); + --accent-border: rgba(106, 191, 141, 0.28); + --overlay-color: rgba(255, 248, 240, 0.7); + --on-accent: #fffdf8; + --shadow: 0 22px 60px rgba(155, 123, 90, 0.14); + --shadow-modal: 0 35px 90px rgba(101, 72, 39, 0.18); +} + +[data-theme="dark"] { + --accent-color: #9fddb5; + --primary-color: #9fddb5; + --secondary-color: #f6c58d; + --success-color: #9fddb5; + --danger-color: #ff9e8c; + --warning-color: #ffd48f; + --bg-color: #13100f; + --surface-color: rgba(34, 28, 26, 0.92); + --panel-color: #1d1817; + --control-color: #2a2321; + --text-color: #f8eee4; + --text-secondary: #d8c6b6; + --border-color: rgba(255, 239, 223, 0.14); + --accent-soft: rgba(159, 221, 181, 0.14); + --accent-border: rgba(159, 221, 181, 0.28); + --overlay-color: rgba(15, 12, 11, 0.8); + --shadow: 0 28px 70px rgba(0, 0, 0, 0.42); + --shadow-modal: 0 35px 90px rgba(0, 0, 0, 0.5); +} + +body { + font-family: "DM Sans", "Segoe UI", "sans-serif"; + background: var(--bg-color); + color: var(--text-color); +} + +[data-theme="light"] body { + background: + radial-gradient(circle at top left, + rgba(255, 219, 174, 0.44), + transparent 30%), + radial-gradient(circle at top right, + rgba(192, 245, 224, 0.46), + transparent 24%), + linear-gradient(180deg, #fffdf8 0%, #f8f5ee 45%, #f5efe5 100%); +} + +html[data-theme="dark"] body { + background: + radial-gradient(circle at top left, + rgba(242, 166, 90, 0.12), + transparent 28%), + radial-gradient(circle at top right, + rgba(159, 221, 181, 0.1), + transparent 24%), + linear-gradient(180deg, #161211 0%, #0f0c0b 100%) !important; +} + +.cursor-glow { + position: fixed; + width: 240px; height: 240px; border-radius: 50%; pointer-events: none; @@ -4445,6 +4907,76 @@ html[data-theme="dark"] body { padding: 0 1rem; } +.sticky-filter-bar { + position: sticky; + top: 88px; + z-index: 950; + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: center; + width: min(1120px, calc(100% - 32px)); + margin: 0 auto 18px; + padding: 12px; + border-radius: 18px; + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(128, 104, 85, 0.12); + backdrop-filter: blur(18px) saturate(1.2); + -webkit-backdrop-filter: blur(18px) saturate(1.2); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08); + opacity: 0; + transform: translateY(-8px); + pointer-events: none; + transition: opacity 180ms ease, transform 180ms ease; +} + +.sticky-filter-bar.visible { + opacity: 1; + transform: translateY(0); + pointer-events: auto; +} + +[data-theme="dark"] .sticky-filter-bar { + background: rgba(14, 18, 28, 0.88); + border-color: rgba(255, 255, 255, 0.08); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.28); +} + +.sticky-tab { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 40px; + padding: 0 16px; + border-radius: 999px; + border: 1px solid rgba(128, 104, 85, 0.12); + background: rgba(255, 255, 255, 0.45); + color: var(--text-secondary); + font-family: var(--font-sans); + font-size: 0.88rem; + font-weight: 600; + letter-spacing: -0.01em; + transition: all var(--duration-fast) ease; +} + +.sticky-tab:hover { + color: var(--text); + border-color: var(--border-accent); + background: var(--accent-soft); +} + +.sticky-tab.active { + color: var(--text); + border-color: var(--accent-glow); + background: var(--accent-soft); + box-shadow: 0 0 0 1px var(--accent-glow); +} + +[data-theme="dark"] .sticky-tab { + background: rgba(255, 255, 255, 0.03); + border-color: rgba(255, 255, 255, 0.08); +} + /* ── Hero Timeline (right side) ───────────────────────────── */ .hero-timeline { position: absolute; @@ -4662,6 +5194,7 @@ html[data-theme="dark"] body { .achievement-card, .featured-lab-card, .project-card, +.sticky-filter-bar, .playground-section, .footer, .modal-content { @@ -4949,8 +5482,6 @@ button:focus-visible, .btn-generate:focus-visible, .calc-btn:focus-visible, .dropdown-item:focus-visible, -.recent-search-chip-label:focus-visible, -.recent-search-chip-remove:focus-visible, .dropdown-recent-text:focus-visible, .dropdown-recent-remove:focus-visible { outline: 3px solid var(--primary-color); @@ -5087,7 +5618,8 @@ button:focus-visible, top: 2px; } -.hero-features{ +.hero-features, +.sticky-filter-inner { background: rgba(255, 255, 255, 0.58); border: 1px solid rgba(128, 104, 85, 0.08); border-radius: 24px; @@ -5095,12 +5627,14 @@ button:focus-visible, gap: 0.55rem; } -[data-theme="dark"] .hero-features{ +[data-theme="dark"] .hero-features, +[data-theme="dark"] .sticky-filter-inner { background: rgba(255, 255, 255, 0.06); border-color: rgba(255, 239, 223, 0.1); } -.feature-badge{ +.feature-badge, +.sticky-tab { border-radius: 999px; padding: 0.8rem 1rem; text-transform: none; @@ -5117,7 +5651,8 @@ button:focus-visible, display: none; } -.feature-badge.active{ +.feature-badge.active, +.sticky-tab.active { background: linear-gradient(135deg, rgba(255, 198, 132, 0.32), rgba(125, 220, 185, 0.3)); @@ -5125,14 +5660,25 @@ button:focus-visible, border: 1px solid rgba(128, 104, 85, 0.1); } -[data-theme="dark"] .feature-badge{ +[data-theme="dark"] .feature-badge, +[data-theme="dark"] .sticky-tab { color: var(--text-secondary); } -[data-theme="dark"] .feature-badge.active { +[data-theme="dark"] .feature-badge.active, +[data-theme="dark"] .sticky-tab.active { border-color: rgba(255, 239, 223, 0.12); } +.sticky-filter-bar { + top: 16px; + left: 12px; + right: 12px; + width: auto; + background: transparent; + border-bottom: 0; + box-shadow: none; +} .hero-card-topline, .progress-card-header, @@ -5509,15 +6055,309 @@ button:focus-visible, width: 5px; height: 5px; border-radius: 50%; - pointer-events: none; - z-index: 0; - opacity: 0.5; - background: radial-gradient(circle, - rgba(255, 197, 115, 0.28) 0%, - rgba(121, 221, 183, 0.12) 42%, - transparent 72%); - transform: translate3d(-50%, -50%, 0); - transition: opacity 0.25s ease; + background: #c084fc; + /* violet */ + position: absolute; + left: -12px; + opacity: 0; + transition: all 0.3s ease; +} + +.footer-links a:hover { + transform: translateX(6px); + color: #c084fc; +} + +.footer-links a:hover::before { + opacity: 1; + left: -8px; + background: #c084fc; +} + +.footer-links a i { + font-size: 0.9rem; + color: var(--accent-color); + opacity: 0.7; + transition: var(--transition); +} + +.footer-links a svg { + font-size: 0.9rem; + color: var(--accent-color); + opacity: 0.7; + transition: var(--transition); +} + + +.card-actions .btn-play { + margin-top: 0; + padding: 0.5rem 1rem; + font-size: 0.82rem; +} + +.card-actions .btn-favorite, +.card-actions .btn-share { + position: static; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: none; + border-radius: 8px; + cursor: pointer; + flex-shrink: 0; + font-size: 0.85rem; + line-height: 1; + transition: + background 0.18s ease, + transform 0.18s ease; +} + +.card-actions .btn-share { + background: rgba(128, 104, 85, 0.06); + color: var(--text-secondary); +} + +.card-actions .btn-share:hover { + background: rgba(106, 191, 141, 0.12); + color: #4a8a5e; + transform: scale(1.1); +} + +.card-actions .btn-favorite { + background: transparent; + color: #a09080; + font-size: 0.9rem; +} + +.card-actions .btn-favorite:hover { + color: #f5a623; + transform: scale(1.15); +} + +.card-actions .btn-favorite.active { + color: #f5a623; +} + +[data-theme="dark"] .card-actions .btn-share { + background: rgba(255, 255, 255, 0.06); + color: var(--text-secondary); +} + +[data-theme="dark"] .card-actions .btn-share:hover { + background: rgba(106, 191, 141, 0.15); + color: #7fcfbd; +} + +[data-theme="dark"] .card-actions .btn-favorite { + color: #7a6a5a; +} + +[data-theme="dark"] .card-actions .btn-favorite:hover, +[data-theme="dark"] .card-actions .btn-favorite.active { + color: #f5a623; +} + +.project-card h3 { + font-family: "Fredoka", sans-serif; + font-size: 1.3rem; + margin-top: 0.85rem; + margin-bottom: 0; +} + +.project-card p { + color: var(--text-secondary); + margin: 0.5rem 0 0.75rem; + font-size: 0.92rem; + line-height: 1.5; +} + +.btn-play { + padding: 0.75rem 1.2rem; + font-size: 0.92rem; +} + +.project-card-badge { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.35rem 0.65rem; + border-radius: 6px; + font-size: 0.7rem; + font-weight: 600; + font-family: "IBM Plex Mono", monospace; + text-transform: uppercase; + letter-spacing: 0.04em; + background: rgba(106, 191, 141, 0.08); + color: #5a8a5a; + border: none; +} + +[data-theme="dark"] .project-card-badge { + background: rgba(106, 191, 141, 0.12); + color: #7fcfbd; +} + +.project-card-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + margin-top: 0.5rem; + padding-top: 0.5rem; + border-top: 1px solid rgba(128, 104, 85, 0.06); +} + +.footer { + margin-top: 3rem; + background: rgba(255, 252, 245, 0.76); + border-top: 1px solid rgba(128, 104, 85, 0.08); +} + +[data-theme="dark"] .footer { + background: rgba(0, 0, 0, 0.7); +} + +[data-theme="dark"] .footer, +[data-theme="dark"] .footer a, +[data-theme="dark"] .footer p, +[data-theme="dark"] .footer h2, +[data-theme="dark"] .footer h3 { + color: var(--text-color); +} + +.reveal-on-scroll { + opacity: 0; + transform: translateY(20px); +} + +.reveal-on-scroll.is-visible { + opacity: 1; + transform: translateY(0); + transition: + opacity 0.55s ease, + transform 0.55s ease; +} + +@media (max-width: 1100px) { + .achievement-grid { + grid-template-columns: 1fr; + } + + .hero-timeline { + display: none; + } + + .hero-shell { + padding: 0 1rem; + } + + .hero-content { + max-width: 700px; + margin-left: auto; + margin-right: auto; + } +} + +@media (max-width: 900px) { + .hero-shell { + flex-direction: column; + gap: 1rem; + padding: 0; + } +} + +@media (max-width: 768px) { + .cursor-glow { + display: none; + } + + .nav-wrapper { + border-radius: 28px; + flex-wrap: wrap; + } + + .search-box { + max-width: none; + width: 100%; + order: 3; + } + + .hero-content { + padding: 1.5rem 1.35rem; + } + + .hero-content::before { + width: 80px; + } + + .hero-title { + font-size: 2.8rem; + } + + .hero-meta span { + padding: 0.5rem 0.75rem; + font-size: 0.75rem; + } + + .section-heading, + .projects-header, + .hero-card-topline, + .progress-card-header, + .progress-caption { + flex-direction: column; + align-items: flex-start; + } +} + +@media (prefers-reduced-motion: reduce) { + + .cursor-glow, + .hero-orb-one, + .hero-orb-two, + .typing-text::after { + animation: none !important; + } + + .project-card, + .project-card:hover, + .btn-explore, + .btn-secondary-hero, + .btn-random-project, + .lab-action, + .inline-link-button, + .btn-play { + transform: none !important; + } + + /* Fix card content alignment */ + .project-card { + text-align: left; + align-items: flex-start; + } + + .project-card h3, + .project-card p { + width: 100%; + text-align: left; + } + + /* Ensure card-actions is aligned properly */ + .project-card .card-actions { + width: 100%; + } + + /* Fix grid gaps on small screens */ + @media (max-width: 480px) { + .projects-grid { + gap: 1rem; + } + + .project-card { + padding: 1rem; + } + } } /* Responsive safety constraints for game columns */ @@ -5879,6 +6719,7 @@ body.sidebar-collapsed .sidebar-dock .sidebar-footer .sidebar-main-controls { .projects-section, .recently-viewed-container, .hero-section, + .hero-timeline-section, .playground-section, .footer { transition: padding-left 280ms cubic-bezier(0.16, 1, 0.3, 1); @@ -6422,3 +7263,260 @@ body.sidebar-collapsed .sidebar-dock .sidebar-footer .sidebar-main-controls { } } + +/* ============================================ + PROJECT STATISTICS DASHBOARD + ============================================ */ + +.stats-dashboard-section { + padding: 3rem 0 1.5rem 0; + position: relative; +} + +/* Dashboard Title - Styled like TRACK-ROUTE badge */ +.stats-dashboard-section .section-header { + margin-bottom: 2rem; + text-align: center; +} + +.stats-dashboard-section .section-heading { + font-family: var(--font-sans); + font-size: clamp(1.5rem, 4vw, 2.75rem); + font-weight: 800; + line-height: 1.05; + letter-spacing: -0.04em; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + position: relative; + margin: 0 auto; + background: linear-gradient(135deg, var(--accent), #06b6d4); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-transform: uppercase; + padding-bottom: 8px; +} + +.stats-dashboard-section .section-heading::after { + content: ''; + position: absolute; + bottom: -4px; + left: 50%; + transform: translateX(-50%); + width: 80%; + height: 4px; + border-radius: 2px; + background: linear-gradient(90deg, transparent, var(--accent), #06b6d4, transparent); + opacity: 0.8; + box-shadow: 0 0 12px var(--accent-glow); + transition: width 0.3s ease; +} + +.stats-dashboard-section .section-heading:hover::after { + width: 100%; +} + +.stats-dashboard-section .section-heading .heading-icon { + font-size: 1.8rem; + line-height: 1; + -webkit-text-fill-color: initial; + background: none; +} + +.stats-dashboard-section .section-heading .heading-text { + background: linear-gradient(135deg, var(--accent), #06b6d4); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Stats Grid */ +.stats-dashboard { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 1rem; + margin-top: 1.5rem; +} + +/* Stats Cards */ +.stats-card { + background: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: 14px; + padding: 1.2rem 0.8rem; + text-align: center; + transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); + cursor: default; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + position: relative; + overflow: hidden; +} + +/* Card hover effect */ +.stats-card:hover { + transform: translateY(-4px); + border-color: var(--accent-color); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); +} + +/* Card glow on hover */ +.stats-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, transparent, var(--accent-color, #22c55e), transparent); + opacity: 0; + transition: opacity 0.3s ease; +} + +.stats-card:hover::before { + opacity: 1; +} + +/* Card numbers */ +.stats-card h3 { + font-family: var(--font-sans, 'Space Grotesk', sans-serif); + font-size: 2rem; + font-weight: 700; + margin: 0; + background: linear-gradient(135deg, var(--accent-color, #22c55e), #06b6d4); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + letter-spacing: -0.02em; + line-height: 1.2; +} + +/* Card labels */ +.stats-card p { + font-size: 0.75rem; + font-weight: 600; + color: var(--text-secondary); + margin: 0.4rem 0 0 0; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +/* Category-specific accent colors */ +.stats-card[data-filter="games"] h3 { + background: linear-gradient(135deg, #ec4899, #f43f5e); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stats-card[data-filter="games"]::before { + background: linear-gradient(90deg, transparent, #ec4899, transparent); +} + +.stats-card[data-filter="math"] h3 { + background: linear-gradient(135deg, #6366f1, #8b5cf6); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stats-card[data-filter="math"]::before { + background: linear-gradient(90deg, transparent, #6366f1, transparent); +} + +.stats-card[data-filter="utilities"] h3 { + background: linear-gradient(135deg, #10b981, #059669); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stats-card[data-filter="utilities"]::before { + background: linear-gradient(90deg, transparent, #10b981, transparent); +} + +.stats-card[data-filter="favorites"] h3 { + background: linear-gradient(135deg, #f59e0b, #d97706); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stats-card[data-filter="favorites"]::before { + background: linear-gradient(90deg, transparent, #f59e0b, transparent); +} + +/* Clickable stats cards */ +.stats-card[data-filter] { + cursor: pointer; +} + +.stats-card[data-filter]:hover { + border-color: var(--accent-color); +} + +/* Responsive - Tablet */ +@media (max-width: 768px) { + .stats-dashboard { + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; + } + + .stats-card { + padding: 1rem 0.5rem; + } + + .stats-card h3 { + font-size: 1.5rem; + } + + .stats-dashboard-section .section-heading { + font-size: 1.25rem; + } +} + +/* Responsive - Mobile */ +@media (max-width: 480px) { + .stats-dashboard { + grid-template-columns: repeat(2, 1fr); + gap: 0.5rem; + } + + .stats-card { + padding: 0.8rem 0.4rem; + border-radius: 10px; + } + + .stats-card h3 { + font-size: 1.2rem; + } + + .stats-card p { + font-size: 0.65rem; + letter-spacing: 0.04em; + } + + .stats-dashboard-section .section-heading { + font-size: 1rem; + } + + .stats-dashboard-section .section-heading .heading-icon { + font-size: 1.2rem; + } +} + +/* Reduced motion preference */ +@media (prefers-reduced-motion: reduce) { + .stats-card { + transition: none; + } + + .stats-card:hover { + transform: none; + } + + .stats-dashboard-section .section-heading::after { + transition: none; + } +} diff --git a/web-app/index.html b/web-app/index.html index 4f748e51..66a7ca69 100644 --- a/web-app/index.html +++ b/web-app/index.html @@ -35,17 +35,14 @@ crossorigin="anonymous" referrerpolicy="no-referrer" /> - - - - - + + -
@@ -89,7 +86,6 @@ - -
- -
-
- -
- -
- -
+ +
+ @@ -304,6 +292,52 @@

+
+
+ +
+

+ Project Statistics +

+
+ +
+ +
+

0

+

Total Projects

+
+ +
+

0

+

Games

+
+ +
+

0

+

Math

+
+ +
+

0

+

Utilities

+
+ +
+

0

+

Favorites

+
+ +
+

0

+

Recently Viewed

+
+ +
+ +
+
+
@@ -328,38 +362,106 @@

Welcome

-
-

0

-

Games

+
+
+
+
+
+
+ +
+ 02 +

Explore

+

+ Browse games, math puzzles & utilities — each one ready + in your browser +

+
+
-
-

0

-

Math

+
+
+ +
+ 03 +

Play

+

+ Launch any project instantly, experiment with code, and + share your wins +

+
+
+
+
+
+
-
-

0

-

Utilities

+
+
+
+
+
+
+ +
+ 04 +

Learn

+

+ Pick up Python concepts as you play — loops, logic, data + & more +

+
+
-
-

0

-

Favorites

+
+
+ +
+ 05 +

Build

+

+ Create your own mini projects and share them with the + community +

+
+
+
+
+
+
-
-

0

-

Recently Viewed

+
+
+
+
+
+
+ +
+ 06 +

Share

+

+ Show off your work, get feedback, and keep growing as a dev +

+
+
- -
-
-
-

Recently Viewed

-
-
-
+ +
+
+
+

+ Recently Viewed +

+
+
+
+
+
@@ -377,7 +479,9 @@

@@ -405,7 +509,8 @@

🐍 Python Playground

- Code Editor + Code + Editor
@@ -443,11 +549,22 @@

🐍 Python Playground

- - - + + + + + + +
+
+ No draft loaded
-
No draft loaded
@@ -457,7 +574,9 @@

🐍 Python Playground

role="dialog"> @@ -474,15 +593,20 @@

🐍 Python Playground

@@ -583,32 +719,36 @@

Stay Updated

- - - - + + + + - + - + + @@ -1165,7 +1387,7 @@

How to Play

- + + +
+ +
+
+ - - -
-
- - - - - - -
-
- -
-
- -
-

- Project Statistics -

-
- -
- -
-

0

-

Total Projects

-
- -
-

0

-

Games

-
- -
-

0

-

Math

-
- -
-

0

-

Utilities

-
- -
-

0

-

Favorites

-
- -
-

0

-

Recently Viewed

-
- -
- -
-
- - -
- - - -
-
TRACK-ROUTE
-
-
-
- -
- 01 -

Welcome

-

- Jump into a tiny Python arcade built for curious beginners -

-
-
-
-
-
-
-
-
-
-
-
-
-
- -
- 02 -

Explore

-

- Browse games, math puzzles & utilities — each one ready - in your browser -

-
-
-
-
-
- -
- 03 -

Play

-

- Launch any project instantly, experiment with code, and - share your wins -

-
-
-
-
-
-
-
-
-
-
-
-
-
- -
- 04 -

Learn

-

- Pick up Python concepts as you play — loops, logic, data - & more -

-
-
-
-
-
- -
- 05 -

Build

-

- Create your own mini projects and share them with the - community -

-
-
-
-
-
-
-
-
-
-
-
-
-
- -
- 06 -

Share

-

- Show off your work, get feedback, and keep growing as a dev -

-
-
-
-
-
-
diff --git a/web-app/js/fix.js b/web-app/js/fix.js index ae0511d4..1c035fad 100644 --- a/web-app/js/fix.js +++ b/web-app/js/fix.js @@ -8,36 +8,6 @@ if (tmplGrid) grid.textContent = tmplGrid.textContent; })(); -// ── 2. Timeline items: reveal on scroll ─────────────────────────────── -(function () { - const items = document.querySelectorAll('.timeline-item[data-reveal]'); - if (!items.length) return; - const io = new IntersectionObserver(entries => { - entries.forEach(e => { - if (e.isIntersecting) { e.target.classList.add('visible'); io.unobserve(e.target); } - }); - }, { threshold: 0.15 }); - items.forEach(el => io.observe(el)); -})(); - -// ── 3. Track-Route badge: hidden until timeline scrolls into view ────── -(function () { - const badge = document.querySelector('.timeline-route-badge'); - const section = document.getElementById('timelineSection'); - if (!badge || !section) return; - Object.assign(badge.style, { - opacity: '0', transform: 'translateY(-16px)', - transition: 'opacity .55s cubic-bezier(.22,1,.36,1), transform .55s cubic-bezier(.22,1,.36,1)' - }); - const io = new IntersectionObserver(entries => { - if (entries[0].isIntersecting) { - badge.style.opacity = '1'; badge.style.transform = 'translateY(0)'; - io.disconnect(); - } - }, { threshold: 0.05 }); - io.observe(section); -})(); - // ── 4. Hero content: guarantee visibility if animations stall ────────── (function () { const shell = document.querySelector('.hero-shell'); @@ -48,159 +18,7 @@ }, 800); })(); -// ── 5. Timeline: one continuous S-snake dotted line, card border to card border ── -(function () { - function buildTimelinePath() { - const container = document.querySelector('.timeline-container'); - const grid = document.querySelector('.timeline-grid'); - if (!container || !grid) return; - - const old = container.querySelector('.timeline-svg'); - if (old) old.remove(); - - const items = Array.from(grid.querySelectorAll('.timeline-item')); - if (items.length < 2) return; - - const cRect = container.getBoundingClientRect(); - const svgW = cRect.width; - const svgH = cRect.height; - - // Collect the anchor point on the card border facing the centre for each item. - // Odd items (i=0,2,4): card is on LEFT → anchor = right-centre border of card - // Even items (i=1,3,5): card is on RIGHT → anchor = left-centre border of card - const anchors = items.map((item, i) => { - const cardEl = item.querySelector('.timeline-content'); - if (!cardEl) return null; - const cr = cardEl.getBoundingClientRect(); - const isLeft = (i % 2 === 0); - return { - x: isLeft ? (cr.right - cRect.left) : (cr.left - cRect.left), - y: cr.top - cRect.top + cr.height / 2 - }; - }).filter(Boolean); - - if (anchors.length < 2) return; - - // Build ONE continuous cubic Bézier path through all anchors. - // The trick for a smooth S with no breaks: - // At every interior anchor the tangent must be continuous. - // We achieve this by using the same horizontal pull direction and - // making control points symmetric around each anchor. - // - // For segment i → i+1: - // a = anchors[i], b = anchors[i+1] - // The path travels horizontally out of a (CP1 at same Y as a), - // crosses over, then arrives horizontally into b (CP2 at same Y as b). - // Pull distance = half the horizontal span between a and b. - // - // To keep the path CONTINUOUS and SMOOTH at every junction we use - // a Catmull-Rom → cubic Bézier conversion approach: - // each segment's control points are tangent-matched to neighbours. - - // Compute tangent at each anchor using Catmull-Rom rule - // (for the endpoints we mirror the first/last interior tangent) - function catmullTangent(prev, curr, next) { - return { - tx: (next.x - prev.x) * 0.5, - ty: (next.y - prev.y) * 0.5 - }; - } - - const n = anchors.length; - const tan = anchors.map((_, i) => { - const prev = anchors[Math.max(i - 1, 0)]; - const next = anchors[Math.min(i + 1, n - 1)]; - return catmullTangent(prev, anchors[i], next); - }); - - // Override tangents to be PURELY HORIZONTAL at each card border. - // This ensures the line exits/enters every card border at 90° (clean T-junction). - // We keep the sign (direction) from Catmull-Rom but zero out the Y component. - const horizontalTan = anchors.map((a, i) => { - const raw = tan[i]; - // Magnitude = half the horizontal gap to the nearest neighbour - const prev = anchors[Math.max(i - 1, 0)]; - const next = anchors[Math.min(i + 1, n - 1)]; - const mag = Math.abs(next.x - prev.x) * 1; - const sign = raw.tx >= 0 ? 1 : -1; - return { tx: sign * mag, ty: 0 }; - }); - - let d = `M ${anchors[0].x.toFixed(2)} ${anchors[0].y.toFixed(2)}`; - - for (let i = 0; i < n - 1; i++) { - const a = anchors[i]; - const b = anchors[i + 1]; - const t0 = horizontalTan[i]; - const t1 = horizontalTan[i + 1]; - - // Cubic Bézier from Catmull-Rom: - // CP1 = a + t0/3, CP2 = b - t1/3 - const cp1x = a.x + t0.tx; - const cp1y = a.y + t0.ty; - const cp2x = b.x - t1.tx; - const cp2y = b.y - t1.ty; - - d += ` C ${cp1x.toFixed(2)} ${cp1y.toFixed(2)}, ${cp2x.toFixed(2)} ${cp2y.toFixed(2)}, ${b.x.toFixed(2)} ${b.y.toFixed(2)}`; - } - - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.setAttribute('class', 'timeline-svg'); - svg.setAttribute('viewBox', `0 0 ${svgW} ${svgH}`); - svg.setAttribute('aria-hidden', 'true'); - svg.style.cssText = `position:absolute;left:0;top:0;width:100%;height:${svgH}px;pointer-events:none;z-index:2;overflow:visible;`; - - // Dim ghost track - const track = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - track.setAttribute('class', 'timeline-svg-track'); - track.setAttribute('d', d); - track.setAttribute('fill', 'none'); - - // Glowing accent fill — animated draw-in - const fill = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - fill.setAttribute('class', 'timeline-svg-fill'); - fill.setAttribute('d', d); - fill.setAttribute('fill', 'none'); - - svg.appendChild(track); - svg.appendChild(fill); - container.insertBefore(svg, container.firstChild); - - // Scroll-triggered draw-in animation - requestAnimationFrame(() => { - const totalLen = fill.getTotalLength ? fill.getTotalLength() : 5000; - fill.style.strokeDasharray = '5 16'; - fill.style.strokeDashoffset = String(totalLen); - fill.style.transition = 'none'; - - const section = document.getElementById('timelineSection'); - const io2 = new IntersectionObserver(entries => { - if (entries[0].isIntersecting) { - requestAnimationFrame(() => { - fill.style.transition = 'stroke-dashoffset 2.4s cubic-bezier(0.16,1,0.3,1)'; - fill.style.strokeDashoffset = '0'; - }); - io2.disconnect(); - } - }, { threshold: 0.05 }); - if (section) io2.observe(section); - }); - } - - if (document.readyState === 'complete') { - buildTimelinePath(); - } else { - window.addEventListener('load', buildTimelinePath); - } - - let resizeTimer; - window.addEventListener('resize', () => { - clearTimeout(resizeTimer); - resizeTimer = setTimeout(buildTimelinePath, 150); - }); -})(); - -// ── 6. Sidebar: hidden during hero+timeline, appears at projects section +// ── 6. Sidebar: hidden during hero, appears at projects section (function () { const projectsSection = document.getElementById('projectsSection'); diff --git a/web-app/js/hero-canvas.js b/web-app/js/hero-canvas.js index a1065aab..b3d15602 100644 --- a/web-app/js/hero-canvas.js +++ b/web-app/js/hero-canvas.js @@ -158,7 +158,6 @@ // Initialize animations on both canvases var mesh1 = new ParticleMesh('boardCanvas'); - var mesh2 = new ParticleMesh('timelineCanvas'); /* ── Observe theme changes ───────────────────────────────── */ var themeObserver = new MutationObserver(function (mutations) { diff --git a/web-app/js/main.js b/web-app/js/main.js index c2e1d3b7..80e8ba7d 100644 --- a/web-app/js/main.js +++ b/web-app/js/main.js @@ -255,7 +255,6 @@ document.addEventListener("DOMContentLoaded", function () { var allMains = Array.from(document.querySelectorAll("main#main-content")); var visibleMain = allMains[1] || allMains[0] || null; - var timelineSectionNode = document.getElementById("timelineSection"); var projectsSectionNode = document.getElementById("projectsSection"); var playgroundSectionNode = document.getElementById("playgroundSection"); var footerNode = document.querySelector("footer.footer"); @@ -319,8 +318,6 @@ document.addEventListener("DOMContentLoaded", function () { var emptyStateHint = document.getElementById("emptyStateHint"); var projectCountBadge = document.getElementById("projectCountBadge"); var mobileMenuToggle = document.getElementById("mobileMenuToggle"); - var navControls = document.getElementById("navControls"); - var navbar = document.getElementById("mainNavbar"); function syncSearchInputs(value, sourceInput) { [searchInput, navSearchInput].forEach(function (input) { @@ -801,14 +798,6 @@ document.addEventListener("DOMContentLoaded", function () { ); heroObserver.observe(heroSection); - window.addEventListener( - "scroll", - function () { - var navH = navbar ? navbar.getBoundingClientRect().height : 72; - stickyFilterBar.style.top = navH + 16 + "px"; - }, - { passive: true } - ); } /* ── Random Project ───────────────────────────────────────── */ @@ -1724,241 +1713,6 @@ historyBadge.textContent=`(${recent.length})`; } })(); - /* ═══════════════════════════════════════════════════════════════ - TIMELINE SCROLL REVEAL - ═══════════════════════════════════════════════════════════════ */ - var timelineItems = document.querySelectorAll(".timeline-item[data-reveal]"); - var timelineFill = document.getElementById("timelineFill"); - var timelineSection = document.getElementById("timelineSection"); - - if (timelineItems.length && !prefersReducedMotion()) { - var timelineObserver = new IntersectionObserver( - function (entries) { - entries.forEach(function (entry) { - if (entry.isIntersecting) { - entry.target.classList.add("visible"); - } - }); - }, - { threshold: 0.25, rootMargin: "0px 0px -50px 0px" } - ); - - timelineItems.forEach(function (item) { - timelineObserver.observe(item); - }); - - /* ── Serpentine SVG Winding Timeline Path ─────────────────── */ - var svgNamespace = "http://www.w3.org/2000/svg"; - - function getElementOffset(el, parent) { - var top = 0; - var left = 0; - var curr = el; - while (curr && curr !== parent) { - top += curr.offsetTop || 0; - - left += curr.offsetLeft || 0; - - curr = curr.offsetParent; - } - return { top: top, left: left }; - } - - function rebuildTimelineSvg() { - var container = document.querySelector(".timeline-container"); - if (!container) return; - var dots = document.querySelectorAll(".timeline-dot"); - if (dots.length < 2) return; - - var containerWidth = container.offsetWidth; - var containerHeight = container.offsetHeight; - - var svg = document.getElementById("timelineSvg"); - if (!svg) { - svg = document.createElementNS(svgNamespace, "svg"); - svg.id = "timelineSvg"; - svg.setAttribute("class", "timeline-svg"); - - var defs = document.createElementNS(svgNamespace, "defs"); - var grad = document.createElementNS(svgNamespace, "linearGradient"); - grad.id = "timelineGrad"; - grad.setAttribute("x1", "0%"); - grad.setAttribute("y1", "0%"); - grad.setAttribute("x2", "0%"); - - grad.setAttribute("y2", "100%"); - - var stop1 = document.createElementNS(svgNamespace, "stop"); - stop1.setAttribute("offset", "0%"); - stop1.setAttribute("stop-color", "var(--accent)"); - - var stop2 = document.createElementNS(svgNamespace, "stop"); - stop2.setAttribute("offset", "100%"); - stop2.setAttribute("stop-color", "#06b6d4"); - - grad.appendChild(stop1); - grad.appendChild(stop2); - defs.appendChild(grad); - - // Define a dynamic layout mask path for progress crawling - var mask = document.createElementNS(svgNamespace, "mask"); - mask.id = "timelineMask"; - - var maskPath = document.createElementNS(svgNamespace, "path"); - maskPath.id = "timelineMaskPath"; - maskPath.setAttribute("fill", "none"); - maskPath.setAttribute("stroke", "#ffffff"); - maskPath.setAttribute("stroke-width", "24"); // Wide enough to fully cover glowing dots - maskPath.setAttribute("stroke-linecap", "round"); - - mask.appendChild(maskPath); - defs.appendChild(mask); - svg.appendChild(defs); - - var track = document.createElementNS(svgNamespace, "path"); - track.id = "timelineSvgTrack"; - track.setAttribute("class", "timeline-svg-track"); - track.setAttribute("fill", "none"); - - var fill = document.createElementNS(svgNamespace, "path"); - fill.id = "timelineSvgFill"; - fill.setAttribute("class", "timeline-svg-fill"); - fill.setAttribute("fill", "none"); - fill.setAttribute("stroke", "var(--accent)"); - fill.setAttribute("mask", "url(#timelineMask)"); - - svg.appendChild(track); - svg.appendChild(fill); - - var grid = document.querySelector(".timeline-grid"); - container.insertBefore(svg, grid); - } - - // Determine layout stable coordinate points for all timeline dots - var coords = []; - dots.forEach(function (dot) { - var offset = getElementOffset(dot, container); - var x = offset.left + dot.offsetWidth / 2; - var y = offset.top + dot.offsetHeight / 2; - coords.push({ x: x, y: y }); - }); - - // Create the winding path - var d = ""; - var startX = containerWidth / 2; - d += "M " + startX + " 0"; - d += " L " + coords[0].x + " " + coords[0].y; - - // Calculate a sweep width that is perfectly responsive - // e.g. 16% of container width, capped at 180px for desktop beauty - var W = Math.min(180, containerWidth * 0.16); - - for (var i = 0; i < coords.length - 1; i++) { - var pStart = coords[i]; - var pEnd = coords[i + 1]; - var H = pEnd.y - pStart.y; - var dy = H * 0.35; // Symmetrical control point height - - // Even segments (0, 2, 4...) snake to the right, odd segments to the left - var dx = i % 2 === 0 ? W : -W; - - var cp1x = pStart.x + dx; - var cp1y = pStart.y + dy; - var cp2x = pEnd.x + dx; - var cp2y = pEnd.y - dy; - - d += - " C " + - cp1x + - " " + - cp1y + - ", " + - cp2x + - " " + - cp2y + - ", " + - pEnd.x + - " " + - pEnd.y; - } - - // Straight exit to the bottom - d += " L " + coords[coords.length - 1].x + " " + containerHeight; - - var trackPath = document.getElementById("timelineSvgTrack"); - var fillPath = document.getElementById("timelineSvgFill"); - var maskPath = document.getElementById("timelineMaskPath"); - if (trackPath && fillPath && maskPath) { - trackPath.setAttribute("d", d); - fillPath.setAttribute("d", d); - maskPath.setAttribute("d", d); - - var totalLength = maskPath.getTotalLength(); - maskPath.style.strokeDasharray = totalLength; - maskPath.dataset.totalLength = totalLength; - - // Trigger scroll progress sync immediately - updateTimelineFill(); - } - } - - - /* ── Activate item based on viewport center crossing timeline dots ── */ - var activeIdx = -1; - var dots = document.querySelectorAll(".timeline-dot"); - /* ── Timeline Fill Progress ───────────────────────────── */ - function updateTimelineFill() { - if (!timelineSection) return; - var container = document.querySelector(".timeline-container"); - if (!container) return; - - var containerRect = container.getBoundingClientRect(); - var viewportCenterY = window.innerHeight / 2; - - // Calculate relative vertical scroll position of the viewport center relative to the container - var relativeY = viewportCenterY - containerRect.top; - var offset = Math.max(0, Math.min(1, relativeY / containerRect.height)); - - /* ── Dynamic SVG path mask scroll synchronization ──────── */ - var maskPath = document.getElementById("timelineMaskPath"); - if (maskPath && maskPath.dataset.totalLength) { - var totalLength = parseFloat(maskPath.dataset.totalLength); - var dashoffset = totalLength - offset * totalLength; - maskPath.style.strokeDashoffset = Math.max( - 0, - Math.min(totalLength, dashoffset) - ); - } - - /* ── Activate item based on viewport center crossing timeline dots ── */ - var activeIdx = -1; - var dots = document.querySelectorAll(".timeline-dot"); - - dots.forEach(function (dot, i) { - var dotRect = dot.getBoundingClientRect(); - var dotCenterY = dotRect.top + dotRect.height / 2; - - // A dot is crossed/passed if its vertical center in the viewport is <= the viewport center - if (dotCenterY <= viewportCenterY) { - activeIdx = i; - } - }); - - timelineItems.forEach(function (item, i) { - item.classList.toggle("active", i === activeIdx); - }); - } - - // Initialize SVG path layout recalculations on page render & resize - rebuildTimelineSvg(); - window.addEventListener("resize", debounce(rebuildTimelineSvg, 150)); - window.addEventListener("scroll", updateTimelineFill, { passive: true }); - } else if (timelineItems.length) { - timelineItems.forEach(function (item) { - item.classList.add("visible"); - }); - } - /* ── Reveal on Scroll (general) ────────────────────────────── */ var revealItems = document.querySelectorAll(".reveal-on-scroll"); if (revealItems.length && !prefersReducedMotion()) {