<!DOCTYPE html>
<html lang="<%= lang %>">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
    <title><%= t('timeline.pageTitle') %></title>
    <meta name="description" content="<%= t('timeline.pageDescription') %>">
    <meta property="og:title" content="<%= t('timeline.pageTitle') %>">
    <meta property="og:description" content="<%= t('timeline.pageDescription') %>">
    <meta property="og:type" content="website">
    <meta name="theme-color" content="#90EE90">
    <link rel="icon" type="image/png" href="/images/transparent_toothlessssss.png">

    <!-- Fonts -->
    <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
    <link rel="preload" href="/Fonts/symbols_skyquake.ttf" as="font" type="font/ttf" crossorigin="anonymous">
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">

    <!-- Tailwind CSS -->
    <link rel="stylesheet" href="/css/output.css">

    <!-- Font Awesome -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" media="print" onload="this.media='all'">

    <style>
        @font-face {
            font-family: 'skyquakesymbols';
            src: url('/Fonts/symbols_skyquake.ttf') format('truetype');
            font-display: swap;
        }

        * { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; }

        body {
            background: #1b1b1b;
            min-height: 100vh;
        }

        /* Hero spacing as page-local CSS, NOT Tailwind utilities: the project
           ships a pre-built output.css with unused classes purged, so new
           lg:pt-* / lg:pb-* utilities don't exist and silently apply nothing. */
        .timeline-hero {
            padding-top: 7rem;      /* clears the fixed 64px nav + gap */
            padding-bottom: 2rem;
        }
        @media (min-width: 1024px) {
            .timeline-hero {
                padding-top: 12.25rem;     /* clearance below the fixed nav (~60px lower on tall screens) */
                padding-bottom: 1.75rem;   /* JS centering adds the rest of the hero→row1 gap */
            }
        }

        .gradient-text {
            background: linear-gradient(135deg, #F5F5DC 0%, #90EE90 100%);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
            color: #F5F5DC; /* fallback if background-clip:text unsupported */
            /* line-height + padding give the gradient paint box room to cover
               glyph descenders (e.g. the 'g' in Italian "La cronologia"). */
            line-height: 1.2;
            padding-bottom: 0.12em;
        }

        .text-accent { color: #F5F5DC; }
        .text-muted { color: #90EE90; }

        /* Serpentine timeline */
        .timeline {
            position: relative;
        }

        /* SVG that holds the flowing connector line, drawn behind the cards */
        .timeline-line {
            position: absolute;
            inset: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            z-index: 0;
            overflow: visible;
        }

        .timeline-line .progress {
            fill: none;
            stroke: url(#timelineGradient);
            stroke-width: 4;
            stroke-linecap: round;
            stroke-linejoin: round;
            /* no filter, no CSS transition — GSAP owns stroke-dashoffset each frame */
        }

        .timeline-line .comet-glow {
            fill: rgba(144, 238, 144, 0.28);
            filter: blur(5px);
        }

        .timeline-line .comet {
            fill: #F5F5DC;
            stroke: #90EE90;
            stroke-width: 2;
            filter: drop-shadow(0 0 6px rgba(144, 238, 144, 0.95));
        }

        .timeline-card-footer {
            margin-top: 1rem;
            padding-top: 0.75rem;
            border-top: 1px solid rgba(245, 245, 220, 0.07);
            display: flex;
            align-items: center;
            gap: 0.45rem;
            color: rgba(144, 238, 144, 0.65);
            font-size: 0.75rem;
            letter-spacing: 0.06em;
        }

        /* The grid of milestone nodes. Default = single column (mobile). */
        .timeline-grid {
            position: relative;
            z-index: 1;
            display: grid;
            grid-template-columns: 1fr;
            gap: 3rem 2rem;
        }

        @media (min-width: 1024px) {
            .timeline {
                margin-top: 1.5rem;   /* base; JS centering adds to this */
            }
            .timeline-grid {
                grid-template-columns: repeat(3, 1fr);
                gap: 10rem 3.75rem;
                align-items: start;
            }
            .timeline-card {
                padding: 2.5rem;
            }
            .timeline-marker {
                top: -1.8rem;
                left: 2.5rem;
                width: 3.6rem;
                height: 3.6rem;
                font-size: 1.35rem;
            }
            .timeline-index {
                font-size: 0.85rem;
            }
            .timeline-date {
                font-size: 1.15rem;
            }
            .timeline-title {
                font-size: 1.85rem;
                line-height: 1.3;
            }
            .timeline-desc {
                font-size: 1.15rem;
                line-height: 1.85;
            }
            .timeline-card-footer {
                margin-top: 2rem;
                padding-top: 1.25rem;
                font-size: 0.9rem;
            }
            /* The line snakes 3 by 3: row 1 left to right, row 2 right to left,
               row 3 left to right, so the connector never crosses itself. */
            .timeline-node:nth-child(1)  { grid-area: 1 / 1; }
            .timeline-node:nth-child(2)  { grid-area: 1 / 2; }
            .timeline-node:nth-child(3)  { grid-area: 1 / 3; }
            .timeline-node:nth-child(4)  { grid-area: 2 / 3; }
            .timeline-node:nth-child(5)  { grid-area: 2 / 2; }
            .timeline-node:nth-child(6)  { grid-area: 2 / 1; }
            /* Static fallback gap before row 2; JS (layoutFirstScreen) overrides
               this on desktop with a measured value so row 2 lands just below
               the fold on any viewport height. */
            .timeline-node:nth-child(4),
            .timeline-node:nth-child(5),
            .timeline-node:nth-child(6)  { margin-top: 8rem; }
            .timeline-node:nth-child(7)  { grid-area: 3 / 1; }
            .timeline-node:nth-child(8)  { grid-area: 3 / 2; }
            .timeline-node:nth-child(9)  { grid-area: 3 / 3; }
            .timeline-node:nth-child(10) { grid-area: 4 / 3; }
            .timeline-node:nth-child(11) { grid-area: 4 / 2; }
            .timeline-node:nth-child(12) { grid-area: 4 / 1; }
            .timeline-node:nth-child(13) { grid-area: 5 / 1; }
            .timeline-node:nth-child(14) { grid-area: 5 / 2; }
        }

        /* Short desktop viewports (e.g. 1080p monitors): trim the hero and card
           footprint so row 1 ends above the comet's resting spot, leaving a
           visible "scroll down" breathing band below it. Tall screens (1440p)
           already have that room, so they keep the roomier sizing above. */
        @media (min-width: 1024px) and (max-height: 1080px) {
            .timeline-hero {
                padding-top: 6.5rem;       /* nav clearance on short screens */
                padding-bottom: 1rem;
            }
            .timeline { margin-top: 0.75rem; }
            .timeline-card { padding: 1.85rem; }
            .timeline-title { font-size: 1.5rem; }
            .timeline-desc { font-size: 1.02rem; line-height: 1.6; }
            .timeline-card-footer { margin-top: 1.25rem; padding-top: 0.85rem; }
        }

        .timeline-card {
            position: relative;
            background: linear-gradient(135deg, rgba(55, 70, 55, 0.55) 0%, rgba(38, 38, 38, 0.55) 100%);
            border: 1px solid rgba(245, 245, 220, 0.08);
            border-radius: 1rem;
            padding: 1.75rem;
        }

        /* Node marker: the dot that the line threads through */
        .timeline-marker {
            position: absolute;
            top: -1.4rem;
            left: 1.75rem;
            width: 2.6rem;
            height: 2.6rem;
            border-radius: 9999px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 1rem;
            color: #1b1b1b;
            background: linear-gradient(135deg, #F5F5DC 0%, #90EE90 100%);
            box-shadow: 0 0 0 5px rgba(27, 27, 27, 1), 0 0 18px rgba(144, 238, 144, 0.45);
        }

        .timeline-index {
            font-family: 'skyquakesymbols', 'Inter', monospace;
            font-size: 0.7rem;
            letter-spacing: 0.18em;
            text-transform: uppercase;
            color: rgba(144, 238, 144, 0.7);
        }

        .timeline-date {
            color: #90EE90;
            font-weight: 700;
            font-size: 0.95rem;
        }

        .timeline-title {
            color: #F5F5DC;
            font-weight: 700;
            font-size: 1.35rem;
            line-height: 1.25;
        }

        .timeline-desc {
            color: rgba(255, 255, 255, 0.78);
            line-height: 1.65;
            font-size: 0.95rem;
        }

        /* Reveal animation: node is always opaque (solid background blocks the SVG
           line); only the inner card fades in. This prevents the line from being
           visible through the semi-transparent card during the fade-in. */
        #scroll-hint {
            display: none;
            position: fixed;
            bottom: 3.5rem;
            left: 50%;
            transform: translateX(-50%);
            z-index: 50;
            flex-direction: column;
            align-items: center;
            gap: 0.6rem;
            color: rgba(245, 245, 220, 0.4);
            font-size: 0.68rem;
            letter-spacing: 0.22em;
            text-transform: uppercase;
            pointer-events: none;
            transition: opacity 0.5s ease;
            white-space: nowrap;
        }
        .hint-line {
            width: 1px;
            height: 52px;
            background: rgba(245, 245, 220, 0.12);
            position: relative;
            overflow: hidden;
        }
        .hint-line::after {
            content: '';
            position: absolute;
            left: 0;
            width: 100%;
            height: 45%;
            background: linear-gradient(to bottom, transparent, #90EE90, transparent);
            animation: hintScroll 1.5s ease-in-out infinite;
        }
        @keyframes hintScroll {
            0%   { top: -45%; }
            100% { top: 100%; }
        }

        /* Full hint (text + animated line) on roomier desktop viewports, where
           there's a void below the comet for it to sit in. Threshold is loose
           (900px) because display scaling shrinks a 1440p monitor's CSS
           viewport well below its physical height. */
        @media (min-width: 1024px) and (min-height: 900px) {
            #scroll-hint { display: flex; }
        }

        /* Small bouncing arrow for shorter screens (and mobile), tucked just
           under the comet's resting area. */
        #scroll-arrow {
            display: flex;
            position: fixed;
            bottom: 1.4rem;
            left: 50%;
            transform: translateX(-50%);
            z-index: 50;
            color: rgba(144, 238, 144, 0.55);
            font-size: 1.05rem;
            pointer-events: none;
            transition: opacity 0.5s ease;
            animation: arrowBounce 1.6s ease-in-out infinite;
        }
        @media (min-width: 1024px) and (min-height: 900px) {
            #scroll-arrow { display: none; }
        }
        @keyframes arrowBounce {
            0%, 100% { transform: translateX(-50%) translateY(0); }
            50%      { transform: translateX(-50%) translateY(6px); }
        }
        @media (prefers-reduced-motion: reduce) {
            #scroll-arrow { animation: none; }
        }

        .timeline-node {
            position: relative;
            z-index: 2;
            border-radius: 1rem;
            /* background is set dynamically in JS only when a card starts
               revealing, so unrevealed cards never block the SVG track line */
        }

        /* Non-GSAP / CSS fallback: hide card until node is revealed */
        .timeline-node:not(.is-visible) .timeline-card { opacity: 0; }
        .timeline-node.is-visible .timeline-card {
            opacity: 1;
            transition: opacity 0.6s ease;
        }

        @media (prefers-reduced-motion: reduce) {
            .timeline-node .timeline-card { opacity: 1 !important; transition: none; }
            .timeline-line .progress { transition: none; }
        }
    </style>
</head>
<body class="text-white antialiased">
    <%- include('partials/nav', { activePage: 'timeline' }) %>

    <!-- Header -->
    <section class="timeline-hero">
        <div class="max-w-[1400px] mx-auto px-6 lg:px-8 text-center">
            <p class="text-muted text-sm font-semibold tracking-[0.25em] uppercase mb-4"><%= t('timeline.eyebrow') %></p>
            <h1 class="text-4xl lg:text-6xl font-extrabold mb-5 gradient-text"><%= t('timeline.heading') %></h1>
            <p class="text-lg text-white/70 max-w-2xl mx-auto">
                <%= t('timeline.pageDescription') %>
            </p>
        </div>
    </section>

    <!-- Timeline -->
    <section class="pb-28">
        <div class="max-w-[1400px] mx-auto px-6 lg:px-12">
            <div class="timeline" id="timeline">
                <!-- Flowing connector line (drawn by JS through the node markers) -->
                <svg class="timeline-line" id="timelineLine" preserveAspectRatio="none" aria-hidden="true">
                    <defs>
                        <linearGradient id="timelineGradient" x1="0%" y1="0%" x2="100%" y2="0%">
                            <stop offset="0%" stop-color="#90EE90"/>
                            <stop offset="100%" stop-color="#F5F5DC"/>
                        </linearGradient>
                    </defs>
                    <path class="progress" id="timelineProgress" d=""></path>
                    <circle class="comet-glow" id="timelineCometGlow" r="12" cx="0" cy="0" style="opacity:0"></circle>
                    <circle class="comet" id="timelineComet" r="5" cx="0" cy="0" style="opacity:0"></circle>
                </svg>

                <%
                    const timelineCards = [
                        { icon: 'seedling', count: 2 }, { icon: 'terminal', count: 3 },
                        { icon: 'tag', count: 4 }, { icon: 'users', count: 8 },
                        { icon: 'file-code', count: 15 }, { icon: 'robot', count: 20 },
                        { icon: 'fire', count: 60 }, { icon: 'laptop-code', count: 100 },
                        { icon: 'route', count: 340 }, { icon: 'handshake', count: 450 },
                        { icon: 'coins', count: 580 }, { icon: 'plug', count: 600 },
                        { icon: 'lightbulb', count: 610 }, { icon: 'location-dot', count: serverCount }
                    ];
                %>
                <div class="timeline-grid">
                    <% timelineCards.forEach((card, index) => { const key = 'timeline.cards.' + (index + 1); %>
                        <article class="timeline-node">
                            <div class="timeline-card">
                                <div class="timeline-marker"><i class="fas fa-<%= card.icon %>"></i></div>
                                <div class="pt-3">
                                    <div class="flex items-center justify-between mb-2">
                                        <span class="timeline-index"><%= String(index + 1).padStart(2, '0') %> · <%= t(key + '.label') %></span>
                                        <span class="timeline-date"><%= t(key + '.date') %></span>
                                    </div>
                                    <h3 class="timeline-title mb-3"><%= t(key + '.title') %></h3>
                                    <p class="timeline-desc"><%- t(key + '.description') %></p>
                                    <div class="timeline-card-footer">
                                        <i class="fab fa-discord"></i><span><%= t('timeline.servers', { count: card.count }) %></span>
                                    </div>
                                </div>
                            </div>
                        </article>
                    <% }); %>
                </div>
            </div>

            <div class="text-center mt-20">
                <a href="/" class="btn-primary px-8 py-4 rounded-xl text-base font-bold inline-flex items-center"
                   style="background: linear-gradient(135deg, #F5F5DC 0%, #E8E8D0 100%); color:#1E1E1E;">
                    <i class="fas fa-home mr-3"></i><%= t('common.backToHome') %>
                </a>
            </div>
        </div>
    </section>

    <!-- Scroll hints — fade out on first scroll. Full version (tall desktop)
         and a compact bouncing arrow (short screens / mobile). -->
    <div id="scroll-hint">
        <span><%= t('timeline.scroll') %></span>
        <div class="hint-line"></div>
    </div>
    <div id="scroll-arrow" aria-hidden="true"><i class="fas fa-chevron-down"></i></div>

    <!-- Footer -->
    <%- include('partials/footer') %>

    <script>
        window.__lang = '<%= lang %>';
        window.__i18n = <%- localeJson %>;
        window.__t = function(key) {
            var parts = key.split('.'), obj = window.__i18n;
            for (var i = 0; i < parts.length; i++) { obj = obj && obj[parts[i]]; }
            return obj !== undefined ? obj : key;
        };
        window.switchLanguage = function(lang) {
            var next = lang || (document.documentElement.lang === 'en' ? 'ru' : 'en');
            if (next === document.documentElement.lang) return;
            document.cookie = 'lang=' + next + ';path=/;max-age=31536000;SameSite=Lax';
            window.location.reload();
        };
    </script>
    <script src="/js/main.js?v=3"></script>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>

    <!-- Serpentine timeline renderer -->
    <script>
    (function () {
        var timeline = document.getElementById('timeline');
        var svg = document.getElementById('timelineLine');
        var progress = document.getElementById('timelineProgress');
        var comet = document.getElementById('timelineComet');
        var cometGlow = document.getElementById('timelineCometGlow');
        if (!timeline || !svg) return;

        var nodes = Array.prototype.slice.call(timeline.querySelectorAll('.timeline-node'));
        var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
        var useGsap = !!(window.gsap && window.ScrollTrigger);
        var drawProgress = 0;        // 0 to 1, how much of the line is drawn
        var targetDrawProgress = 0;
        var pathLength = 0;
        var nodeProgresses = [];
        var nodeRevealed = [];
        var state = { progress: 0 };
        var lineTrigger = null;
        var nodeTriggers = [];
        var nextRevealIndex = 0;

        // Build a rounded-corner path that threads through a list of points.
        function roundedPath(pts, radius) {
            if (pts.length < 2) return '';
            var d = 'M ' + pts[0].x.toFixed(1) + ' ' + pts[0].y.toFixed(1);
            for (var i = 1; i < pts.length - 1; i++) {
                var p0 = pts[i - 1], p1 = pts[i], p2 = pts[i + 1];
                var v1 = unit(p1, p0), v2 = unit(p1, p2);
                var r1 = Math.min(radius, dist(p0, p1) / 2);
                var r2 = Math.min(radius, dist(p2, p1) / 2);
                var a = { x: p1.x + v1.x * r1, y: p1.y + v1.y * r1 };
                var b = { x: p1.x + v2.x * r2, y: p1.y + v2.y * r2 };
                d += ' L ' + a.x.toFixed(1) + ' ' + a.y.toFixed(1);
                d += ' Q ' + p1.x.toFixed(1) + ' ' + p1.y.toFixed(1) + ' ' + b.x.toFixed(1) + ' ' + b.y.toFixed(1);
            }
            var last = pts[pts.length - 1];
            d += ' L ' + last.x.toFixed(1) + ' ' + last.y.toFixed(1);
            return d;
        }
        function unit(from, to) {
            var dx = to.x - from.x, dy = to.y - from.y, len = Math.hypot(dx, dy) || 1;
            return { x: dx / len, y: dy / len };
        }
        function dist(a, b) { return Math.hypot(a.x - b.x, a.y - b.y); }

        // Returns the marker's centre relative to the timeline container using
        // offsetTop/offsetLeft traversal — unaffected by CSS/GSAP transforms.
        function getMarkerCenter(node) {
            var marker = node.querySelector('.timeline-marker') || node;
            var x = marker.offsetLeft + marker.offsetWidth / 2;
            var y = marker.offsetTop + marker.offsetHeight / 2;
            var el = marker.offsetParent;
            while (el && el !== timeline) {
                x += el.offsetLeft;
                y += el.offsetTop;
                el = el.offsetParent;
            }
            return { x: x, y: y };
        }

        // Desktop layout pass: vertically centre row 1 + the comet's breathing
        // band in the viewport (spare space split evenly above row 1 and below
        // the comet), then push row 2 just under the fold so it stays hidden.
        // This keeps the same balanced look on any viewport height — no
        // per-resolution magic numbers.
        var COMET_BAND = 90; // breathing room reserved below row 1 for the comet
        function layoutFirstScreen() {
            var rowTwo = [nodes[3], nodes[4], nodes[5]].filter(Boolean);
            // Reset both adjustables so we measure the natural layout each time.
            timeline.style.marginTop = '';
            rowTwo.forEach(function (n) { n.style.marginTop = ''; });
            if (!window.matchMedia('(min-width: 1024px)').matches) return;
            if (!rowTwo.length) return;

            var vh = window.innerHeight;
            rowTwo.forEach(function (n) { n.style.marginTop = '0px'; });

            // Natural row-1 extent (tallest of the first three cards).
            var row1Top = Infinity, row1Bottom = 0;
            for (var k = 0; k < 3 && k < nodes.length; k++) {
                var r = nodes[k].getBoundingClientRect();
                row1Top = Math.min(row1Top, r.top);
                row1Bottom = Math.max(row1Bottom, r.bottom);
            }
            var row1Height = row1Bottom - row1Top;

            // Centre: half the leftover height goes above row 1, half below comet.
            var spare = vh - row1Top - row1Height - COMET_BAND;
            var topMargin = Math.max(0, spare * 0.5);
            var base = parseFloat(getComputedStyle(timeline).marginTop) || 0;
            timeline.style.marginTop = (base + topMargin) + 'px';

            // After the shift, drop row 2 just under the fold (+40 clears its marker).
            var row2Top = rowTwo[0].getBoundingClientRect().top;
            var gap = Math.max(48, (vh + 40) - row2Top);
            rowTwo.forEach(function (n) { n.style.marginTop = gap + 'px'; });
        }

        function build() {
            _samples = null;
            layoutFirstScreen();
            // Use layout dimensions (not getBoundingClientRect) so the SVG
            // coordinate system matches the transform-independent getMarkerCenter.
            svg.setAttribute('viewBox', '0 0 ' + timeline.offsetWidth + ' ' + timeline.offsetHeight);

            var pts = nodes.map(getMarkerCenter);

            var d = roundedPath(pts, 36);
            progress.setAttribute('d', d);

            pathLength = progress.getTotalLength();
            progress.style.strokeDasharray = pathLength;
            progress.style.strokeDashoffset = pathLength * (1 - drawProgress);
            nodeProgresses = nodes.map(function (node, index) {
                return nodePathProgress(node, index, pathLength);
            });

            updateComet(pathLength);
            return { pts: pts, len: pathLength };
        }

        function updateComet(len) {
            if (!comet || !cometGlow || reduceMotion || drawProgress <= 0 || drawProgress >= 1) {
                if (comet) comet.style.opacity = 0;
                if (cometGlow) cometGlow.style.opacity = 0;
                return;
            }
            var point = progress.getPointAtLength(len * drawProgress);
            var x = point.x.toFixed(1);
            var y = point.y.toFixed(1);
            comet.setAttribute('cx', x);
            comet.setAttribute('cy', y);
            cometGlow.setAttribute('cx', x);
            cometGlow.setAttribute('cy', y);
            comet.style.opacity = 1;
            cometGlow.style.opacity = 1;
        }

        // Find roughly how far along the path a point sits (coarse sampling).
        var _samples = null;
        function lengthAtPoint(target, len) {
            if (!_samples) {
                _samples = [];
                var steps = 120;
                for (var i = 0; i <= steps; i++) {
                    var l = len * (i / steps);
                    var p = progress.getPointAtLength(l);
                    _samples.push({ l: l, x: p.x, y: p.y });
                }
            }
            var best = 0, bestD = Infinity;
            for (var j = 0; j < _samples.length; j++) {
                var dd = Math.hypot(_samples[j].x - target.x, _samples[j].y - target.y);
                if (dd < bestD) { bestD = dd; best = _samples[j].l; }
            }
            return best;
        }

        function nodePathProgress(node, index, len) {
            if (index === nodes.length - 1) return 1;
            return lengthAtPoint(getMarkerCenter(node), len) / len;
        }

        // Map the on-screen "reveal line" (~72% down the viewport) to a point on
        // the path. The serpentine only ever descends, so sample Y increases
        // monotonically with path length — the farthest sample at/above the
        // reveal line is the frontier. This keeps the comet just below the last
        // in-view card on load, then leading the draw as you scroll, instead of
        // jumping ahead off-screen.
        var REVEAL_FRACTION = 0.85;
        function frontierProgress() {
            if (!pathLength || !_samples) return state.progress;
            var tlTop = timeline.getBoundingClientRect().top;
            var revealY = window.innerHeight * REVEAL_FRACTION - tlTop;
            // The line/comet render behind the opaque cards, so never let the
            // comet rest within column 3's card — keep it at least COMET_BAND
            // below card 3's bottom (node index 2, the column the line descends).
            if (nodes[2]) {
                var card3Bottom = nodes[2].getBoundingClientRect().bottom - tlTop;
                revealY = Math.max(revealY, card3Bottom + COMET_BAND);
            }
            var frontierL = 0;
            for (var i = 0; i < _samples.length; i++) {
                if (_samples[i].y <= revealY) frontierL = _samples[i].l;
            }
            return Math.min(1, Math.max(0, frontierL / pathLength));
        }
        function updateFromScroll() {
            advanceTo(frontierProgress(), 0.4);
        }

        function revealReachedNodes() {
            // Scan forward only — nodes are in path order so we never need to re-check
            while (nextRevealIndex < nodes.length) {
                if (nodeRevealed[nextRevealIndex]) { nextRevealIndex++; continue; }
                if (drawProgress < (nodeProgresses[nextRevealIndex] || 0)) break;
                var node = nodes[nextRevealIndex];
                nodeRevealed[nextRevealIndex] = true;
                node.classList.add('is-visible');
                if (useGsap) {
                    node.style.background = 'rgba(27,27,27,0)';
                    gsap.fromTo(node,
                        { y: -16, scale: 0.97, backgroundColor: 'rgba(27,27,27,0)' },
                        { y: 0, scale: 1, backgroundColor: 'rgba(27,27,27,1)', duration: 0.55, ease: 'power2.out', overwrite: true,
                          onComplete: function () { node.style.background = '#1b1b1b'; } });
                    var card = node.querySelector('.timeline-card');
                    if (card) gsap.fromTo(card, { opacity: 0 }, { opacity: 1, duration: 0.55, ease: 'power2.out' });
                }
                nextRevealIndex++;
            }
        }

        function renderProgress() {
            drawProgress = state.progress;
            progress.style.strokeDashoffset = pathLength * (1 - drawProgress);
            updateComet(pathLength);
            revealReachedNodes();
            if (drawProgress >= 1) {
                nodes.forEach(function (n, index) {
                    nodeRevealed[index] = true;
                    n.classList.add('is-visible');
                    if (useGsap) {
                        n.style.background = '#1b1b1b';
                        gsap.set(n, { y: 0, scale: 1 });
                        var card = n.querySelector('.timeline-card');
                        if (card) gsap.set(card, { opacity: 1 });
                    }
                });
            }
        }

        function advanceTo(value, duration) {
            var next = Math.max(targetDrawProgress, value);
            targetDrawProgress = Math.min(1, Math.max(0, next));
            if (targetDrawProgress <= state.progress + 0.001) return;

            if (!useGsap) {
                state.progress = targetDrawProgress;
                renderProgress();
                return;
            }

            gsap.to(state, {
                progress: targetDrawProgress,
                duration: duration || 0.85,
                ease: 'power3.out',
                overwrite: true,
                onUpdate: renderProgress
            });
        }

        function destroyScrollTriggers() {
            if (lineTrigger) {
                lineTrigger.kill();
                lineTrigger = null;
            }
            nodeTriggers.forEach(function (trigger) { trigger.kill(); });
            nodeTriggers = [];
        }

        function createScrollTriggers() {
            if (!useGsap) return;
            gsap.registerPlugin(ScrollTrigger);

            destroyScrollTriggers();
            nextRevealIndex = 0;
            nodes.forEach(function (node, index) {
                var card = node.querySelector('.timeline-card');
                if (nodeRevealed[index]) {
                    nextRevealIndex = index + 1; // skip already-revealed nodes
                    node.style.background = '#1b1b1b';
                    gsap.set(node, { y: 0, scale: 1, transformOrigin: '50% 50%' });
                    if (card) gsap.set(card, { opacity: 1 });
                } else {
                    node.style.background = '';   // no background: don't block the track
                    gsap.set(node, { y: -16, scale: 0.97, transformOrigin: '50% 50%' });
                    if (card) gsap.set(card, { opacity: 0 });
                }
            });

            // One trigger fires onUpdate across the whole scroll range; the draw
            // amount comes from frontierProgress() (viewport-relative), not the
            // trigger's own 0..1, so the comet tracks the on-screen reveal line.
            lineTrigger = ScrollTrigger.create({
                trigger: timeline,
                start: 'top bottom',
                endTrigger: nodes[nodes.length - 1] || timeline,
                end: 'bottom top',
                invalidateOnRefresh: true,
                onUpdate: updateFromScroll
            });
        }

        // Fade both scroll hints out for good on the first scroll (CSS media
        // queries decide which one is actually visible per screen size).
        function setupScrollHints() {
            var hints = [document.getElementById('scroll-hint'),
                         document.getElementById('scroll-arrow')];
            function hide() { hints.forEach(function (h) { if (h) h.style.opacity = '0'; }); }
            if (window.scrollY > 8) { hide(); return; }
            window.addEventListener('scroll', hide, { once: true, passive: true });
        }

        function init() {
            build();
            setupScrollHints();
            if (reduceMotion) {
                drawProgress = 1;
                state.progress = 1;
                progress.style.strokeDashoffset = 0;
                nodes.forEach(function (n) { n.classList.add('is-visible'); });
                return;
            }
            if (useGsap) {
                createScrollTriggers();
                ScrollTrigger.refresh();
                updateFromScroll();
            } else {
                advanceTo(1, 0);
            }
        }

        // Recompute geometry on resize (layout flips between 1 and 3 columns).
        var resizeTimer;
        window.addEventListener('resize', function () {
            clearTimeout(resizeTimer);
            resizeTimer = setTimeout(function () {
                build();
                if (!reduceMotion && useGsap) {
                    createScrollTriggers();
                    ScrollTrigger.refresh();
                }
            }, 120);
        });

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
        } else {
            init();
        }
        // Re-measure after fonts/icons settle so markers are positioned correctly.
        window.addEventListener('load', function () {
            build();
            if (!reduceMotion && useGsap) {
                createScrollTriggers();
                ScrollTrigger.refresh();
                updateFromScroll();
            }
        });
    })();
    </script>
</body>
</html>
