Seen on the first run on Cleura 2026-11-06: 13 photos tagged since 2026-09-29 were never synced. portfolio-sync.mjs
sets exit code 1 when one photo fails (DSC00342_PSMS16-square.jpg: "Input image exceeds pixel limit", sharp's default
268 MP) and set -e then stopped before commit/push - on Nurem as well. Now the photos that worked are committed and
built, the run still ends with exit 1 so systemd shows it, and sharp's limit is 1 000 MP (JPEGs shrink while decoding;
the container gets 3 GB).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security review: GIT_URL (user:token@) was stored as the clone's remote URL, and the repo, .git included, was mounted
into the container that runs npm install. Now: umask 077 + 700 directories, the remote is the clean URL, git gets the
token from a credential helper at fetch/push time, and the container sees an empty tmpfs over .git.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deploy/cleura/portfolio-sync.sh: same steps as /opt/immich-sync/run-sync.sh on Nurem (fetch main, portfolio-sync.mjs,
commit + push when photos changed) and then build-live.sh main on Cleura; flock against overlapping runs, the sync
runs in the pinned Node container. install-portfolio-sync.sh puts it in place; secrets stay in
/etc/portal/portfolio-sync.env (root 600). The snippet comment no longer mentions the removed /download/original.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Linus 2026-11-06: everything public lives on imagesbyolofsson.se (Cleura), the customer
subdomain is only for ordering and the logged-in portal; photos are sold as posters
instead of being downloadable.
- Portfolio: download menu (S/M/L), the original password dialog and /download/original
are gone; the lightbox has "Köp som poster" -> /posters instead.
- about.astro and contact.astro removed: Caddy on Cleura sends /about, /contact and
/posters to the portal, so the About text and collaborations are edited once in admin.
They stay in sitemap-0.xml (PORTAL_PAGES); the sitemap index lists /posters/sitemap.xml.
- Menu and 404 page link Posters; privacy, robots.txt and llms.txt no longer mention
password downloads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Homepage images: <Picture> from src/assets (AVIF/WebP/JPG srcset, width/
height, first image eager + fetchpriority high, rest lazy), Swedish alt
texts. (The duplicate EXIF-laden copies in public/3d-scroll were deleted
in 30e385e; README links now point at src/assets.)
- Portfolio: originals are no longer referenced (no .src access), so Astro
does not copy them to dist/. Small/Medium/Large downloads and the lightbox
srcset are EXIF-free JPGs from getImage. Old -s/-m files are no longer
imported.
- Alt texts: Swedish fallback from date (EXIF or filename), never camera
model or "undefined"; portfolio-meta.ts overrides per image.
- Gallery items are <a href> to the large image (lightbox on click);
download options are buttons; no empty H3, no H4 for the password box.
- Stats beacon unchanged; item keys computed at build time (same format).
- Script moved inside the layout so it is no longer rendered after </html>.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvE3C43Uwtv2vtnoFBLDw7
- www -> apex 301, old URLs (/my-people, /animales-and-nature,
/celebrations-with-family, /northen-lights) -> /portfolio 301,
trailing slash and .html -> clean URL 301.
- try_files $uri $uri.html, otherwise 404.html with status 404.
- /_astro/*: public, max-age=31536000, immutable (path-based map, no
duplicate Cache-Control headers); HTML must-revalidate.
- HSTS, Referrer-Policy, Permissions-Policy, X-Frame-Options, CSP that
allows the Meta Pixel and the stats beacon to customer.imagesbyolofsson.se.
- deploy/caddy-site.snippet: the same rules as importable Caddy snippets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvE3C43Uwtv2vtnoFBLDw7
- New /tjanster: genres from existing copy, where/price (contact for offer
while unknown), booking flow via customer portal, delivery in a private
gallery, originals after payment, FAQ (also emitted as FAQPage).
- Homepage: tagline + text section on who/what/where/how to book.
- About: Swedish, photographer framing instead of "by day/by night",
Adobe line. Contact: Swedish, plain mailto info@, booking CTA, fixed
unclosed div. Privacy: Swedish (Google Limited Use line kept in English).
- Unique Swedish title and description per page; scripts moved inside the
layout (no <script> after </html>).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvE3C43Uwtv2vtnoFBLDw7
- astro.config: site, trailingSlash "never", build.format "file".
- /sitemap-index.xml + /sitemap-0.xml as prerendered endpoints (no new
dependency); pages are derived from src/pages/*.astro, 404 excluded.
- robots.txt: all bots allowed, AI crawlers/training bots explicitly
allowed (Linus's decision), /download/ disallowed, sitemap line.
- /llms.txt generated from business.ts, incl. usage-with-attribution note.
- 404.astro in Swedish with links back, noindex.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvE3C43Uwtv2vtnoFBLDw7
- business.ts: single source of truth; unknown facts (city, area, prices,
packages, phone, delivery time, experience, testimonials) are null and are
skipped everywhere. faq.ts builds answers only from known facts.
- portfolio-meta.ts: data hook for real per-image alt texts.
- SEO.astro: title, description, canonical, Open Graph, Twitter, og:image
(EXIF-free JPG via getImage, absolute URL).
- SchemaOrg.astro: @graph with WebSite, Person, ProfessionalService
(sameAs, ReserveAction to the customer portal, offer catalog without
prices), WebPage, optional FAQPage and BreadcrumbList.
- MainLayout: lang=sv, Swedish nav (adds Tjänster), consent banner and
footer in Swedish, lyxlimo/karleksguiden line removed, info@ address.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WvE3C43Uwtv2vtnoFBLDw7
Counts go to customer.imagesbyolofsson.se/api/stats/hit via sendBeacon. No cookie, IP or id stored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Meta Pixel (2453598878495749) loads only after the visitor clicks Accept;
choice stored in localStorage (ibo-consent).
- Decline after a previous Accept clears _fbp/_fbc and reloads.
- "Cookie settings" link in the footer reopens the banner.
- Privacy page: removed "no tracking cookies", added Meta Pixel section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- storleksraderna far en mork hinna vid hover i stallet for ljus
- pilen i knappen gar gra medan menyn ar oppen, vit igen nar den stangs
(bade vid val av storlek och nar man klickar bort menyn)
- knappen trycks ner lite vid klick (active:scale-95)
- neutral-700 i stallet for helvit sa den inte skriker mot bilden
- tunn svart kant + ring/skugga for en sleak kontur
- knappen grupperad med EXIF i en hogergrupp; footern ar justify-between,
darfor hamnade den tidigare mitt i raden
- menyn forankras till hoger pa desktop sa den inte gar utanfor kortet
Knappen var kantig medan allt runt omkring ar rundat. Nu:
- rounded-full + hover:scale-105 som sajtens ovriga CTA-knappar
- nedladdningsikon, uppercase tracking-widest
- menyn rundad med backdrop-blur och mjuk in/ut-overgang
- losenordsrutan rundad med samma kort-kansla som lightboxen
- Escape stanger menyn innan lightboxen
DOWNLOAD-knapp i lightboxen oppnar en storleksmeny. S/M/L pekar pa filer i
repot; Original gar till /download/original och oppnar en losenordsruta.
Losenordet sparas i sessionStorage (bara fliken), och HEAD-anrop anvands sa
fel losenord ger 401 utan att dra igang en 70 MB-nedladdning.
🔴 Globen filtrerar nu bort -s/-m ur galleriet - utan det visades varje bild
tre ganger. Variantkartan (assetId -> hashade URL:er) genereras i frontmattern.
Genererar -s (800px) och -m (1600px) vid sidan av befintlig 3000px-fil.
En nedladdning fran Immich -> tre storlekar. Befintliga 82 bilder backfillade.
Original (27-70 MB/st, ~4 GB totalt) ligger INTE i repot utan hamtas live
via original-proxyn pa /download/original, skyddad av losenord + kontroll
att assetet faktiskt bar portfolio-taggen.
RAW formats (.CR2/.ARW/etc) can't be converted by sharp/libvips. Skip them
with a clear log instead of counting as failures. Tag the JPEG export instead.
- portfolio-sync: GitHub-runner kan ej nå Tailscale-Coolify, förlitar
på Coolifys auto-deploy-on-push istället
- ta bort codeql.yml (tom matrix, ogiltig, failade på varje push)
The previous favicon was the dark logo on transparent which was nearly
invisible. This uses the proper logo with purple-cyan gradient glow that
shows clearly against both light and dark browser tabs.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render logo to square PNG favicons (32/180/512px) and link them in
MainLayout.astro head. Previously the active layout had no favicon.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MainLayout footer now includes attribution: 'Crafting digital
experiences for lyxlimo.se & karleksguiden.se' visible on all pages.
On index page only: hovering lyxlimo.se tints hero-logo with
green (#4fd1c5 teal), hovering karleksguiden.se tints with
magenta (#c026d3). Mouseleave restores default white smoky-path
animation. Mechanism reuses existing data-logo-tint attribute on
#text-wrapper.
Removed logo-pulse functionality entirely (threadLogoIn/Out paths,
runLogoPulse function, LOGO_PROBABILITY, updateLogoThreadPaths,
resize/scroll listeners for logo threads). Hover-based UX replaces
the random pulse approach per Linus design preference.
Threads now lock to logo bracket pin endpoints (where vertical legs
of ::before / ::after bracket-corners end):
- Rosa tråd: från (0, viewport.height) → (logoLeft, logoTop + 40% height)
Quadratic Bezier with cp=(endX, startY) = x^2 curve, går rakt först
och accelererar upp i slutet
- Cyan tråd: från (logoRight, logoBottom - 40% height) → (viewport.width, 0)
Quadratic Bezier with cp=(startX, endY) = -x^2 curve, accelererar upp
i början och decelererar mot slutet
Permanent paths som uppdateras vid resize + scroll + 500ms/1500ms
post-load. runLogoPulse triggar update före varje pulse.
Pulse-blob is inside the SAME SVG as the path, so applyTransform
should use raw SVG coords (pt.x, pt.y) — not getScreenCTM-converted
ones. Double-transform was pushing pulse to SVG-X 9794+ which renders
off-screen-right.
Also phase2 now uses entryPath-end + exitPath-start positions in
SVG-coord-space so the inside-logo glide is continuous with phase1
and phase3.
* fix(pulse): split halves now meet simultaneously + no backtracking
Two bugs fixed after Linus feedback:
1. Path-form: top/bottom split halves used Bezier curves that could
loop back on themselves when entry.x lay outside box X-range.
Now use clamped startX/endX (max(xL+r, min(xR-r, entry.x))) so
pathen alltid rör sig framåt (entry.x → exit.x), aldrig vänder.
2. Synchronized meeting: previously each blob moved at fixed velocity
along its own path → shorter path arrived first and waited.
Now both blobs share a phase2T (0→1) progress fraction, so they
meet at exit-point simultaneously. The path with different length
simply moves at a slightly different speed (averaged). Phase2
duration based on avg(topLen,bottomLen)/velocity so total animation
time stays consistent.
* feat: logo-pulse with pink->green color transition
Adds a 4th pulse target: the IMAGES BY OLOFSSON hero-text logo.
20% chance per pulse-slot when logo is visible (footer scrolled in).
Flow:
1. New purple wave-thread on bottom-left (mirror of cyan wave1 on right)
2. Pulse glides in along purple thread to logo-left edge
3. Pulse enters logo + tints smoky-path stroke pink (#e879f9) + adds
pink text-shadow halo on h1
4. Mid-passage: tint transitions to green (#4fd1c5) — matches default
smoke + cyan glow
5. Pulse exits logo via wave1 (existing cyan thread on right)
6. Logo tint reverts to default (white smokeShift animation)
CSS-side: data-logo-tint attribute on #text-wrapper drives stroke +
text-shadow overrides with 0.6-0.8s transitions for smooth fade.
* fix(logo-pulse): dynamic paths anchored to logo + 50% probability
Previous version tried to use existing wave1+threadPurple paths for
logo-pulse glide-in/out, but those paths are in SVG-fixed coordinates
that don't align with logo's viewport position when hero is scrolled
into view. wave1 has translateX(2283) which pushes it off-screen for
ultrawide viewports.
Fix: build dynamic Bezier paths anchored to the logo's actual
getBoundingClientRect() position at pulse-time:
- Entry path: from (offLeft, logoY+dipOffset) curves up into logo-left
- Exit path: from logo-right curves up to (offRight, logoY-riseOffset)
Paths are appended to box-trail-group, used for one pulse, then
removed in cleanup. No reliance on fixed wave-thread coordinates.
Also raises LOGO_PROBABILITY 20%->50% so the effect is visible more
often when hero is in view.
* fix(logo-pulse): visible entry/exit threads + restore pulse color after
Two bugs fixed:
1. Entry+exit paths were stroke=transparent — pulse appeared to spawn
out of nothing. Now stroke=url(#gradPurple) on entry (subtle pink
thread visible from off-screen left) and stroke=url(#gradMain) on
exit (cyan thread visible going off-screen right). Both at
stroke-width 0.8 + opacity 0.55 to match other wave-thread style.
2. After logo-pulse completed, pulse-gradient stops were left in pink
state — next regular wave-pulse rendered as stale pink instead of
cyan/theme color. Now updatePulseColors(false) is called in cleanup
to restore default cyan-tema.
* fix(logo-pulse): entry/exit threads now properly visible
Previous version used gradient strokes (gradPurple/gradMain) which
have stop-opacity 0 at ends + 0.45 mid, multiplied by path-opacity
0.55 = ~0.25 effective opacity. Practically invisible.
Now solid stroke colors (#e879f9 pink, #4fd1c5 cyan), stroke-width 2
(was 0.8), opacity 0.85 (was 0.55), plus pulseGlowMid filter for soft
halo. Should be clearly visible from off-screen edges to logo.
* feat(logo-pulse): permanent visible threads (no dynamic creation)
Previously dynamic paths were built/destroyed per pulse, were too
thick (stroke-width 2 + glow), and persisted incorrectly causing
visual artifacts.
Now:
- threadLogoIn: permanent purple wave-thread, same style as other
wave-threads (gradPurple stroke, stroke-width 0.7, opacity 0.55)
- threadLogoOut: permanent cyan wave-thread (gradMain stroke 0.7,
opacity 0.50)
- runLogoPulse uses these permanent paths via getPointAtLength
- getScreenCTM converts SVG-coords to screen-coords so pulse follows
thread even with scroll/viewport variance
* fix(logo-threads): align Y-coords to actually cross logo
Logo Y-center is approx 287 in screen-coords. Previous thread Y-coords
(400-600) only grazed logo-bottom or sat entirely below it. Adjusted:
- threadLogoIn: M-1000 500 Q 200 350 850 287 T 2600 200
Kommer från nedre-vänster (Y=500), kurvar UPP till logo-center
(Y=287), fortsätter upp till övre-höger (Y=200)
- threadLogoOut: M-1000 350 Q 400 200 850 287 T 2600 500
Kommer från övre-vänster (Y=350), genom logo-center (Y=287), ner
till nedre-höger (Y=500) — vertikalt motsatt entry-tråd
* fix(pulse): split halves now meet simultaneously + no backtracking
Two bugs fixed after Linus feedback:
1. Path-form: top/bottom split halves used Bezier curves that could
loop back on themselves when entry.x lay outside box X-range.
Now use clamped startX/endX (max(xL+r, min(xR-r, entry.x))) so
pathen alltid rör sig framåt (entry.x → exit.x), aldrig vänder.
2. Synchronized meeting: previously each blob moved at fixed velocity
along its own path → shorter path arrived first and waited.
Now both blobs share a phase2T (0→1) progress fraction, so they
meet at exit-point simultaneously. The path with different length
simply moves at a slightly different speed (averaged). Phase2
duration based on avg(topLen,bottomLen)/velocity so total animation
time stays consistent.
* feat: logo-pulse with pink->green color transition
Adds a 4th pulse target: the IMAGES BY OLOFSSON hero-text logo.
20% chance per pulse-slot when logo is visible (footer scrolled in).
Flow:
1. New purple wave-thread on bottom-left (mirror of cyan wave1 on right)
2. Pulse glides in along purple thread to logo-left edge
3. Pulse enters logo + tints smoky-path stroke pink (#e879f9) + adds
pink text-shadow halo on h1
4. Mid-passage: tint transitions to green (#4fd1c5) — matches default
smoke + cyan glow
5. Pulse exits logo via wave1 (existing cyan thread on right)
6. Logo tint reverts to default (white smokeShift animation)
CSS-side: data-logo-tint attribute on #text-wrapper drives stroke +
text-shadow overrides with 0.6-0.8s transitions for smooth fade.
* fix(logo-pulse): dynamic paths anchored to logo + 50% probability
Previous version tried to use existing wave1+threadPurple paths for
logo-pulse glide-in/out, but those paths are in SVG-fixed coordinates
that don't align with logo's viewport position when hero is scrolled
into view. wave1 has translateX(2283) which pushes it off-screen for
ultrawide viewports.
Fix: build dynamic Bezier paths anchored to the logo's actual
getBoundingClientRect() position at pulse-time:
- Entry path: from (offLeft, logoY+dipOffset) curves up into logo-left
- Exit path: from logo-right curves up to (offRight, logoY-riseOffset)
Paths are appended to box-trail-group, used for one pulse, then
removed in cleanup. No reliance on fixed wave-thread coordinates.
Also raises LOGO_PROBABILITY 20%->50% so the effect is visible more
often when hero is in view.
* fix(logo-pulse): visible entry/exit threads + restore pulse color after
Two bugs fixed:
1. Entry+exit paths were stroke=transparent — pulse appeared to spawn
out of nothing. Now stroke=url(#gradPurple) on entry (subtle pink
thread visible from off-screen left) and stroke=url(#gradMain) on
exit (cyan thread visible going off-screen right). Both at
stroke-width 0.8 + opacity 0.55 to match other wave-thread style.
2. After logo-pulse completed, pulse-gradient stops were left in pink
state — next regular wave-pulse rendered as stale pink instead of
cyan/theme color. Now updatePulseColors(false) is called in cleanup
to restore default cyan-tema.
* fix(logo-pulse): entry/exit threads now properly visible
Previous version used gradient strokes (gradPurple/gradMain) which
have stop-opacity 0 at ends + 0.45 mid, multiplied by path-opacity
0.55 = ~0.25 effective opacity. Practically invisible.
Now solid stroke colors (#e879f9 pink, #4fd1c5 cyan), stroke-width 2
(was 0.8), opacity 0.85 (was 0.55), plus pulseGlowMid filter for soft
halo. Should be clearly visible from off-screen edges to logo.
* feat(logo-pulse): permanent visible threads (no dynamic creation)
Previously dynamic paths were built/destroyed per pulse, were too
thick (stroke-width 2 + glow), and persisted incorrectly causing
visual artifacts.
Now:
- threadLogoIn: permanent purple wave-thread, same style as other
wave-threads (gradPurple stroke, stroke-width 0.7, opacity 0.55)
- threadLogoOut: permanent cyan wave-thread (gradMain stroke 0.7,
opacity 0.50)
- runLogoPulse uses these permanent paths via getPointAtLength
- getScreenCTM converts SVG-coords to screen-coords so pulse follows
thread even with scroll/viewport variance
* fix(pulse): split halves now meet simultaneously + no backtracking
Two bugs fixed after Linus feedback:
1. Path-form: top/bottom split halves used Bezier curves that could
loop back on themselves when entry.x lay outside box X-range.
Now use clamped startX/endX (max(xL+r, min(xR-r, entry.x))) so
pathen alltid rör sig framåt (entry.x → exit.x), aldrig vänder.
2. Synchronized meeting: previously each blob moved at fixed velocity
along its own path → shorter path arrived first and waited.
Now both blobs share a phase2T (0→1) progress fraction, so they
meet at exit-point simultaneously. The path with different length
simply moves at a slightly different speed (averaged). Phase2
duration based on avg(topLen,bottomLen)/velocity so total animation
time stays consistent.
* feat: logo-pulse with pink->green color transition
Adds a 4th pulse target: the IMAGES BY OLOFSSON hero-text logo.
20% chance per pulse-slot when logo is visible (footer scrolled in).
Flow:
1. New purple wave-thread on bottom-left (mirror of cyan wave1 on right)
2. Pulse glides in along purple thread to logo-left edge
3. Pulse enters logo + tints smoky-path stroke pink (#e879f9) + adds
pink text-shadow halo on h1
4. Mid-passage: tint transitions to green (#4fd1c5) — matches default
smoke + cyan glow
5. Pulse exits logo via wave1 (existing cyan thread on right)
6. Logo tint reverts to default (white smokeShift animation)
CSS-side: data-logo-tint attribute on #text-wrapper drives stroke +
text-shadow overrides with 0.6-0.8s transitions for smooth fade.
* fix(logo-pulse): dynamic paths anchored to logo + 50% probability
Previous version tried to use existing wave1+threadPurple paths for
logo-pulse glide-in/out, but those paths are in SVG-fixed coordinates
that don't align with logo's viewport position when hero is scrolled
into view. wave1 has translateX(2283) which pushes it off-screen for
ultrawide viewports.
Fix: build dynamic Bezier paths anchored to the logo's actual
getBoundingClientRect() position at pulse-time:
- Entry path: from (offLeft, logoY+dipOffset) curves up into logo-left
- Exit path: from logo-right curves up to (offRight, logoY-riseOffset)
Paths are appended to box-trail-group, used for one pulse, then
removed in cleanup. No reliance on fixed wave-thread coordinates.
Also raises LOGO_PROBABILITY 20%->50% so the effect is visible more
often when hero is in view.
* fix(logo-pulse): visible entry/exit threads + restore pulse color after
Two bugs fixed:
1. Entry+exit paths were stroke=transparent — pulse appeared to spawn
out of nothing. Now stroke=url(#gradPurple) on entry (subtle pink
thread visible from off-screen left) and stroke=url(#gradMain) on
exit (cyan thread visible going off-screen right). Both at
stroke-width 0.8 + opacity 0.55 to match other wave-thread style.
2. After logo-pulse completed, pulse-gradient stops were left in pink
state — next regular wave-pulse rendered as stale pink instead of
cyan/theme color. Now updatePulseColors(false) is called in cleanup
to restore default cyan-tema.
* fix(logo-pulse): entry/exit threads now properly visible
Previous version used gradient strokes (gradPurple/gradMain) which
have stop-opacity 0 at ends + 0.45 mid, multiplied by path-opacity
0.55 = ~0.25 effective opacity. Practically invisible.
Now solid stroke colors (#e879f9 pink, #4fd1c5 cyan), stroke-width 2
(was 0.8), opacity 0.85 (was 0.55), plus pulseGlowMid filter for soft
halo. Should be clearly visible from off-screen edges to logo.
* fix(pulse): split halves now meet simultaneously + no backtracking
Two bugs fixed after Linus feedback:
1. Path-form: top/bottom split halves used Bezier curves that could
loop back on themselves when entry.x lay outside box X-range.
Now use clamped startX/endX (max(xL+r, min(xR-r, entry.x))) so
pathen alltid rör sig framåt (entry.x → exit.x), aldrig vänder.
2. Synchronized meeting: previously each blob moved at fixed velocity
along its own path → shorter path arrived first and waited.
Now both blobs share a phase2T (0→1) progress fraction, so they
meet at exit-point simultaneously. The path with different length
simply moves at a slightly different speed (averaged). Phase2
duration based on avg(topLen,bottomLen)/velocity so total animation
time stays consistent.
* feat: logo-pulse with pink->green color transition
Adds a 4th pulse target: the IMAGES BY OLOFSSON hero-text logo.
20% chance per pulse-slot when logo is visible (footer scrolled in).
Flow:
1. New purple wave-thread on bottom-left (mirror of cyan wave1 on right)
2. Pulse glides in along purple thread to logo-left edge
3. Pulse enters logo + tints smoky-path stroke pink (#e879f9) + adds
pink text-shadow halo on h1
4. Mid-passage: tint transitions to green (#4fd1c5) — matches default
smoke + cyan glow
5. Pulse exits logo via wave1 (existing cyan thread on right)
6. Logo tint reverts to default (white smokeShift animation)
CSS-side: data-logo-tint attribute on #text-wrapper drives stroke +
text-shadow overrides with 0.6-0.8s transitions for smooth fade.
* fix(logo-pulse): dynamic paths anchored to logo + 50% probability
Previous version tried to use existing wave1+threadPurple paths for
logo-pulse glide-in/out, but those paths are in SVG-fixed coordinates
that don't align with logo's viewport position when hero is scrolled
into view. wave1 has translateX(2283) which pushes it off-screen for
ultrawide viewports.
Fix: build dynamic Bezier paths anchored to the logo's actual
getBoundingClientRect() position at pulse-time:
- Entry path: from (offLeft, logoY+dipOffset) curves up into logo-left
- Exit path: from logo-right curves up to (offRight, logoY-riseOffset)
Paths are appended to box-trail-group, used for one pulse, then
removed in cleanup. No reliance on fixed wave-thread coordinates.
Also raises LOGO_PROBABILITY 20%->50% so the effect is visible more
often when hero is in view.
* fix(logo-pulse): visible entry/exit threads + restore pulse color after
Two bugs fixed:
1. Entry+exit paths were stroke=transparent — pulse appeared to spawn
out of nothing. Now stroke=url(#gradPurple) on entry (subtle pink
thread visible from off-screen left) and stroke=url(#gradMain) on
exit (cyan thread visible going off-screen right). Both at
stroke-width 0.8 + opacity 0.55 to match other wave-thread style.
2. After logo-pulse completed, pulse-gradient stops were left in pink
state — next regular wave-pulse rendered as stale pink instead of
cyan/theme color. Now updatePulseColors(false) is called in cleanup
to restore default cyan-tema.
* fix(pulse): split halves now meet simultaneously + no backtracking
Two bugs fixed after Linus feedback:
1. Path-form: top/bottom split halves used Bezier curves that could
loop back on themselves when entry.x lay outside box X-range.
Now use clamped startX/endX (max(xL+r, min(xR-r, entry.x))) so
pathen alltid rör sig framåt (entry.x → exit.x), aldrig vänder.
2. Synchronized meeting: previously each blob moved at fixed velocity
along its own path → shorter path arrived first and waited.
Now both blobs share a phase2T (0→1) progress fraction, so they
meet at exit-point simultaneously. The path with different length
simply moves at a slightly different speed (averaged). Phase2
duration based on avg(topLen,bottomLen)/velocity so total animation
time stays consistent.
* feat: logo-pulse with pink->green color transition
Adds a 4th pulse target: the IMAGES BY OLOFSSON hero-text logo.
20% chance per pulse-slot when logo is visible (footer scrolled in).
Flow:
1. New purple wave-thread on bottom-left (mirror of cyan wave1 on right)
2. Pulse glides in along purple thread to logo-left edge
3. Pulse enters logo + tints smoky-path stroke pink (#e879f9) + adds
pink text-shadow halo on h1
4. Mid-passage: tint transitions to green (#4fd1c5) — matches default
smoke + cyan glow
5. Pulse exits logo via wave1 (existing cyan thread on right)
6. Logo tint reverts to default (white smokeShift animation)
CSS-side: data-logo-tint attribute on #text-wrapper drives stroke +
text-shadow overrides with 0.6-0.8s transitions for smooth fade.
* fix(logo-pulse): dynamic paths anchored to logo + 50% probability
Previous version tried to use existing wave1+threadPurple paths for
logo-pulse glide-in/out, but those paths are in SVG-fixed coordinates
that don't align with logo's viewport position when hero is scrolled
into view. wave1 has translateX(2283) which pushes it off-screen for
ultrawide viewports.
Fix: build dynamic Bezier paths anchored to the logo's actual
getBoundingClientRect() position at pulse-time:
- Entry path: from (offLeft, logoY+dipOffset) curves up into logo-left
- Exit path: from logo-right curves up to (offRight, logoY-riseOffset)
Paths are appended to box-trail-group, used for one pulse, then
removed in cleanup. No reliance on fixed wave-thread coordinates.
Also raises LOGO_PROBABILITY 20%->50% so the effect is visible more
often when hero is in view.
* fix(pulse): split halves now meet simultaneously + no backtracking
Two bugs fixed after Linus feedback:
1. Path-form: top/bottom split halves used Bezier curves that could
loop back on themselves when entry.x lay outside box X-range.
Now use clamped startX/endX (max(xL+r, min(xR-r, entry.x))) so
pathen alltid rör sig framåt (entry.x → exit.x), aldrig vänder.
2. Synchronized meeting: previously each blob moved at fixed velocity
along its own path → shorter path arrived first and waited.
Now both blobs share a phase2T (0→1) progress fraction, so they
meet at exit-point simultaneously. The path with different length
simply moves at a slightly different speed (averaged). Phase2
duration based on avg(topLen,bottomLen)/velocity so total animation
time stays consistent.
* feat: logo-pulse with pink->green color transition
Adds a 4th pulse target: the IMAGES BY OLOFSSON hero-text logo.
20% chance per pulse-slot when logo is visible (footer scrolled in).
Flow:
1. New purple wave-thread on bottom-left (mirror of cyan wave1 on right)
2. Pulse glides in along purple thread to logo-left edge
3. Pulse enters logo + tints smoky-path stroke pink (#e879f9) + adds
pink text-shadow halo on h1
4. Mid-passage: tint transitions to green (#4fd1c5) — matches default
smoke + cyan glow
5. Pulse exits logo via wave1 (existing cyan thread on right)
6. Logo tint reverts to default (white smokeShift animation)
CSS-side: data-logo-tint attribute on #text-wrapper drives stroke +
text-shadow overrides with 0.6-0.8s transitions for smooth fade.
Two bugs fixed after Linus feedback:
1. Path-form: top/bottom split halves used Bezier curves that could
loop back on themselves when entry.x lay outside box X-range.
Now use clamped startX/endX (max(xL+r, min(xR-r, entry.x))) so
pathen alltid rör sig framåt (entry.x → exit.x), aldrig vänder.
2. Synchronized meeting: previously each blob moved at fixed velocity
along its own path → shorter path arrived first and waited.
Now both blobs share a phase2T (0→1) progress fraction, so they
meet at exit-point simultaneously. The path with different length
simply moves at a slightly different speed (averaged). Phase2
duration based on avg(topLen,bottomLen)/velocity so total animation
time stays consistent.