When fullscreen is active, clicking a marker now triggers fsBtn.click()
to exit cleanly (handles class, body overflow, tripMap.resize + icon),
then waits 450ms for the exit animation before scrolling to the entry
and firing the highlight. Also fixes missing icon-swap CSS for
.home-map-col.is-fullscreen (was only targeting .feed-map-wrap).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
Attribution: MapLibre v4 uses <details> and may open it after load
regardless of compact:true — remove the open attribute in the load
handler to guarantee collapsed state.
Button: switch from teal to --color-canvas (#22201B) so it sits quietly
against the dark map; icon reads in --color-ink (warm cream).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
Button in bottom-right of #trip-map (z-index:1000), hidden ≥769px.
Attribution moved to bottom-left to free the corner. Clicking toggles
.is-fullscreen on .home-map-col (position:fixed, 100dvh), locks body
scroll, and calls tripMap.resize() for MapLibre to re-render.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
Button is back inside #feed-map with z-index:1000 to clear all MapLibre
layers. Attribution control disabled in constructor and re-added to
bottom-left so bottom-right is free for the fullscreen button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
MapLibre's attribution button occupies bottom-right of the container.
Moving our button out of the map div avoids MapLibre's DOM entirely,
and top-right is clear of all default MapLibre controls.
Position anchor moves to feed-map-wrap (position:relative).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
Button in the bottom-right corner of the map, hidden ≥769px. Clicking
it toggles .is-fullscreen on .feed-map-wrap (position:fixed, full
viewport), locks body scroll, and calls feedMap.resize() so MapLibre
re-renders at the new size. Icon swaps between expand SVG and ✕.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
- Radius: trip-panel-toggle now uses --radius-full, consistent with filter pills
- Animation: stats/cycling blocks use CSS grid-template-rows 0fr→1fr transition
(inner trip-panel-inner div carries decoration so border/padding don't peek
out when collapsed; display:none removed)
- Close button: ↑ Close stats / ↑ Close cycling at bottom of each panel,
hidden ≥769px, triggers the header toggle via data-toggle attribute
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
Filter bar now has one job: content type (All/Journal/Stories) + sort icon (↑/↓).
Stats and Cycling move to a lean text-button row below the bar with a
rotating ▾ caret — CSS handles expand/collapse state via .is-active,
no JS changes needed for the caret animation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
Button sits in the right filter group alongside Stats/Cycling.
Default state: ascending (↑ Oldest first, no highlight).
Toggled state: descending (↓ Newest first, is-active pill style).
DOM reversal uses insertBefore against the anchored #feed-filter-empty
so the empty-state message stays last regardless of sort direction.
Interacts safely with the type filter (show/hide by data-type).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
pswp.currSlide is a Slide instance whose DOM element is stored as
.container (.pswp__zoom-wrap). The .el property belongs to the
itemHolder wrapper, not the Slide — so currSlide.el was always
undefined, the null-guard exited early, and no animation played.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
Previous approach (CSS transition + reflow trick) is unreliable in
Firefox. New approach: PhotoSwipe emits 'change' synchronously before
painting; we add a direction-aware CSS keyframe animation to the
incoming slide element, with animation-fill-mode:both so there is no
flash before the animation starts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
PhotoSwipe's goTo() moves slides instantly (no spring animation unlike
swipe). Intercepts keydown in capture phase, sets a CSS transition and
forces a reflow before PhotoSwipe moves the container, so the browser
animates from the old position to the new one. Cleans up on close.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
Creates partials/entry-journal.html.twig and partials/entry-story.html.twig
so trip, dailies, and home all use the same up-to-date markup. Home page
gains PhotoSwipe, blurred fill, adaptive aspect ratio, and hash-based
marker scroll. Future changes only need to happen in one place.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
Portrait entries (first image taller than wide) get a 4:5 container —
Instagram's proven cap that prevents single photos dominating the screen.
Landscape entries keep 4:3. Aspect-ratio moved from slide to wrap so the
strip inherits it via height:100%.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
- object-fit: contain + ::before blurred background fill on all slides
- dots moved inside photo-wrap, overlaid at bottom with shadow for contrast
- arrows hidden on touch devices via @media (hover: none)
- margin-bottom increased for breathing room below photo block
- trip.html.twig brought up to parity with dailies: same structure,
same PhotoSwipe init, same expand button, same dot overlay
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
Move PhotoSwipe CSS from per-entry assets.addCss() (runs after head is
committed) to a single <link> tag at block start. Restore the expand
button as the reliable mobile tap target — it dispatches a synthetic
click on the visible <a> slide, which bubbles to PhotoSwipe's gallery
handler. Merge dot sync into the single module script.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
Drops ~80 lines of fragile custom lightbox JS/HTML/CSS in favour of
PhotoSwipe v5 loaded from CDN. Slides are now <a> tags (the pswp-gallery
children) which always fire click events on iOS even inside scroll
containers — the root cause of the mobile tap issue. Pinch-to-zoom and
swipe-to-dismiss come for free. Dot sync kept as a separate vanilla JS
block.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
- Aspect ratio 3:2 → 4:3 (less aggressive crop; closer to phone native)
- Slides become <button> elements with data-full pointing to original image
- Tap/click any photo in the feed opens a full-screen lightbox showing the
uncropped original; prev/next browses all feed photos; Esc/arrows/backdrop
click close
- Dot indicator now syncs with scroll via IntersectionObserver
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
Replaces the boolean toggle with a select field offering:
on — connect all consecutive entries (chronological line)
manual — force_connect entries only (user-controlled connections)
intelligent_gpx — suppress connectors where GPX covers both endpoints;
force_connect overrides (original smart logic, restored)
off — no connectors at all; force_connect also ignored
buildJourneySegments gains an optional trackpointsPerFile param used
only by intelligent_gpx mode. renderGpxJourney extracts trackpoints
only when connectMode is intelligent_gpx. dailies.html.twig falls
back from intelligent_gpx → on (mini-map has no GPX tracks to
suppress against).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
autoconnect:true now connects every consecutive entry pair in
chronological order; the old proximity check (suppress where GPX
covers the route) is removed entirely.
- buildJourneySegments: drops allTrackpoints/thresholdKm params;
logic is now force_connect || autoconnect (binary, no GPX math)
- renderGpxJourney: no longer extracts trackpoints; just renders
visual GPX layers then calls buildJourneySegments
- dailies.html.twig: removes GPX URL collection, toGeoJSON CDN load,
and the Promise.all — connectors are now synchronous
- extractTrackpoints/isNearTrack/haversineKm removed (dead code)
- blueprint help text updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
Adds two configurable toggles to the trip blueprint (Admin2 Trip tab):
- use_gpx: show/hide GPX tracks on all maps (default: enabled)
- autoconnect: draw connector lines between markers (default: enabled)
When use_gpx is off, GPX files are not fetched or rendered on any map
(home, map, trip, dailies). The stats panel in trip.html.twig still
reads GPX_URLS directly and is unaffected.
When autoconnect is off, buildJourneySegments suppresses all
auto-connectors; only entries with force_connect:true still draw a
line — making force_connect behaviour independent of both settings.
Also refactors the inline Promise.all in trip.html.twig to use the
shared renderGpxJourney utility (reducing duplication).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
The home map was drawing an initial addJourneyLine, then trying to remove
layer 'home-journey' in the Promise.all callback — but addJourneyLine names
the layer 'home-journey-line', so removeLayer was a no-op and removeSource
failed (layer still referencing the source), leaving a ghost line on top of
the GPX tracks.
Extract the Promise.all → GPX tracks → buildJourneySegments → addJourneySegments
pattern into MapUtils.renderGpxJourney() and replace both map.html.twig and
home.html.twig with the shared call. No upfront journey line is drawn — the
function handles the no-GPX case correctly via Promise.all([]).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
user/config/media.yaml defines only 'gpx', which replaces the system
media.types instead of merging (blueprint-unaware key replacement). This
causes page.media[hero_image] to return undefined for jpg/png files.
Fallback constructs the hero URL directly from page.url + filename,
matching what shortcode plugins already do. The page.media path is still
tried first so it works correctly if the config is ever fixed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr