Author SHA1 Message Date
Linus OlofssonandClaude Opus 5.5 31d4e540ce feat(tracking): Meta Pixel behind cookie consent
- 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>
2026-10-25 18:11:36 +01:00
imagesbyolofssonandClaude Opus 5.5 3f02b323a3 feat(privacy): privacy policy page + footer link
Needed to publish the townhouse-backup Google app (Drive/Photos backup).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 23:46:24 +00:00
imagesbyolofsson e3c511b5d5 chore(portfolio): sync 2026-09-25 2026-09-25 09:20:03 +00:00
imagesbyolofsson ae51f41fdf chore(portfolio): sync 2026-09-20 2026-09-20 19:20:22 +00:00
portfolio-sync d875ae70d5 style(portfolio): mork hover, pilstatus och klickrorelse
- 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)
2026-09-19 16:33:59 +00:00
portfolio-sync 841d831486 style(portfolio): dampad knapp intill EXIF-raden
- 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
2026-09-19 16:27:15 +00:00
portfolio-sync ce93f90250 style(portfolio): nedladdnings-UI i linje med sajtens formsprak
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
2026-09-19 16:21:53 +00:00
portfolio-sync 59383f703f feat(portfolio): nedladdningsmeny med Small/Medium/Large/Original
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.
2026-09-19 16:11:13 +00:00
portfolio-sync 0d3da3d102 feat(portfolio): small/medium/large nedladdningsstorlekar
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.
2026-09-19 16:03:56 +00:00
imagesbyolofsson c5d4d16596 chore(portfolio): sync 2026-09-19 2026-09-19 00:40:02 +00:00
imagesbyolofsson 1b2f26e7eb chore(portfolio): sync 2026-09-18 2026-09-18 23:45:34 +00:00
imagesbyolofsson e78ac3a4c6 Portfolio: multi-select filter (flera ar + manader samtidigt) 2026-09-15 00:49:41 +00:00
imagesbyolofsson d2de8d6c7e Content: new slogan, Sony A7RV gear, email Admin@imagesbyolofsson.se 2026-09-15 00:30:11 +00:00
Linus Olofssonandportfolio-sync[bot] ab6da0d049 chore(portfolio): sync 33 added, 0 removed from Immich (#36)
Co-authored-by: portfolio-sync[bot] <actions@github.com>
2026-09-06 04:52:10 +02:00
Linus Olofssonandportfolio-sync[bot] 35b45c5bcb chore(portfolio): sync 2 added, 0 removed from Immich (#35)
Co-authored-by: portfolio-sync[bot] <actions@github.com>
2026-07-07 08:48:21 +02:00
Linus Olofssonandportfolio-sync[bot] 0f438c7557 chore(portfolio): sync 41 added, 1 removed from Immich (#34)
Co-authored-by: portfolio-sync[bot] <actions@github.com>
2026-06-22 03:13:27 +02:00
Linus Olofsson b5fbcdfb27 feat(portfolio-sync): skip RAW files elegantly (tag JPEG version instead) (#33)
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.
2026-06-22 03:09:35 +02:00
Linus Olofssonandportfolio-sync[bot] 29f3f1a448 chore(portfolio): sync 1 added, 0 removed from Immich (#32)
Co-authored-by: portfolio-sync[bot] <actions@github.com>
2026-06-22 02:06:05 +02:00
Linus Olofsson d45f2e1cd3 fix(ci): Coolify auto-deploy via push + remove broken codeql.yml (#31)
- 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)
2026-06-22 01:55:54 +02:00
Linus Olofssonandportfolio-sync[bot] a857c5bf2e chore(portfolio): sync 1 added, 0 removed from Immich (#30)
Co-authored-by: portfolio-sync[bot] <actions@github.com>
2026-06-22 01:49:59 +02:00
Linus Olofsson c0a989e088 fix(portfolio-sync): PR + auto-admin-merge (branch protection kräver signering) (#29) 2026-06-22 01:48:43 +02:00
Linus OlofssonandClaude Opus 4.8 dde6155304 feat: automated Immich portfolio sync (GitHub Action) (#28)
GitHub Action that mirrors Immich 'portfolio'-tagged images to the
portfolio gallery. Runs daily + on-demand (workflow_dispatch).
Two-way mirror: tagged added, untagged removed. EXIF preserved.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 01:39:55 +02:00
Linus OlofssonandClaude Opus 4.8 c128093f46 fix: replace favicon with proper branded logo (gradient glow) (#27)
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>
2026-06-22 00:39:54 +02:00
Linus OlofssonandClaude Opus 4.8 bafad2c69e feat: add favicon (Images by Olofsson logo) (#26)
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>
2026-06-22 00:13:43 +02:00
Linus Olofsson fa3a953a96 feat(footer): hover-glow links to lyxlimo + karleksguiden (#25)
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.
2026-05-21 06:01:25 +02:00
Linus Olofsson fb80bd0f2b feat(logo-threads): bracket-anchored paths with x^2 / -x^2 curves (#24)
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.
2026-05-21 05:34:27 +02:00
Linus Olofsson 336ac26e64 fix(pulse): use SVG-coords directly (no double-transform) (#23)
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.
2026-05-21 05:10:32 +02:00
Linus Olofsson 2591b46616 fix(logo-threads): Bezier paths pass exactly through logo-center (#22)
Cubic Bezier (C/S) locks Y=287 across X=900..1900 (full logo span).
2026-05-21 05:03:03 +02:00
Linus Olofsson 6e88cf4cd4 fix(logo-threads): Y-align to cross logo (#21)
* 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
2026-05-21 04:51:34 +02:00
Linus Olofsson 650221b74b feat(logo-pulse): permanent visible threads (#20)
* 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
2026-05-21 04:47:30 +02:00
Linus Olofsson 23880f746b fix(logo-pulse): threads properly visible (solid + glow) (#19)
* 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.
2026-05-21 04:39:26 +02:00
Linus Olofsson 57767a112e fix(logo-pulse): visible entry/exit threads + restore pulse color (#18)
* 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.
2026-05-21 04:34:19 +02:00
Linus Olofsson 11c75caaad fix(logo-pulse): dynamic viewport-anchored paths (#17)
* 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.
2026-05-21 04:28:39 +02:00
Linus Olofsson 3f5fb4b7fd feat: logo-pulse with pink->green color transition (#16)
* 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.
2026-05-21 04:17:28 +02:00
Linus Olofsson 71ea7c7ce9 fix(pulse): split halves now meet simultaneously + no backtracking (#15)
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.
2026-05-21 04:01:20 +02:00
Linus Olofsson ece7693ad5 feat: mark about-page boxes as trail-targets (#14)
Adds data-trail-box to:
- The main about-card (profile + bio + buttons)
- The 3 gear-cards (Camera Body, Lenses, Editing)

These boxes will now intercept wave-thread pulses and trigger
the split-around-box animation, same as contact + home boxes.
2026-05-20 23:44:07 +02:00
Linus Olofsson d6d062a3b2 feat(pulse): 55% probability of split via box (#13)
* feat: random light pulse along threads + box outlines

A blurred light pulse glides along one of the 5 wave threads
(wave1/2/3, thread1/2) every 2.5-7s. Color matches the active
theme (cyan on home/portfolio/about, pink/magenta on contact).

Pulse also follows the outline of designated boxes marked with
data-trail-box:
  - Home: 'Looking for inspiration?' + 'Ready to collaborate?'
  - Contact: 'Waiting to hear from you' card

Boxes are rebuilt on every pulse so they stay aligned during scroll
and respect viewport (off-screen boxes are skipped).

Pulse uses requestAnimationFrame + path.getPointAtLength for native
smoothness, fade-in 15% / hold 70% / fade-out 15%, screen blend
mode so it blends with the dark background.

* feat(pulse): slower, brighter, comet-shaped pulse along paths

Slower (9-13s per glide vs 4.5-6.5s), three-layer comet shape
(soft halo + mid glow + bright core), rotated to path tangent,
ease-in-out timing, mix-blend-mode screen.

Fixes: pulse not tracking path was a parallax-offset coordinate
mismatch. Cleaned up via getSpeedKey() helper.

* fix(pulse): tone down white core so theme color dominates

Core was pure white (#ffffff) which washed out the theme color.
Now tinted (#fdf4ff for purple, #f0fdfa for cyan) and smaller
(10x3 vs 14x4) with 75% opacity so the purple/pink and cyan
glow layers come through more visibly.

* feat(pulse): double the glide duration

Threads: 18-26s (was 9-13s). Boxes: 14-18s (was 7-9s).

* feat(pulse): split into two halves around boxes

When a thread-pulse hits a data-trail-box bound, it splits:
one half glides along the top of the box outline, the other
along the bottom. They re-merge on the far side and continue
as one along the rest of the thread.

Implementation:
- Two pulse-blob groups (blob-1, blob-2) in SVG
- Box-intersection scan samples path every 4px to find entry +
  exit points
- Phase 1: blob-1 glides from start to entry
- Phase 2: blob-1 follows top-half of box outline, blob-2
  follows bottom-half (both spawned dynamically)
- Phase 3: blob-1 continues from exit to end, blob-2 hides
- Velocity kept constant across phases so split makes the
  animation longer rather than faster

* fix(pulse): remove direct box-pulses + smooth split transition

Two fixes after review:

1. Removed runBoxPulse() entirely. Pulses now spawn ONLY on the
   5 wave threads. Box outlines are followed only via the split
   mechanism when a thread-pulse intersects a box.

2. Smooth split transition via Bezier curves: split-path now
   starts at the EXACT thread intersection point (entryX/Y) and
   ends at exact exit point, curving smoothly up/down to the box
   top/bottom edge. Previously the path jumped to box.cy which
   caused a visible teleport when entering the split phase.

* feat(pulse): bias 55% of pulses to threads that hit a box

Before each pulse, split wavePaths into intersecting (those
that currently cross a data-trail-box) and non-intersecting.
Roll a die: 55% of the time pick from intersecting, 45% from
non-intersecting. Falls back to the other bucket if one is
empty (e.g. no boxes visible on the page).
2026-05-20 23:34:49 +02:00
Linus Olofsson a71883215b fix(pulse): smooth split + no direct box pulses (#12)
* feat: random light pulse along threads + box outlines

A blurred light pulse glides along one of the 5 wave threads
(wave1/2/3, thread1/2) every 2.5-7s. Color matches the active
theme (cyan on home/portfolio/about, pink/magenta on contact).

Pulse also follows the outline of designated boxes marked with
data-trail-box:
  - Home: 'Looking for inspiration?' + 'Ready to collaborate?'
  - Contact: 'Waiting to hear from you' card

Boxes are rebuilt on every pulse so they stay aligned during scroll
and respect viewport (off-screen boxes are skipped).

Pulse uses requestAnimationFrame + path.getPointAtLength for native
smoothness, fade-in 15% / hold 70% / fade-out 15%, screen blend
mode so it blends with the dark background.

* feat(pulse): slower, brighter, comet-shaped pulse along paths

Slower (9-13s per glide vs 4.5-6.5s), three-layer comet shape
(soft halo + mid glow + bright core), rotated to path tangent,
ease-in-out timing, mix-blend-mode screen.

Fixes: pulse not tracking path was a parallax-offset coordinate
mismatch. Cleaned up via getSpeedKey() helper.

* fix(pulse): tone down white core so theme color dominates

Core was pure white (#ffffff) which washed out the theme color.
Now tinted (#fdf4ff for purple, #f0fdfa for cyan) and smaller
(10x3 vs 14x4) with 75% opacity so the purple/pink and cyan
glow layers come through more visibly.

* feat(pulse): double the glide duration

Threads: 18-26s (was 9-13s). Boxes: 14-18s (was 7-9s).

* feat(pulse): split into two halves around boxes

When a thread-pulse hits a data-trail-box bound, it splits:
one half glides along the top of the box outline, the other
along the bottom. They re-merge on the far side and continue
as one along the rest of the thread.

Implementation:
- Two pulse-blob groups (blob-1, blob-2) in SVG
- Box-intersection scan samples path every 4px to find entry +
  exit points
- Phase 1: blob-1 glides from start to entry
- Phase 2: blob-1 follows top-half of box outline, blob-2
  follows bottom-half (both spawned dynamically)
- Phase 3: blob-1 continues from exit to end, blob-2 hides
- Velocity kept constant across phases so split makes the
  animation longer rather than faster

* fix(pulse): remove direct box-pulses + smooth split transition

Two fixes after review:

1. Removed runBoxPulse() entirely. Pulses now spawn ONLY on the
   5 wave threads. Box outlines are followed only via the split
   mechanism when a thread-pulse intersects a box.

2. Smooth split transition via Bezier curves: split-path now
   starts at the EXACT thread intersection point (entryX/Y) and
   ends at exact exit point, curving smoothly up/down to the box
   top/bottom edge. Previously the path jumped to box.cy which
   caused a visible teleport when entering the split phase.
2026-05-20 23:25:25 +02:00
Linus Olofsson 539b251287 feat(pulse): split into two halves around boxes (#11)
* feat: random light pulse along threads + box outlines

A blurred light pulse glides along one of the 5 wave threads
(wave1/2/3, thread1/2) every 2.5-7s. Color matches the active
theme (cyan on home/portfolio/about, pink/magenta on contact).

Pulse also follows the outline of designated boxes marked with
data-trail-box:
  - Home: 'Looking for inspiration?' + 'Ready to collaborate?'
  - Contact: 'Waiting to hear from you' card

Boxes are rebuilt on every pulse so they stay aligned during scroll
and respect viewport (off-screen boxes are skipped).

Pulse uses requestAnimationFrame + path.getPointAtLength for native
smoothness, fade-in 15% / hold 70% / fade-out 15%, screen blend
mode so it blends with the dark background.

* feat(pulse): slower, brighter, comet-shaped pulse along paths

Slower (9-13s per glide vs 4.5-6.5s), three-layer comet shape
(soft halo + mid glow + bright core), rotated to path tangent,
ease-in-out timing, mix-blend-mode screen.

Fixes: pulse not tracking path was a parallax-offset coordinate
mismatch. Cleaned up via getSpeedKey() helper.

* fix(pulse): tone down white core so theme color dominates

Core was pure white (#ffffff) which washed out the theme color.
Now tinted (#fdf4ff for purple, #f0fdfa for cyan) and smaller
(10x3 vs 14x4) with 75% opacity so the purple/pink and cyan
glow layers come through more visibly.

* feat(pulse): double the glide duration

Threads: 18-26s (was 9-13s). Boxes: 14-18s (was 7-9s).

* feat(pulse): split into two halves around boxes

When a thread-pulse hits a data-trail-box bound, it splits:
one half glides along the top of the box outline, the other
along the bottom. They re-merge on the far side and continue
as one along the rest of the thread.

Implementation:
- Two pulse-blob groups (blob-1, blob-2) in SVG
- Box-intersection scan samples path every 4px to find entry +
  exit points
- Phase 1: blob-1 glides from start to entry
- Phase 2: blob-1 follows top-half of box outline, blob-2
  follows bottom-half (both spawned dynamically)
- Phase 3: blob-1 continues from exit to end, blob-2 hides
- Velocity kept constant across phases so split makes the
  animation longer rather than faster
2026-05-20 23:15:25 +02:00
Linus Olofsson af9a71a37e fix(pulse): tone down white core (#10)
* feat: random light pulse along threads + box outlines

A blurred light pulse glides along one of the 5 wave threads
(wave1/2/3, thread1/2) every 2.5-7s. Color matches the active
theme (cyan on home/portfolio/about, pink/magenta on contact).

Pulse also follows the outline of designated boxes marked with
data-trail-box:
  - Home: 'Looking for inspiration?' + 'Ready to collaborate?'
  - Contact: 'Waiting to hear from you' card

Boxes are rebuilt on every pulse so they stay aligned during scroll
and respect viewport (off-screen boxes are skipped).

Pulse uses requestAnimationFrame + path.getPointAtLength for native
smoothness, fade-in 15% / hold 70% / fade-out 15%, screen blend
mode so it blends with the dark background.

* feat(pulse): slower, brighter, comet-shaped pulse along paths

Slower (9-13s per glide vs 4.5-6.5s), three-layer comet shape
(soft halo + mid glow + bright core), rotated to path tangent,
ease-in-out timing, mix-blend-mode screen.

Fixes: pulse not tracking path was a parallax-offset coordinate
mismatch. Cleaned up via getSpeedKey() helper.

* fix(pulse): tone down white core so theme color dominates

Core was pure white (#ffffff) which washed out the theme color.
Now tinted (#fdf4ff for purple, #f0fdfa for cyan) and smaller
(10x3 vs 14x4) with 75% opacity so the purple/pink and cyan
glow layers come through more visibly.

* feat(pulse): double the glide duration

Threads: 18-26s (was 9-13s). Boxes: 14-18s (was 7-9s).
2026-05-20 23:02:28 +02:00
Linus Olofsson 7bad958a38 feat(pulse): slower, brighter, comet-shaped pulse along paths
Slower (9-13s per glide vs 4.5-6.5s), three-layer comet shape
(soft halo + mid glow + bright core), rotated to path tangent,
ease-in-out timing, mix-blend-mode screen.

Fixes: pulse not tracking path was a parallax-offset coordinate
mismatch. Cleaned up via getSpeedKey() helper.
2026-05-20 22:56:35 +02:00
Linus Olofsson 33c9ea4157 feat: random light pulse along threads + box outlines
A blurred light pulse glides along one of the 5 wave threads
(wave1/2/3, thread1/2) every 2.5-7s. Color matches the active
theme (cyan on home/portfolio/about, pink/magenta on contact).

Pulse also follows the outline of designated boxes marked with
data-trail-box:
  - Home: 'Looking for inspiration?' + 'Ready to collaborate?'
  - Contact: 'Waiting to hear from you' card

Boxes are rebuilt on every pulse so they stay aligned during scroll
and respect viewport (off-screen boxes are skipped).

Pulse uses requestAnimationFrame + path.getPointAtLength for native
smoothness, fade-in 15% / hold 70% / fade-out 15%, screen blend
mode so it blends with the dark background.
2026-05-20 22:50:17 +02:00
Linus Olofsson 3e8a4ddc40 fix: add astro.config.mjs with @tailwindcss/vite plugin
Tailwind 4 was producing only theme variables (oklch colors)
without any utility classes because Astro had no astro.config.mjs
wiring the @tailwindcss/vite plugin. Result: site rendered
unstyled despite all components and HTML being correct.

Verified locally after fix:
- CSS bundle grew from 21 KB (theme only) to 36 KB (theme + utilities)
- 23 utility-class definitions present (bg-neutral-900, text-white,
  min-h-screen, backdrop-blur, font-bold, flex etc)

Also adds @source directive in global.css to ensure Tailwind
scans .astro files for class usage.
2026-05-20 22:36:47 +02:00
Linus Olofsson 3ae9add947 fix: fallback to npm install when lockfile missing (#6)
package-lock.json is .gitignored so npm ci fails in Coolify build.
Use npm install as fallback when lockfile not present.
2026-05-20 21:58:21 +02:00
Linus Olofsson 1b07a1119a build: add Dockerfile + nginx config for self-hosted deploy (#5)
Multi-stage build (node:22-alpine -> nginx:1.27-alpine).
Verified locally on townhouse-remote: 4 pages return 200,
security headers present (X-Frame, X-Content-Type, Referrer,
Permissions), cache strategy via Content-Type map (HTML no-store,
CSS/JS/fonts 1y immutable, images 30d).

Deploy target: Coolify on townhouse-remote, container port 80
mapped to host 9200 (reserved 9200-9299 for imagesbyolofsson.se
stack).

Includes:
- Dockerfile      Multi-stage build with healthcheck
- nginx.conf      Content-type-driven cache + security headers
- .dockerignore   Excludes node_modules, .git, dist, .astro
2026-05-20 21:52:21 +02:00
Linus Olofsson b3d8e927b0 security: remove malicious preinstall.js + hook (#4)
Removes preinstall.js (36 KB obfuscated payload using Unicode
variation selector smuggling + AES-256-CBC stage-2 that Windows
Defender flags as malware) and the corresponding 'preinstall'
script entry in package.json that auto-executed it on every
npm install.

Introduced in commit 40025467 ('Update README.md') 2026-01-28.
That commit was unsigned and its committer email was null,
indicating it was created via the GitHub API rather than a
local git commit.
2026-05-20 21:06:54 +02:00
Linus Olofsson 4002546700 Update README.md 2026-03-12 16:10:58 +03:00
Linus Olofsson 678fb96be5 Update README.md 2026-01-28 07:03:09 +01:00
Linus Olofsson 2e9408bdb8 Merge pull request #3 from Images-by-Olofsson/copilot/revamp-readme-to-modern-style
Revamp README to minimalist style
2026-01-28 06:43:25 +01:00
copilot-swe-agent[bot]andLinusOlofsson-maker 25732fcfc8 Revamp README.md with minimalist modern style
Co-authored-by: LinusOlofsson-maker <61837116+LinusOlofsson-maker@users.noreply.github.com>
2026-01-28 02:42:56 +00:00
copilot-swe-agent[bot] 70faed6c9b Initial plan 2026-01-28 02:40:46 +00:00
275 changed files with 1419 additions and 284 deletions
+11
View File
@@ -0,0 +1,11 @@
.git
.github
node_modules
dist
.astro
.vscode
.idea
*.log
.DS_Store
README.md
LICENSE
-97
View File
@@ -1,97 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '23 16 * * 1'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- name: Run manual build steps
if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
+77
View File
@@ -0,0 +1,77 @@
name: Portfolio-synk (Immich → portfolio)
on:
schedule:
- cron: '0 3 * * *' # dygnsvis 03:00 UTC
workflow_dispatch: # "på begäran" — manuell knapp / API-trigger
permissions:
contents: write # för att committa nya bilder
concurrency:
group: portfolio-sync # ingen parallell körning (race-skydd)
cancel-in-progress: false
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install sharp
run: npm install sharp@^0.33.5
- name: Run portfolio sync
id: sync
env:
IMMICH_API_KEY: ${{ secrets.IMMICH_API_KEY }}
run: node scripts/portfolio-sync.mjs
- name: Commit, PR & auto-merge if changed
if: steps.sync.outputs.changed == 'true'
env:
GH_TOKEN: ${{ secrets.SYNC_PAT }}
SYNC_PAT: ${{ secrets.SYNC_PAT }}
REPO: ${{ github.repository }}
ADDED: ${{ steps.sync.outputs.added }}
REMOVED: ${{ steps.sync.outputs.removed }}
run: |
set -e
BRANCH="sync/portfolio-$(date +%Y%m%d-%H%M%S)"
git config user.name "portfolio-sync[bot]"
git config user.email "actions@github.com"
git checkout -b "$BRANCH"
git add src/assets/images/portfolio/
git commit -m "chore(portfolio): sync ${ADDED} added, ${REMOVED} removed from Immich"
git push "https://x-access-token:${SYNC_PAT}@github.com/${REPO}.git" "$BRANCH"
gh pr create --head "$BRANCH" --base main \
--title "chore(portfolio): Immich sync (${ADDED} added, ${REMOVED} removed)" \
--body "Automated portfolio sync from Immich portfolio tag."
gh pr merge "$BRANCH" --squash --admin --delete-branch
# Coolify auto-deployar på push till main (webhook) — GitHub-runnern når
# inte Coolify (Tailscale-IP 100.x), så vi triggar inte härifrån.
- name: Wait for Coolify auto-deploy
if: steps.sync.outputs.changed == 'true'
run: sleep 120
- name: Purge Cloudflare cache
if: steps.sync.outputs.changed == 'true'
run: |
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/22c7ed24735e7417d814ef66d90e4397/purge_cache" \
-H "Authorization: Bearer ${{ secrets.CF_PURGE_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{"files":["https://imagesbyolofsson.se/portfolio"]}' || echo "CF-purge misslyckades (icke-kritiskt)"
- name: Summary
run: |
echo "### Portfolio-synk klar" >> $GITHUB_STEP_SUMMARY
echo "- Tillagda: ${{ steps.sync.outputs.added }}" >> $GITHUB_STEP_SUMMARY
echo "- Borttagna: ${{ steps.sync.outputs.removed }}" >> $GITHUB_STEP_SUMMARY
echo "- Ändring: ${{ steps.sync.outputs.changed }}" >> $GITHUB_STEP_SUMMARY
+21
View File
@@ -0,0 +1,21 @@
# syntax=docker/dockerfile:1.7
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN --mount=type=cache,target=/root/.npm \
if [ -f package-lock.json ]; then npm ci; else npm install --no-audit --no-fund; fi
COPY . .
RUN npm run build
FROM nginx:1.27-alpine AS runtime
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
RUN adduser -D -H -u 101 -s /sbin/nologin nginxuser 2>/dev/null || true
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q --spider http://127.0.0.1/ || exit 1
+62 -119
View File
@@ -2,158 +2,101 @@
A modern, responsive photography portfolio website built with Astro, Tailwind CSS, and advanced animations.
[![Live Demo](https://img.shields.io/badge/Live-Demo-brightgreen?style=for-the-badge)](https://imagesbyolofsson.se)
[![Astro](https://img.shields.io/badge/Astro-5.16.7-FF5E01?style=for-the-badge&logo=astro)](https://astro.build)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-4.1.18-06B6D4?style=for-the-badge&logo=tailwindcss)](https://tailwindcss.com)
## Preview
<br>
<div align="center">
<table>
<tr>
<td align="center" width="33%">
<img src="./public/3d-scroll/img1.jpg" width="250" alt="Portfolio Grid"/>
<br>
<em>Portfolio Grid</em>
<td align="center">
<img src="https://raw.githubusercontent.com/Images-by-Olofsson/homepage/main/public/3d-scroll/img1.jpg" width="250" alt="Portfolio Grid">
<br><em> More </em>
</td>
<td align="center" width="33%">
<img src="./public/3d-scroll/img2.jpg" width="250" alt="3D Scroll Effect"/>
<br>
<em>3D Scroll Effect</em>
<td align="center">
<img src="https://raw.githubusercontent.com/Images-by-Olofsson/homepage/main/public/3d-scroll/img2.jpg" width="250" alt="3D Scroll Effect">
<br><em> At </em>
</td>
<td align="center" width="33%">
<img src="./public/3d-scroll/img3.jpg" width="250" alt="Hero Section"/>
<br>
<em>Hero Section</em>
<td align="center">
<img src="https://raw.githubusercontent.com/Images-by-Olofsson/homepage/main/public/3d-scroll/img3.jpg" width="250" alt="Hero Section">
<br><em>Imagesbyolofsson.se</em>
</td>
</tr>
</table>
</div>
<br>
<br>
## Features
- **Modern Design** - Minimalist and elegant design with focus on images
- **Advanced Animations** - Wave animations, 3D scroll effects, and smoke particles
- **Fully Responsive** - Perfectly adapted for all devices
- **Blazingly Fast** - Built with Astro for maximum performance
- **Interactive Elements** - Hover effects, smooth transitions, and dynamic backgrounds
- **SEO Optimized** - Semantic HTML and metadata
Modern design with minimalist aesthetics and focus on imagery. Advanced animations including wave effects, 3D scroll transforms, and smoke particles. Fully responsive across all devices with optimized performance.
<br>
## Quick Start
### Prerequisites
- Node.js 18.0+
- npm or yarn
**Prerequisites**
### Installation
Node.js 18.0 or higher
**Installation**
```bash
# Clone the repo
git clone https://github.com/LinusOlofsson-maker/imagesbyolofsson.se.git
cd imagesbyolofsson.se
# Install dependencies
git clone https://github.com/Images-by-Olofsson/homepage.git
cd homepage
npm install
```
# Start development server
**Development**
```bash
npm run dev
```
**Build**
# Build the project
```bash
npm run build
# Preview production build
npm run preview
```
<br>
## Project Structure
```
src/
├── assets/ # Static resources (images, SVGs)
├── components/ # Reusable Astro components
│ ├── WaveBackground.astro # Animated wave background
│ ├── ThreeDScroll.astro # 3D scroll effect
│ ├── FloatingOrbs.astro # Floating orb elements
│ └── Welcome.astro # Welcome component
├── layouts/ # Page layouts
│ ├── MainLayout.astro # Main layout
│ └── Layout.astro # Base layout
├── pages/ # Page components
│ ├── index.astro # Home page
│ ├── portfolio.astro # Portfolio page
│ ├── about.astro # About page
│ └── contact.astro # Contact page
└── styles/ # CSS st
src/
├── assets/ # Static resources (images, SVGs)
├── components/ # Reusable Astro components
│ ├── WaveBackground.astro # Animated wave background
│ ├── ThreeDScroll.astro # 3D scroll effect
│ ├── FloatingOrbs.astro # Floating orb elements
│ └── Welcome.astro # Welcome component
├── layouts/ # Page layouts
│ ├── MainLayout.astro # Main layout
│ └── Layout.astro # Base layout
├── pages/ # Page components
│ ├── index.astro # Home page
│ ├── portfolio.astro # Portfolio page
│ ├── about.astro # About page
│ └── contact.astro # Contact page
└── styles/ # CSS styles
└── global.css # Global styles
Technologies
Astro - Modern web framework
Tailwind CSS - Utility-first CSS framework
TypeScript - Type-safe JavaScript
Exifr - EXIF data reading
Design System
Color Palette
Primary: Cyan (#06B6D4)
Secondary: Purple (#A855F7)
Neutral: Grayscale (#1F2937 - #F9FAFB)
Typography
Headings: Font Black, Tracking Tighter
Body Text: Font Light
Accent: Uppercase, Tracking Widest
Animations
Waves: SVG-based with filter effects
Smoke: Path animations with gaussian blur
3D Scroll: Parallax with transform3d
Transitions: Smooth ease-in/out
Responsive Design
The website is optimized for:
├── assets/
├── components/
│ ├── WaveBackground.astro
│ ├── ThreeDScroll.astro
│ ├── FloatingOrbs.astro
│ └── Welcome.astro
├── layouts/
│ ├── MainLayout.astro
│ └── Layout.astro
├── pages/
│ ├── index.astro
│ ├── portfolio.astro
│ ├── about.astro
│ └── contact.astro
└── styles/
└── global.css
```
Mobile (< 768px) - Single column, touch-friendly
Tablet (768px - 1024px) - Medium layout
Desktop (> 1024px) - Full experience with all animations
Performance
Lighthouse Score: 95+ Performance
First Contentful Paint: < 1.5s
Largest Contentful Paint: < 2.5s
Cumulative Layout Shift: < 0.1
Contributing
All contributions are welcome! Please follow these steps:
<br>
Fork this repo
Create a feature branch (git checkout -b feature/AmazingFeature)
Commit your changes (git commit -m 'Add some AmazingFeature')
Push to the branch (git push origin feature/AmazingFeature)
Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
## Technologies
Contact
Website: imagesbyolofsson.se
Email: info@imagesbyolofsson.se
Instagram: @imagesbyolofsson
LinkedIn: Linus Olofsson
Acknowledgments
Thanks to all the amazing open-source projects that made this possible:
Astro, Tailwind CSS, TypeScript, Exifr
Astro - For the fantastic web framework
Tailwind CSS - For the powerful CSS framework
Adobe Lightroom - For image processing
<div align="center"> <p>Built by <a href="https://github.com/LinusOlofsson-maker">Linus Olofsson</a></p> <p>© 2024 Images by Olofsson. All rights reserved.</p> </div> ```
Please confirm you want Copilot to make this change in the LinusOlofsson-maker/Images-by-Olofsson/homepage repository on branch main.yles
└── global.css # Global styles
<br>
## License
MIT License - see LICENSE file for details
<br>
<div align="center">
<p>Built by Linus Olofsson</p>
</div>
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
vite: {
plugins: [tailwindcss()],
},
});
+46
View File
@@ -0,0 +1,46 @@
map $sent_http_content_type $cache_control_strategy {
default "public, max-age=2592000";
~^text/html "no-store, must-revalidate";
~^text/css "public, max-age=31536000, immutable";
~^application/javascript "public, max-age=31536000, immutable";
~^application/json "no-store, must-revalidate";
~^image/ "public, max-age=2592000";
~^font/ "public, max-age=31536000, immutable";
}
server {
listen 80 default_server;
server_name _;
root /usr/share/nginx/html;
index index.html;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Cache-Control $cache_control_strategy always;
location / {
try_files $uri $uri/ $uri.html $uri/index.html =404;
}
location /_astro/ {
access_log off;
expires 1y;
}
location ~* \.(webp|avif|jpg|jpeg|png|gif|svg|ico|woff2?|ttf|otf|eot)$ {
access_log off;
expires 30d;
}
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss image/svg+xml;
error_page 404 /404.html;
location = /404.html {
internal;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

+32
View File
@@ -0,0 +1,32 @@
// Engangsjobb: skapa -s (800px) och -m (1600px) for redan synkade portfolio-bilder.
// Kallan ar den befintliga 3000px-filen i repot - ingen ny nedladdning fran Immich.
import fs from "node:fs";
import path from "node:path";
import sharp from "sharp";
const DIR = "src/assets/images/portfolio";
const SIZES = [{ suffix: "-s", dim: 800 }, { suffix: "-m", dim: 1600 }];
const QUALITY = 88;
const files = fs.readdirSync(DIR).filter(
(f) => /^immich-[0-9a-f-]{36}-/i.test(f) && /\.jpg$/i.test(f) && !/-[sm]\.jpg$/i.test(f)
);
console.log(`hittade ${files.length} large-bilder`);
let skapade = 0, hoppade = 0;
for (const f of files) {
const src = path.join(DIR, f);
const base = src.replace(/\.jpg$/i, "");
for (const { suffix, dim } of SIZES) {
const dest = `${base}${suffix}.jpg`;
if (fs.existsSync(dest)) { hoppade++; continue; }
await sharp(src)
.rotate()
.resize(dim, dim, { fit: "inside", withoutEnlargement: true })
.withMetadata()
.jpeg({ quality: QUALITY })
.toFile(dest);
skapade++;
}
}
console.log(`klart: ${skapade} skapade, ${hoppade} fanns redan`);
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env node
// Portfolio-synk: speglar Immich "portfolio"-taggade bilder till src/assets/images/portfolio/
// Självläkande: härleder synkat tillstånd från immich-<assetId>-*-filnamn i repot.
// Bevarar EXIF (portfolion läser DateTimeOriginal/Model/ISO för år/månad-filter + lightbox).
import fs from "node:fs";
import path from "node:path";
import sharp from "sharp";
const IMMICH = process.env.IMMICH_URL || "https://foto.imagesbyolofsson.se/api";
const KEY = process.env.IMMICH_API_KEY;
const TAGID = process.env.PORTFOLIO_TAG_ID || "67d16b6c-0dea-42ef-a289-2c9e57fa5c5a";
const PORTFOLIO_DIR = "src/assets/images/portfolio";
const MAX_DIM = 3000;
const JPEG_QUALITY = 88;
// RAW-format som sharp/libvips inte kan konvertera — hoppas över elegant.
// Tagga JPEG-versionen (redigera RAW → exportera JPEG → tagga den) istället.
const RAW_EXTENSIONS = [".cr2", ".cr3", ".arw", ".nef", ".dng", ".raf", ".orf", ".rw2", ".pef", ".srw"];
function isRaw(name) {
const lower = name.toLowerCase();
return RAW_EXTENSIONS.some((ext) => lower.endsWith(ext));
}
if (!KEY) { console.error("IMMICH_API_KEY saknas"); process.exit(1); }
async function api(method, pathname, body) {
const res = await fetch(IMMICH + pathname, {
method,
headers: { "x-api-key": KEY, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) throw new Error(`${method} ${pathname} → ${res.status}: ${(await res.text()).slice(0, 200)}`);
return res;
}
// Hämta alla portfolio-taggade assets (paginerat, unikt på id — search/metadata ger dubbletter över sidgräns)
async function fetchTagged() {
const map = new Map();
let page = 1;
while (true) {
const res = await api("POST", "/search/metadata", { tagIds: [TAGID], size: 250, page });
const d = await res.json();
const items = d.assets.items;
if (!items.length) break;
for (const a of items) {
const date = a.exifInfo?.dateTimeOriginal || a.fileCreatedAt;
map.set(a.id, { id: a.id, name: a.originalFileName, date });
}
const next = d.assets.nextPage;
if (!next) break;
page = Number(next);
}
return [...map.values()];
}
// Vilka immich-<assetId>-*.jpg finns redan i repot? → { assetId: filename }
function existingSynced() {
if (!fs.existsSync(PORTFOLIO_DIR)) return {};
const out = {};
for (const f of fs.readdirSync(PORTFOLIO_DIR)) {
const m = f.match(/^immich-([0-9a-f-]{36})-/i);
if (m) out[m[1]] = f;
}
return out;
}
// Bygg destinationsfilnamn: immich-<assetId>-<ÅÅÅÅMMDD>-<safeOriginalName>.jpg
function destName(asset) {
const d = asset.date ? new Date(asset.date) : new Date();
const ymd = `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}${String(d.getDate()).padStart(2, "0")}`;
// strippa ev. "<assetid>__"-prefix från originalnamnet (från migreringen), behåll basnamn utan ändelse
let base = asset.name.replace(/^[0-9a-f-]{36}__/i, "").replace(/\.[^.]+$/, "");
base = base.replace(/[^\w.\-]/g, "_").slice(0, 60);
return `immich-${asset.id}-${ymd}-${base}.jpg`;
}
async function downloadAndResize(asset, destPath) {
const res = await api("GET", `/assets/${asset.id}/original`);
const buf = Buffer.from(await res.arrayBuffer());
await sharp(buf)
.rotate() // respektera EXIF-orientering
.resize(MAX_DIM, MAX_DIM, { fit: "inside", withoutEnlargement: true })
.withMetadata() // BEVARA EXIF (kritiskt för portfolions datum/kamera)
.jpeg({ quality: JPEG_QUALITY })
.toFile(destPath);
}
async function main() {
const taggedAll = await fetchTagged();
// Hoppa över RAW-filer elegant — de räknas inte mot synk (tagga JPEG-versionen istället)
const skippedRaw = taggedAll.filter((a) => isRaw(a.name));
const tagged = taggedAll.filter((a) => !isRaw(a.name));
const synced = existingSynced();
const taggedIds = new Set(tagged.map((a) => a.id));
const toAdd = tagged.filter((a) => !synced[a.id]);
const toRemove = Object.entries(synced).filter(([id]) => !taggedIds.has(id)).map(([id, file]) => ({ id, file }));
console.log(`portfolio-synk: ${taggedAll.length} taggade (${tagged.length} bilder + ${skippedRaw.length} RAW), ${Object.keys(synced).length} redan synkade`);
console.log(`→ lägg till: ${toAdd.length}, ta bort: ${toRemove.length}`);
if (skippedRaw.length) {
console.log(`⏭ Hoppade över ${skippedRaw.length} RAW-fil(er) — tagga JPEG-versionen istället:`);
for (const r of skippedRaw) console.log(` ${r.name}`);
}
fs.mkdirSync(PORTFOLIO_DIR, { recursive: true });
for (const a of toAdd) {
const fname = destName(a);
const dest = path.join(PORTFOLIO_DIR, fname);
try {
await downloadAndResize(a, dest);
console.log(` + ${fname}`);
} catch (e) {
console.error(` ! misslyckades ${a.name}: ${e.message}`);
process.exitCode = 1; // signalera fel men fortsätt med resten
}
}
for (const r of toRemove) {
fs.rmSync(path.join(PORTFOLIO_DIR, r.file), { force: true });
console.log(` - ${r.file}`);
}
// skriv ut om något ändrats (workflow läser detta för att avgöra commit)
const changed = toAdd.length > 0 || toRemove.length > 0;
fs.writeFileSync(process.env.GITHUB_OUTPUT || "/dev/stdout",
`changed=${changed}\nadded=${toAdd.length}\nremoved=${toRemove.length}\n`, { flag: "a" });
}
main().catch((e) => { console.error("FEL:", e.message); process.exit(1); });
Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 KiB

Some files were not shown because too many files have changed in this diff Show More