diff --git a/web/views/timeline.ejs b/web/views/timeline.ejs index 3229e5b..775de6a 100644 --- a/web/views/timeline.ejs +++ b/web/views/timeline.ejs @@ -45,8 +45,8 @@ } @media (min-width: 1024px) { .timeline-hero { - padding-top: 7.5rem; - padding-bottom: 3.25rem; /* room between hero text and row 1 */ + padding-top: 8.5rem; /* clearance below the fixed nav */ + padding-bottom: 1.75rem; /* JS centering adds the rest of the hero→row1 gap */ } } @@ -125,7 +125,7 @@ @media (min-width: 1024px) { .timeline { - margin-top: 2.5rem; + margin-top: 1.5rem; /* base; JS centering adds to this */ } .timeline-grid { grid-template-columns: repeat(3, 1fr); @@ -169,7 +169,7 @@ .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 (applyFirstGap) overrides + /* 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), @@ -191,10 +191,10 @@ already have that room, so they keep the roomier sizing above. */ @media (min-width: 1024px) and (max-height: 1080px) { .timeline-hero { - padding-top: 4.5rem; - padding-bottom: 1.5rem; + padding-top: 6.5rem; /* nav clearance on short screens */ + padding-bottom: 1rem; } - .timeline { margin-top: 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; } @@ -479,27 +479,47 @@ return { x: x, y: y }; } - // Size the gap before row 2 so it sits just below the fold on load, - // measured from the real row-1 bottom and the real viewport height — - // no per-resolution magic numbers. Desktop (3-col) only. - function applyFirstGap() { + // 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; - if (!window.matchMedia('(min-width: 1024px)').matches) { - rowTwo.forEach(function (n) { n.style.marginTop = ''; }); - return; - } - // Measure row 2's natural top with no extra gap (grid row gap only). + + var vh = window.innerHeight; rowTwo.forEach(function (n) { n.style.marginTop = '0px'; }); - var top = rowTwo[0].getBoundingClientRect().top + window.pageYOffset; - // +32 so the marker (which pokes ~1.8rem above the node) clears too. - var gap = Math.max(48, (window.innerHeight + 32) - top); + + // 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; - applyFirstGap(); + 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); @@ -568,7 +588,7 @@ // 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.82; + var REVEAL_FRACTION = 0.85; function frontierProgress() { if (!pathLength || !_samples) return state.progress; var revealY = window.innerHeight * REVEAL_FRACTION - timeline.getBoundingClientRect().top;