From 095e515fe3fa792feb22272877526273c0ff74cc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 19:12:38 +0000 Subject: [PATCH 01/11] feat(services): the trunk's tip blazes with a glowing trail as it grows A white-hot head with a fading glow trail rides the scroll-scrubbed trunk tip on the same timeline and range, flaring up after each release and burning out into the next. Only with scroll timelines and motion allowed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y8bgwtK8JrdWVHhi7dm9yW --- src/lib/components/organisms/Services.svelte | 50 ++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/src/lib/components/organisms/Services.svelte b/src/lib/components/organisms/Services.svelte index 5b53664..73f9197 100644 --- a/src/lib/components/organisms/Services.svelte +++ b/src/lib/components/organisms/Services.svelte @@ -454,6 +454,7 @@ ); --node-center: calc(var(--node-top) + var(--node) / 2); --trunk-color: rgba(var(--color-accent-rgb), var(--opacity-border)); + --trunk-top: 0px; grid-area: log; align-self: center; @@ -472,15 +473,18 @@ &::before { content: ''; position: absolute; - top: 0; + top: var(--trunk-top); bottom: 0; left: calc(var(--trunk-x) - 1px); width: 2px; background: var(--trunk-color); } + &:first-child { + --trunk-top: calc(-1 * var(--space-block)); + } + &:first-child::before { - top: calc(-1 * var(--space-block)); background: linear-gradient( to bottom, transparent, @@ -568,7 +572,7 @@ /* Motion: the confetti floats on its own, slow and out of step, further the nearer it sits. * Scroll scrubs the log against a scan line 60% down the viewport: the trunk's tip follows - * it, each small commit pops in as the tip reaches it, and each release lands with a flash of + * it, blazing with a glowing trail, each small commit pops in as the tip reaches it, and each release lands with a flash of * the Halo. Without scroll timelines the log rests drawn; with reduced motion everything * rests. */ @media (prefers-reduced-motion: no-preference) { @@ -593,6 +597,31 @@ animation-timeline: --release; animation-range: entry-crossing 0% entry-crossing 100%; } + + /* The blaze: a white-hot head on the tip with a glowing trail behind it, on the + * same timeline and range as the trunk so it rides exactly on the tip. It flares + * up after each release and burns out into the next. */ + &::after { + content: ''; + position: absolute; + left: calc(var(--trunk-x) - 1px); + width: 2px; + height: var(--space-major); + translate: 0 -100%; + border-radius: var(--radius-full); + background: linear-gradient( + to bottom, + transparent, + var(--color-accent) 75%, + var(--color-sparkle) + ); + filter: drop-shadow(0 0 3px var(--color-accent)) + drop-shadow(0 0 3px var(--color-accent)); + pointer-events: none; + animation: blaze linear both; + animation-timeline: --release; + animation-range: entry-crossing 0% entry-crossing 100%; + } } .node { @@ -621,6 +650,21 @@ } } + @keyframes blaze { + 0% { + top: var(--trunk-top); + opacity: 0; + } + 8%, + 88% { + opacity: 1; + } + 100% { + top: 100%; + opacity: 0; + } + } + @keyframes merge { 0% { scale: 0.4; From cc0458f6dbbb40e2191aa20ba2d77be6b499b165 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 19:22:55 +0000 Subject: [PATCH 02/11] feat(services): commits and releases form molten where the blaze strikes Each commit dot and release node is born as a white-hot blob, morphing through uneven border radii and cooling from sparkle to accent into its circle, with an ember glow that outlasts the pop like the trail behind the tip. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y8bgwtK8JrdWVHhi7dm9yW --- src/lib/components/organisms/Services.svelte | 69 +++++++++++++++++--- 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/src/lib/components/organisms/Services.svelte b/src/lib/components/organisms/Services.svelte index 73f9197..98656df 100644 --- a/src/lib/components/organisms/Services.svelte +++ b/src/lib/components/organisms/Services.svelte @@ -572,9 +572,9 @@ /* Motion: the confetti floats on its own, slow and out of step, further the nearer it sits. * Scroll scrubs the log against a scan line 60% down the viewport: the trunk's tip follows - * it, blazing with a glowing trail, each small commit pops in as the tip reaches it, and each release lands with a flash of - * the Halo. Without scroll timelines the log rests drawn; with reduced motion everything - * rests. */ + * it, blazing with a glowing trail; each small commit pops in molten as the tip reaches it, + * and each release lands with a flash of the Halo. Without scroll timelines the log rests + * drawn; with reduced motion everything rests. */ @media (prefers-reduced-motion: no-preference) { .glyph :global(svg) { animation: float var(--float) ease-in-out var(--phase) infinite; @@ -625,10 +625,15 @@ } .node { - animation: merge linear both; - animation-timeline: --release; - animation-range: entry var(--node-top) entry - calc(var(--node-center) + var(--pop)); + animation: + merge linear both, + ember linear both; + animation-timeline: --release, --release; + animation-range: + entry var(--node-top) entry + calc(var(--node-center) + var(--pop)), + entry var(--node-top) entry + calc(var(--node-center) + var(--pop) * 3); } .commit { @@ -636,9 +641,13 @@ view-timeline-inset: var(--scan-inset); &::before { - animation: commit linear both; - animation-timeline: --commit; - animation-range: entry 0% entry calc(0% + var(--pop) / 2); + animation: + commit linear both, + ember linear both; + animation-timeline: --commit, --commit; + animation-range: + entry 0% entry calc(0% + var(--pop) / 2), + entry 0% entry calc(0% + var(--pop) * 1.5); } } } @@ -665,19 +674,35 @@ } } + /* Releases and commits are struck by the blaze: each forms as a molten blob, white-hot like + * the trail's head, wobbles through uneven radii and cools into its circle. The ember glow + * outlasts the pop, fading as slowly as the trail behind the tip. The blob radii are shapes, + * not design radii; they all settle on a circle. */ @keyframes merge { 0% { scale: 0.4; opacity: 0; + border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; + background: var(--color-sparkle); + } + 30% { + border-radius: 62% 38% 34% 66% / 58% 32% 68% 42%; + background: var(--color-sparkle); } 55% { scale: 1.15; opacity: 1; + border-radius: 40% 60% 58% 42% / 62% 44% 56% 38%; box-shadow: 0 0 1px 7px rgba(var(--color-accent-rgb), var(--opacity-border)); } + 78% { + border-radius: 54% 46% 44% 56% / 46% 56% 44% 54%; + } 100% { scale: 1; + border-radius: 50%; + background: var(--color-accent); box-shadow: 0 0 0 0 rgba(var(--color-accent-rgb), 0); } } @@ -686,13 +711,37 @@ 0% { scale: 0; opacity: 0; + border-radius: 70% 30% 40% 60% / 40% 70% 30% 60%; + background: var(--color-sparkle); + } + 35% { + border-radius: 35% 65% 60% 40% / 65% 35% 65% 35%; + background: var(--color-sparkle); } 60% { scale: 1.5; opacity: 1; + border-radius: 58% 42% 38% 62% / 42% 60% 40% 58%; } 100% { scale: 1; + border-radius: 50%; + background: var(--color-accent); + } + } + + @keyframes ember { + 0% { + filter: drop-shadow(0 0 0 rgba(var(--color-accent-rgb), 0)) + drop-shadow(0 0 0 rgba(var(--color-accent-rgb), 0)); + } + 12% { + filter: drop-shadow(0 0 3px var(--color-sparkle)) + drop-shadow(0 0 3px var(--color-accent)); + } + 100% { + filter: drop-shadow(0 0 0 rgba(var(--color-accent-rgb), 0)) + drop-shadow(0 0 0 rgba(var(--color-accent-rgb), 0)); } } From 6614b5c899f8b1fe315ee1d1aea5101986ace20c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 19:27:04 +0000 Subject: [PATCH 03/11] fix(services): keep the blaze behind the badges, and calm the molten pop The trail was painted over release nodes and commit dots, and the fading-in node let the trunk show through. Badges now stack above the trunk and turn opaque early. Gentler blob radii and overshoot, a shorter white-hot phase and a single, softer, shorter ember glow; the trail drops its doubled glow. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y8bgwtK8JrdWVHhi7dm9yW --- src/lib/components/organisms/Services.svelte | 52 +++++++++----------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/src/lib/components/organisms/Services.svelte b/src/lib/components/organisms/Services.svelte index 98656df..bcec347 100644 --- a/src/lib/components/organisms/Services.svelte +++ b/src/lib/components/organisms/Services.svelte @@ -532,6 +532,8 @@ * subject. */ .commits { position: absolute; + /* Above the trunk and its blaze, which are drawn by the li's pseudo-elements. */ + z-index: 1; top: calc(var(--node-top) + var(--node)); bottom: 0; left: calc(var(--trunk-x) - var(--commit) / 2); @@ -559,6 +561,7 @@ /* The merged badge: solid forge, like the banner's status chip, carrying the service's glyph. */ .node { position: absolute; + z-index: 1; top: var(--node-top); left: calc(var(--trunk-x) - var(--node) / 2); display: grid; @@ -615,8 +618,7 @@ var(--color-accent) 75%, var(--color-sparkle) ); - filter: drop-shadow(0 0 3px var(--color-accent)) - drop-shadow(0 0 3px var(--color-accent)); + filter: drop-shadow(0 0 3px var(--color-accent)); pointer-events: none; animation: blaze linear both; animation-timeline: --release; @@ -633,7 +635,7 @@ entry var(--node-top) entry calc(var(--node-center) + var(--pop)), entry var(--node-top) entry - calc(var(--node-center) + var(--pop) * 3); + calc(var(--node-center) + var(--pop) * 2); } .commit { @@ -647,7 +649,7 @@ animation-timeline: --commit, --commit; animation-range: entry 0% entry calc(0% + var(--pop) / 2), - entry 0% entry calc(0% + var(--pop) * 1.5); + entry 0% entry calc(0% + var(--pop)); } } } @@ -682,23 +684,21 @@ 0% { scale: 0.4; opacity: 0; - border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; + border-radius: 42% 58% 56% 44% / 44% 42% 58% 56%; background: var(--color-sparkle); } - 30% { - border-radius: 62% 38% 34% 66% / 58% 32% 68% 42%; + 15% { + opacity: 1; + } + 20% { background: var(--color-sparkle); } 55% { - scale: 1.15; - opacity: 1; - border-radius: 40% 60% 58% 42% / 62% 44% 56% 38%; + scale: 1.1; + border-radius: 46% 54% 53% 47% / 54% 47% 53% 46%; box-shadow: 0 0 1px 7px rgba(var(--color-accent-rgb), var(--opacity-border)); } - 78% { - border-radius: 54% 46% 44% 56% / 46% 56% 44% 54%; - } 100% { scale: 1; border-radius: 50%; @@ -711,17 +711,16 @@ 0% { scale: 0; opacity: 0; - border-radius: 70% 30% 40% 60% / 40% 70% 30% 60%; + border-radius: 58% 42% 46% 54% / 46% 58% 42% 54%; background: var(--color-sparkle); } - 35% { - border-radius: 35% 65% 60% 40% / 65% 35% 65% 35%; + 20% { + opacity: 1; background: var(--color-sparkle); } 60% { - scale: 1.5; - opacity: 1; - border-radius: 58% 42% 38% 62% / 42% 60% 40% 58%; + scale: 1.3; + border-radius: 47% 53% 54% 46% / 53% 46% 54% 47%; } 100% { scale: 1; @@ -731,17 +730,14 @@ } @keyframes ember { - 0% { - filter: drop-shadow(0 0 0 rgba(var(--color-accent-rgb), 0)) - drop-shadow(0 0 0 rgba(var(--color-accent-rgb), 0)); + 0%, + 100% { + filter: drop-shadow(0 0 0 rgba(var(--color-accent-rgb), 0)); } 12% { - filter: drop-shadow(0 0 3px var(--color-sparkle)) - drop-shadow(0 0 3px var(--color-accent)); - } - 100% { - filter: drop-shadow(0 0 0 rgba(var(--color-accent-rgb), 0)) - drop-shadow(0 0 0 rgba(var(--color-accent-rgb), 0)); + filter: drop-shadow( + 0 0 3px rgba(var(--color-accent-rgb), var(--opacity-text-muted)) + ); } } From 342bf0e1b6328d4f7af4f83125c6ce2fd07b2fe8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 19:41:37 +0000 Subject: [PATCH 04/11] style(services): tone the blaze and molten pop down a notch Half-strength glows on the trail and embers, a dimmer trail head, a shorter white-hot phase and smaller overshoots. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y8bgwtK8JrdWVHhi7dm9yW --- src/lib/components/organisms/Services.svelte | 25 +++++++++++++------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/lib/components/organisms/Services.svelte b/src/lib/components/organisms/Services.svelte index bcec347..7b99239 100644 --- a/src/lib/components/organisms/Services.svelte +++ b/src/lib/components/organisms/Services.svelte @@ -615,10 +615,17 @@ background: linear-gradient( to bottom, transparent, - var(--color-accent) 75%, - var(--color-sparkle) + var(--color-accent) 80%, + color-mix( + in srgb, + var(--color-sparkle) 60%, + var(--color-accent) + ) + ); + filter: drop-shadow( + 0 0 3px + rgba(var(--color-accent-rgb), var(--opacity-half)) ); - filter: drop-shadow(0 0 3px var(--color-accent)); pointer-events: none; animation: blaze linear both; animation-timeline: --release; @@ -690,11 +697,11 @@ 15% { opacity: 1; } - 20% { + 12% { background: var(--color-sparkle); } 55% { - scale: 1.1; + scale: 1.06; border-radius: 46% 54% 53% 47% / 54% 47% 53% 46%; box-shadow: 0 0 1px 7px rgba(var(--color-accent-rgb), var(--opacity-border)); @@ -714,12 +721,14 @@ border-radius: 58% 42% 46% 54% / 46% 58% 42% 54%; background: var(--color-sparkle); } + 12% { + background: var(--color-sparkle); + } 20% { opacity: 1; - background: var(--color-sparkle); } 60% { - scale: 1.3; + scale: 1.2; border-radius: 47% 53% 54% 46% / 53% 46% 54% 47%; } 100% { @@ -736,7 +745,7 @@ } 12% { filter: drop-shadow( - 0 0 3px rgba(var(--color-accent-rgb), var(--opacity-text-muted)) + 0 0 3px rgba(var(--color-accent-rgb), var(--opacity-half)) ); } } From 6df18147df5885c48aa8e63a2b78d0854b1a5421 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 19:44:19 +0000 Subject: [PATCH 05/11] feat(services): balls take in the blaze like liquid Where the trail's head strikes, commits and releases form as a drop pulled up towards it, splat as it lands and wobble like jelly (damped stretch and squash) into their circle, replacing the random blob radii. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y8bgwtK8JrdWVHhi7dm9yW --- src/lib/components/organisms/Services.svelte | 58 +++++++++++++++----- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/src/lib/components/organisms/Services.svelte b/src/lib/components/organisms/Services.svelte index 7b99239..35a7186 100644 --- a/src/lib/components/organisms/Services.svelte +++ b/src/lib/components/organisms/Services.svelte @@ -683,29 +683,47 @@ } } - /* Releases and commits are struck by the blaze: each forms as a molten blob, white-hot like - * the trail's head, wobbles through uneven radii and cools into its circle. The ember glow - * outlasts the pop, fading as slowly as the trail behind the tip. The blob radii are shapes, - * not design radii; they all settle on a circle. */ + /* Releases and commits are struck by the blaze and take it in like liquid: each forms as a + * white-hot drop pulled up towards the trail's head, splats as the head lands, then wobbles + * like jelly (stretch and squash, damped) and cools into its circle. The ember glow outlasts + * the wobble, fading as slowly as the trail behind the tip. The radii are drop shapes, not + * design radii; they all settle on a circle. */ @keyframes merge { 0% { - scale: 0.4; + scale: 0.4 0.55; opacity: 0; - border-radius: 42% 58% 56% 44% / 44% 42% 58% 56%; + border-radius: 50% 50% 50% 50% / 70% 70% 30% 30%; + background: var(--color-sparkle); + } + 12% { background: var(--color-sparkle); } 15% { opacity: 1; } - 12% { - background: var(--color-sparkle); + 28% { + scale: 0.88 1.14; + border-radius: 50% 50% 50% 50% / 64% 64% 36% 36%; + } + 42% { + scale: 1.1 0.9; + border-radius: 50% 50% 50% 50% / 44% 44% 56% 56%; } 55% { - scale: 1.06; - border-radius: 46% 54% 53% 47% / 54% 47% 53% 46%; box-shadow: 0 0 1px 7px rgba(var(--color-accent-rgb), var(--opacity-border)); } + 58% { + scale: 0.96 1.05; + border-radius: 50% 50% 50% 50% / 54% 54% 46% 46%; + } + 74% { + scale: 1.02 0.98; + border-radius: 50%; + } + 88% { + scale: 0.995 1.01; + } 100% { scale: 1; border-radius: 50%; @@ -718,7 +736,7 @@ 0% { scale: 0; opacity: 0; - border-radius: 58% 42% 46% 54% / 46% 58% 42% 54%; + border-radius: 50% 50% 50% 50% / 72% 72% 28% 28%; background: var(--color-sparkle); } 12% { @@ -727,9 +745,21 @@ 20% { opacity: 1; } - 60% { - scale: 1.2; - border-radius: 47% 53% 54% 46% / 53% 46% 54% 47%; + 32% { + scale: 0.85 1.25; + border-radius: 50% 50% 50% 50% / 66% 66% 34% 34%; + } + 50% { + scale: 1.2 0.85; + border-radius: 50% 50% 50% 50% / 42% 42% 58% 58%; + } + 68% { + scale: 0.95 1.06; + border-radius: 50% 50% 50% 50% / 54% 54% 46% 46%; + } + 84% { + scale: 1.02 0.98; + border-radius: 50%; } 100% { scale: 1; From 6c9b05ec942329655c45cfb7c980bcb758b26b51 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 19:50:39 +0000 Subject: [PATCH 06/11] feat(services): a release's colour runs into its title as the blaze lands When the node is struck, its title fills with the accent from left to right: a soft-edged gradient clipped to the text, scrubbed on the release's scroll timeline, so scrolling back drains it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y8bgwtK8JrdWVHhi7dm9yW --- src/lib/components/organisms/Services.svelte | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/lib/components/organisms/Services.svelte b/src/lib/components/organisms/Services.svelte index 35a7186..f566733 100644 --- a/src/lib/components/organisms/Services.svelte +++ b/src/lib/components/organisms/Services.svelte @@ -645,6 +645,25 @@ calc(var(--node-center) + var(--pop) * 2); } + /* The release's colour runs into its title as the blaze lands: a soft-edged fill + * sweeping left to right, scrubbed with the node, so scrolling back drains it. */ + .log h3 { + background: linear-gradient( + to right, + var(--color-accent-text) 46%, + var(--color-text) 54% + ) + 100% 0 / 220% 100% no-repeat; + /* The sweep spans the words, not the whole column. */ + width: fit-content; + background-clip: text; + color: transparent; + animation: kindle linear both; + animation-timeline: --release; + animation-range: entry var(--node-center) entry + calc(var(--node-center) + var(--pop) * 2); + } + .commit { view-timeline: --commit block; view-timeline-inset: var(--scan-inset); @@ -688,6 +707,12 @@ * like jelly (stretch and squash, damped) and cools into its circle. The ember glow outlasts * the wobble, fading as slowly as the trail behind the tip. The radii are drop shapes, not * design radii; they all settle on a circle. */ + @keyframes kindle { + to { + background-position: 0 0; + } + } + @keyframes merge { 0% { scale: 0.4 0.55; From 6864374ff185b56801f9d7798665be173b1bcd20 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 19:55:51 +0000 Subject: [PATCH 07/11] feat(services): titles reveal like secret ink with a fine glitter edge Unrevealed titles wait slightly faded; the kindle sweep gets a sparkle seam at its edge and a band of fine twinkling dust (three sparse speck grids of co-prime sizes, masked to ride the edge on the same scroll timeline, twinkling on the clock). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y8bgwtK8JrdWVHhi7dm9yW --- src/lib/components/organisms/Services.svelte | 99 ++++++++++++++++++-- 1 file changed, 91 insertions(+), 8 deletions(-) diff --git a/src/lib/components/organisms/Services.svelte b/src/lib/components/organisms/Services.svelte index f566733..4c647b5 100644 --- a/src/lib/components/organisms/Services.svelte +++ b/src/lib/components/organisms/Services.svelte @@ -645,23 +645,69 @@ calc(var(--node-center) + var(--pop) * 2); } - /* The release's colour runs into its title as the blaze lands: a soft-edged fill - * sweeping left to right, scrubbed with the node, so scrolling back drains it. */ + /* The release's colour runs into its title as the blaze lands, like secret ink in a + * magic book: the title waits a little faded, then a glinting edge sweeps left to + * right, leaving the accent behind it and a fine twinkling dust along it. Scrubbed + * with the node, so scrolling back drains it. */ .log h3 { + --sweep: calc(var(--node-center) + var(--pop) * 2); + + position: relative; + /* The sweep spans the words, not the whole column. */ + width: fit-content; background: linear-gradient( to right, - var(--color-accent-text) 46%, - var(--color-text) 54% + var(--color-accent-text) 44%, + var(--color-sparkle) 48%, + rgba(var(--color-text-rgb), var(--opacity-text-muted)) + 54% ) 100% 0 / 220% 100% no-repeat; - /* The sweep spans the words, not the whole column. */ - width: fit-content; background-clip: text; color: transparent; animation: kindle linear both; animation-timeline: --release; - animation-range: entry var(--node-center) entry - calc(var(--node-center) + var(--pop) * 2); + animation-range: entry var(--node-center) entry var(--sweep); + + /* The dust: three sparse speck grids of co-prime sizes read as random glitter, shown only in a + * soft band that rides on the sweep's edge, twinkling on the clock. */ + &::after { + content: ''; + position: absolute; + inset: -0.3em 0; + pointer-events: none; + background: + radial-gradient( + circle at 30% 40%, + var(--color-sparkle) 0 0.6px, + transparent 1.1px + ) + 0 0 / 17px 13px, + radial-gradient( + circle at 70% 25%, + var(--color-accent-text) 0 0.5px, + transparent 1px + ) + 0 0 / 23px 19px, + radial-gradient( + circle at 45% 75%, + var(--color-sparkle) 0 0.7px, + transparent 1.2px + ) + 0 0 / 29px 11px; + mask: linear-gradient( + to right, + transparent, + black 40% 60%, + transparent + ) -36% + 0 / 30% 100% no-repeat; + animation: + dust linear both, + twinkle 0.9s steps(1) infinite; + animation-timeline: --release, auto; + animation-range: entry var(--node-center) entry var(--sweep); + } } .commit { @@ -713,6 +759,43 @@ } } + /* The band's centre tracks the kindle edge, which runs from -10% to 110% of the title. */ + @keyframes dust { + 0% { + mask-position: -36% 0; + opacity: 0; + } + 10%, + 85% { + opacity: var(--opacity-half); + } + 100% { + mask-position: 136% 0; + opacity: 0; + } + } + + @keyframes twinkle { + 0% { + background-position: + 0 0, + 0 0, + 0 0; + } + 33% { + background-position: + 3px 2px, + -4px 3px, + 5px -2px; + } + 66% { + background-position: + -2px 4px, + 2px -3px, + -5px 1px; + } + } + @keyframes merge { 0% { scale: 0.4 0.55; From b54983836908cd8dbb678a052002ddb5298503fd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 20:00:25 +0000 Subject: [PATCH 08/11] feat(services): title glitter faints when scrolling rests A passive window scroll listener flags the band while scrolling (150ms idle timeout); the dust fades in quickly while it moves and faints over 0.6s once it rests, via filter so it composes with the sweep's opacity. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y8bgwtK8JrdWVHhi7dm9yW --- src/lib/components/organisms/Services.svelte | 27 ++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/lib/components/organisms/Services.svelte b/src/lib/components/organisms/Services.svelte index 4c647b5..a121200 100644 --- a/src/lib/components/organisms/Services.svelte +++ b/src/lib/components/organisms/Services.svelte @@ -133,6 +133,18 @@ $effect(() => () => cancelAnimationFrame(frame)); + // The title glitter only sparkles while the page is moving: it faints once scrolling rests. + let scrolling = $state(false); + let resting: ReturnType | undefined; + + function scrolled() { + scrolling = true; + clearTimeout(resting); + resting = setTimeout(() => (scrolling = false), 150); + } + + $effect(() => () => clearTimeout(resting)); + // The cell edges between seeds, each drawn once, without the field's border: clip the field // by the bisector to every other seed, and track which seed made each edge of the cell. function voronoi( @@ -218,9 +230,12 @@ ]; + +
Date: Sun, 4 Oct 2026 20:10:58 +0000 Subject: [PATCH 09/11] style(services): a faint glint on the title glitter Each speck gets a 2px half-strength sparkle drop-shadow, in the same filter list as the idle fade so the two interpolate together. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y8bgwtK8JrdWVHhi7dm9yW --- src/lib/components/organisms/Services.svelte | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/lib/components/organisms/Services.svelte b/src/lib/components/organisms/Services.svelte index a121200..fa1bb42 100644 --- a/src/lib/components/organisms/Services.svelte +++ b/src/lib/components/organisms/Services.svelte @@ -686,13 +686,22 @@ /* The dust: three sparse speck grids of co-prime sizes read as random glitter, * shown only in a soft band that rides on the sweep's edge, twinkling on the - * clock. It faints when scrolling rests (filter, as the sweep owns opacity). */ + * clock, each speck with a faint glint. It faints when scrolling rests (filter, as + * the sweep owns opacity). */ &::after { content: ''; position: absolute; inset: -0.3em 0; pointer-events: none; - filter: opacity(0); + --glint: drop-shadow( + 0 0 2px + color-mix( + in srgb, + var(--color-sparkle) 50%, + transparent + ) + ); + filter: opacity(0) var(--glint); transition: filter 0.6s var(--ease-3); background: radial-gradient( @@ -772,7 +781,7 @@ * the wobble, fading as slowly as the trail behind the tip. The radii are drop shapes, not * design radii; they all settle on a circle. */ .scrolling .log h3::after { - filter: opacity(1); + filter: opacity(1) var(--glint); transition-duration: 0.15s; } From 763af9f9641c13a3c7c77cb964bccdf5eea24653 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 20:15:34 +0000 Subject: [PATCH 10/11] feat(services): titles kindle fully within the release's merge The title sweep now shares the node's merge range end, so each title is completely in ember by the time its release settles, instead of trailing on for another pop of scroll. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y8bgwtK8JrdWVHhi7dm9yW --- src/lib/components/organisms/Services.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/components/organisms/Services.svelte b/src/lib/components/organisms/Services.svelte index fa1bb42..031aa3e 100644 --- a/src/lib/components/organisms/Services.svelte +++ b/src/lib/components/organisms/Services.svelte @@ -665,7 +665,9 @@ * right, leaving the accent behind it and a fine twinkling dust along it. Scrubbed * with the node, so scrolling back drains it. */ .log h3 { - --sweep: calc(var(--node-center) + var(--pop) * 2); + /* Ends with the node's merge: by the time the release settles, its title is + * fully kindled. */ + --sweep: calc(var(--node-center) + var(--pop)); position: relative; /* The sweep spans the words, not the whole column. */ From c100cdd3b790f62ad6f197a81499b2e3cb801d55 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 20:23:40 +0000 Subject: [PATCH 11/11] feat(services): titles kindle on the strike, not with the scroll An IntersectionObserver with the scan line as its root edge marks a release kindled once its node's centre crosses it; the title then plays its glitter reveal on the clock (0.9s) and is put out again when scrolled back above the line. The idle-scroll gate on the glitter goes: the dust now only exists during the timed reveal. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y8bgwtK8JrdWVHhi7dm9yW --- src/lib/components/organisms/Services.svelte | 77 ++++++++++---------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/src/lib/components/organisms/Services.svelte b/src/lib/components/organisms/Services.svelte index 031aa3e..9e21a39 100644 --- a/src/lib/components/organisms/Services.svelte +++ b/src/lib/components/organisms/Services.svelte @@ -133,17 +133,27 @@ $effect(() => () => cancelAnimationFrame(frame)); - // The title glitter only sparkles while the page is moving: it faints once scrolling rests. - let scrolling = $state(false); - let resting: ReturnType | undefined; - - function scrolled() { - scrolling = true; - clearTimeout(resting); - resting = setTimeout(() => (scrolling = false), 150); - } - - $effect(() => () => clearTimeout(resting)); + // A release's title kindles as an event, not scrubbed: once the blaze strikes the node (its + // centre crosses the scan line, 60% down the viewport, where the trunk's tip rides), the + // title plays its reveal on the clock. Scrolling back above the line puts it out again. + let nodes: HTMLElement[] = $state([]); + let kindled = $state(services.map(() => false)); + + $effect(() => { + const observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + const index = nodes.indexOf(entry.target as HTMLElement); + const { top, height } = entry.boundingClientRect; + const line = entry.rootBounds?.bottom ?? innerHeight * 0.6; + kindled[index] = top + height / 2 < line; + } + }, + { rootMargin: '0px 0px -40% 0px', threshold: [0, 0.5, 1] } + ); + for (const node of nodes) observer.observe(node); + return () => observer.disconnect(); + }); // The cell edges between seeds, each drawn once, without the field's border: clip the field // by the bisector to every other seed, and track which seed made each edge of the cell. @@ -230,12 +240,9 @@ ]; - -
    - {#each services as service (service)} -
  1. -
  2. +