Compare commits

...

136 Commits

Author SHA1 Message Date
m038 21c1d22859 fix: PhotoSwipe bg doesn't cover bottom — add !important and pin bg to viewport 2026-06-22 09:11:52 +02:00
m038 68b328dabc feat: enrich Slovenia 2024 Piran entry with coords and weather 2026-06-22 09:07:17 +02:00
m038 817bd17959 feat: split Slovenia 2024 into its own trip
Move Piran entry out of us-canada-mex-2024 into a new slovenia-2024 trip.
Rename entry folder to match the post title convention.
Fix us-canada-mex-2024 date_start to 2024-07-21 (first actual US entry).
2026-06-22 09:06:00 +02:00
m038 77dd99ee2b feat(stories): add mini-map via shared partial, add story card IDs 2026-06-22 01:39:29 +02:00
m038 857f33be54 refactor(dailies): use shared feed-map partial 2026-06-22 01:37:33 +02:00
m038 320a98893a feat: add shared feed-map partial (dailies + stories) 2026-06-22 01:33:26 +02:00
m038 e07fb3a72a feat(map): exit fullscreen on marker click, then scroll to entry
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
2026-06-22 01:12:52 +02:00
m038 1bb588d1d2 fix(trip): switch panel animation to max-height (grid-template-rows broken)
grid-template-rows: 0fr fails to fully collapse when the direct grid
child has overflow:hidden (creates a BFC that prevents 0-height).
max-height: 0 → 600px with overflow:hidden is simpler and reliable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 01:09:38 +02:00
m038 aa1cb7411c fix(map): collapse attribution on load; darken fullscreen button
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
2026-06-22 01:05:12 +02:00
m038 5fe8c015f1 fix(map): theme fullscreen button with accent colour
Replace plain white with --color-accent/--color-accent-on so the button
reads as a site control rather than a stray MapLibre element.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 01:00:50 +02:00
m038 6f9538053c feat(map): mobile fullscreen button on trip page map
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
2026-06-22 00:56:57 +02:00
m038 5e503cf3a5 fix(map): fullscreen btn inside map div, attribution moved to bottom-left
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
2026-06-22 00:20:23 +02:00
m038 ce860cfef9 fix(map): move fullscreen button outside feed-map div, top-right corner
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
2026-06-21 23:54:13 +02:00
m038 989755d33c feat(map): mobile fullscreen button for feed mini-map
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
2026-06-21 23:44:09 +02:00
m038 9ddf52c635 fix(trip): raise stat-value clamp floor to 2rem (32px)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:35:32 +02:00
m038 9b62f79301 fix(trip): raise stat-value clamp floor to --text-xl (1.75rem)
22px floor was too close to the preferred at 375px (6vw=22.5px), so
values were pinned near the minimum. 28px floor makes values pop more
on small screens while long values like ~12,366 still wrap gracefully.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:33:06 +02:00
m038 64aa9ec023 revert(trip): restore stat-label to --text-xs
Small label is intentional — the contrast with the larger value is the
visual hierarchy. Revert the sm bump from the previous commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:30:24 +02:00
m038 9bfd96af2c fix(trip): bump stat-label to --text-sm, widen stat-value fluid range
Label: xs (12px) → sm (14px) for clearer hierarchy below the value.
Value preferred: 5.5vw → 6vw so short values stay bold on mid phones.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:28:42 +02:00
m038 000af6934f fix(trip): raise stat-value clamp floor to --text-lg for visual hierarchy
14px floor was too close to the 12px label size. 1.375rem keeps the
value visually dominant over the label even at minimum size.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:23:39 +02:00
m038 c94e36a861 fix(trip): fluid stat-value font size with clamp()
Replaces fixed 3rem with clamp(--text-sm, 5.5vw, --text-3xl) so long
values like "4:32:15" scale down on mobile instead of overflowing.
Desktop (≥870px viewport) is unchanged at 3rem.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:21:58 +02:00
m038 2c831628b2 fix(trip): fix cycling stats mobile grid — span lone last card full width
7 cycling stat blocks in a 2-col mobile grid leaves a lone card in the
last row's left column with empty space on the right. Using
:last-child:nth-child(odd) + grid-column: 1/-1 spans that card across
both columns. Also minmax(0,1fr) on both grids for strictly equal widths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:18:09 +02:00
m038 02fc666661 feat(trip): pill radius on panel toggles, slide animation, mobile close button
- 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
2026-06-21 23:09:52 +02:00
m038 c3cb224402 style(trip): give Stats/Cycling panel toggles a square bordered style
Border + 4px radius instead of borderless text, matching the visual
weight of the filter pills without the full pill roundness.
Active state gets teal border + accent-light background like other active controls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:03:56 +02:00
m038 2b8ea1963b refactor(trip): declutter filter bar — move Stats/Cycling to panel toggles
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
2026-06-21 22:58:28 +02:00
m038 f94880e758 feat: add sort toggle to dailies and stories pages
dailies: reverse Twig output to ascending (matching trip default),
add feed-sort-bar above feed, add sort JS using [data-type] + appendChild.

stories: wrap heading in flex header row with sort button inline,
add sort JS targeting .story-card children of .stories-grid.

CSS: feed-sort-bar (right-aligned button above feed),
stories-listing__header (flex row, baseline-aligned), heading margin moved to header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 22:27:11 +02:00
m038 b6142cee44 feat(trip): add ascending/descending sort toggle button
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
2026-06-21 22:21:55 +02:00
m038 53bfe5955d fix(photoswipe): target currSlide.container not currSlide.el
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
2026-06-21 21:45:22 +02:00
m038 9f503c011d fix(photoswipe): keyboard arrow animation via CSS keyframes
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
2026-06-21 21:36:48 +02:00
m038 415d95ed47 feat(photoswipe): animate keyboard arrow navigation in lightbox
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
2026-06-21 21:29:06 +02:00
m038 e787544a2b feat(strip): smooth scroll animation on arrow button clicks
scroll-behavior: smooth on the strip element ensures programmatic
scrollBy calls animate consistently, cooperating with scroll-snap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 21:22:42 +02:00
m038 9f94164c61 fix(arrows): insert strip-controls after wrap, not inside it
Dots moved inside journal-photo-wrap (overflow:hidden) earlier, so
controls were being clipped. Now inserts after the wrap element.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 21:13:43 +02:00
m038 608ccfdecd fix(partial): restore data-slides on photo strip
Missing data-slides caused base.html.twig arrow script to read
slideCount as 1 and bail before creating prev/next controls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 21:10:48 +02:00
m038 933652fd57 refactor(templates): extract entry markup into shared partials
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
2026-06-21 21:07:12 +02:00
m038 fdaed1033a fix(ios): use 100dvh for PhotoSwipe to fix dynamic viewport
iOS Safari freezes 100vh at the initial viewport height (address bar
visible). 100dvh tracks the live viewport as browser chrome shows/hides.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:48:13 +02:00
m038 bc77baca2e fix(scroll): clear URL hash when back-to-top is clicked
Uses history.pushState to strip the stale #entry-slug without
triggering a page jump, then smooth-scrolls to the top.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:41:41 +02:00
m038 7c9a55224a fix(scroll): use hash navigation for marker clicks
Browser handles scroll natively via window.location.hash, respecting
scroll-margin-top. Updates URL for shareability and screen reader
compatibility. Added html { scroll-behavior: smooth } for smooth
hash navigation globally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:40:15 +02:00
m038 85ba3747b1 fix(scroll): use block:start so scroll-margin-top is respected
block:center ignores scroll-margin-top. block:start positions the
entry's top edge at the margin offset, clearing the sticky header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:36:46 +02:00
m038 71f8629d18 fix(scroll): add scroll-margin-top to all feed entry types
Offsets scroll targets by header height + 1rem so marker clicks land
below the sticky nav, not behind it. Applies to both .journal-post
and .entry-card (story cards).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:33:57 +02:00
m038 b1492918d5 feat(trip): add back-to-top button
Reuses .story-totop styles. Appears after scrolling past 80% of
viewport height, smooth-scrolls to top on click.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:29:07 +02:00
m038 95ea38d250 fix(feed): reduce excess spacing between entries
Removed redundant margin-bottom on .journal-post (feed gap already
separates items). Reduced padding-bottom and gap from 3rem to 2rem,
cutting the between-entry whitespace roughly in half.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:25:02 +02:00
m038 81be69f08d fix(photos): make PhotoSwipe background fully opaque
Prevents page dots from leaking through the semi-transparent overlay.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:21:33 +02:00
m038 71eaa3e788 feat(photos): adaptive aspect ratio per entry (portrait 4:5, landscape 4:3)
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
2026-06-21 20:18:05 +02:00
m038 5c75f1416f feat(photos): blurred ambient fill, dots overlay, PhotoSwipe on trip + dailies
- 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
2026-06-21 20:09:24 +02:00
m038 3379e50503 fix(dailies): fix PhotoSwipe CSS loading + restore expand button
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
2026-06-21 19:39:33 +02:00
m038 30c8937566 feat(dailies): replace custom lightbox with PhotoSwipe v5
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
2026-06-21 19:31:27 +02:00
m038 770a96b099 feat(dailies): 4:3 photo strip, lightbox, and dot sync
- 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
2026-06-21 19:06:54 +02:00
m038 604ba00c70 content: set transport_mode on all central-asia-2023 entries; add plane option to blueprint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 18:28:44 +02:00
m038 b6c9d0b2ac fix: centripetal Catmull-Rom spline to prevent overshooting near close markers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 18:20:26 +02:00
m038 51ab99b839 content: move Farewell Vodka entry between Khorog and Bukhara
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 18:10:08 +02:00
m038 2f733e5ffc chore: ignore ui-test post entries from gitignore
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 18:02:54 +02:00
m038 d7e3162f55 (Grav GitSync) Automatic Commit from m038 2026-06-21 15:51:01 +00:00
m038 8e127e7e3a (Grav GitSync) Automatic Commit from m038 2026-06-21 15:45:54 +00:00
m038 e853cb543a (Grav GitSync) Automatic Commit from m038 2026-06-21 15:40:39 +00:00
m038 e29953ab90 (Grav GitSync) Automatic Commit from m038 2026-06-21 15:38:11 +00:00
m038 366974475f (Grav GitSync) Automatic Commit from m038 2026-06-21 15:37:57 +00:00
m038 fa29888578 chore: ignore git-sync.yaml and security.yaml
Both files are server-specific: git-sync.yaml contains an encrypted
token, security.yaml holds Grav nonces/salts. Neither should roam
across environments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 17:25:22 +02:00
m038 31f3c6fb2f fix: resolve AX6/AX7 a11y violations
- gpx-manager: raise th color #666→#999 (6.9:1 contrast on dark bg)
- gpx-manager: raise .gpx-delete text #c0392b→#e07070 (6.2:1 contrast)
- gpx-manager: add visible label text 'Choose GPX file' to file input
- snap-gallery: add tabindex=0 to .pgallery__frame for keyboard scrollability

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM
2026-06-21 17:24:29 +02:00
m038 936662e35c chore: update admin password
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 16:17:08 +02:00
m038 a440583691 content: add home page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 15:10:51 +02:00
m038 6486d377b2 chore: commit config drift from CMS usage
accounts/mischa.yaml: pagesViewMode set to tree via Admin UI
config/site.yaml: active_trip updated from demo to us-canada-mex-2024

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 15:10:49 +02:00
m038 6c842ebe7f content: apply enrichment data to central-asia-2023 entries
Fills in lat, lng, weather_temp_c, and weather_desc for all 23 entries.
Completes the enrichment committed in 3f53bf5 which only renamed folders.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 15:10:45 +02:00
m038 89c9771a84 chore: untrack third-party plugins from user repo
All plugins except cache-on-save and story-blocks are managed by GPM
and server-install.sh — they should not be in version control.
Fixes .gitignore to plugins/* with explicit allows for custom plugins only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 15:10:33 +02:00
m038 89ae41d9ec Ignore italy-2026-demo in user repo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 15:01:52 +02:00
m038 3983615c99 Remove demo content from user repo
Demo trip italy-2026-demo was accidentally committed; it should only
exist in docs/demo/ and be loaded locally via make demo-load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 15:01:39 +02:00
m038 8412d1540c content: enrich us-canada-mex-2024 entries with location, weather, and rename folders to title slugs
Applied location_city, location_country, lat, lng, weather_temp_c, weather_desc to all
11 entries (pixelfed-2 through pixelfed-12). Renamed folders from pixelfed-N format to
date-title-slug format. pixelfed-1 (Piran/Slovenia) left untouched.
2026-06-21 14:24:01 +02:00
m038 3f53bf5b85 content: enrich central-asia-2023 entries with location, weather, and rename folders to title slugs
Applied location (city, country, lat, lng) and weather (temp_c, desc) enrichment to all 22
journal entries. Renamed entry folders from pixelfed-N numbering to date-title-slug format.
Bukhara entry (pixelfed-15) had its date corrected from 2023-09-23 to 2023-10-02.
2026-06-21 14:18:59 +02:00
m038 3c77d6cdad docs: remove stale docs/ content migrated to main repo docs/ on 2026-06-19 2026-06-21 14:16:33 +02:00
m038 e44105b330 content: enrich italy-2025 entries with location and weather 2026-06-21 13:17:47 +02:00
m038 f6a8657de2 chore: move Off to first position in connect markers select
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 12:12:45 +02:00
m038 89e2708b1e docs: switch blueprint help text line breaks from \n to <br>
Admin2 renders help as HTML so \n collapses; <br> is required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 12:07:13 +02:00
m038 f78ab147af docs: format blueprint help text with newlines
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 12:04:50 +02:00
m038 886ed21e5d docs: add descriptive help text to use_gpx and autoconnect blueprint fields
Explains each connect markers mode and the dependency between
Intelligent GPX and Show GPX being enabled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 11:53:02 +02:00
m038 eafc431e0e feat: expand connect markers to 4-mode select
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
2026-06-21 11:42:03 +02:00
m038 9809950347 refactor: simplify connector logic — remove GPX proximity suppression
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
2026-06-21 11:38:50 +02:00
m038 21b572677e feat: add per-trip use_gpx and autoconnect toggles
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
2026-06-21 11:20:25 +02:00
m038 7c2303c4e8 fix: sort trip page entries ascending (oldest first) 2026-06-21 11:03:49 +02:00
m038 a4b3e526fb content: update home page intro copy 2026-06-21 10:53:04 +02:00
m038 3018ae16ff feat: use page content field for home page description instead of subtitle header 2026-06-21 10:49:54 +02:00
m038 ff9ea3a0a7 fix: correct home blueprint field names to header.title/header.subtitle 2026-06-21 10:45:55 +02:00
m038 913e4bf19a feat: pull home page title/subtitle from page content instead of hardcoding 2026-06-21 10:43:50 +02:00
m038 6eaa00d612 fix: share GPX+journey rendering via MapUtils.renderGpxJourney
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
2026-06-21 10:37:56 +02:00
m038 04e4fa3dcd feat: add between-trips highlights mode with grid and map markers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 01:48:39 +02:00
m038 8edbfd2dd3 feat: add travelling branch and GPX to home map (active trip mode) 2026-06-21 01:43:24 +02:00
m038 b1cffca953 chore: mark demo entries as featured for homepage highlight testing 2026-06-21 01:36:05 +02:00
m038 cf364bc298 feat: add blueprints for active_trip/travelling config, tagline, featured fields 2026-06-21 01:36:00 +02:00
m038 5a6c00eaa4 demo: add Montalcino story showcasing full-bleed and image-caption shortcodes
Demonstrates all three image-caption width variants (column, full, bleed via
full-bleed shortcode) in a real story context with narrative prose.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM
2026-06-20 23:51:51 +02:00
m038 512f1ce9b2 feat(story-blocks): add full-bleed and image-caption shortcodes
Two new shortcodes for immersive storytelling:
- [full-bleed image="" caption="" credit=""] — viewport-wide image, max 80vh
- [image-caption image="" caption="" credit="" width="column|full|bleed"] — photo at configurable width with caption

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM
2026-06-20 23:25:16 +02:00
m038 ee107eebdf fix: add collection config to demo dailies.md; photo strip keyboard access
- demo dailies.md: add content.items collection config (required for page.collection())
- base.html.twig: add tabindex="0" to journal-photo-strip for WCAG scrollable-region-focusable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 22:12:46 +02:00
m038 63a826fc8e chore: switch active trip to italy-2026-demo, remove japan-korea-2026
- active_trip: italy-2026-demo in site.yaml (was japan-korea-2026)
- post-form.md parent updated to /trips/italy-2026-demo/dailies
- Remove japan-korea-2026 trip pages (no real trip exists)
- Remove stale old italy-2026-demo entries/stories/GPX from git tracking
  (these were leftover from before the demo-source approach)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 22:00:37 +02:00
m038 a9ce9a257c feat(demo): add journal entries days 5–8 with photos
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 21:30:17 +02:00
m038 5ee0b8510f feat(demo): add journal entries days 1–4 with photos 2026-06-20 21:27:12 +02:00
m038 19d34622ca feat(demo): add story 4 — Florence Without a Map 2026-06-20 21:24:49 +02:00
m038 dd764c8726 feat(demo): add story 3 — One Evening in Siena 2026-06-20 21:23:18 +02:00
m038 dacda6fca0 feat(demo): add story 2 — Val d'Orcia at Dawn 2026-06-20 21:21:52 +02:00
m038 8f87155c1d feat(demo): add story 1 — Sorano: Rock and Time 2026-06-20 21:19:57 +02:00
m038 42ed59a6b3 chore(demo): cleanup old demo data, rename GPX files, update trip.md
- Remove japan-korea-2026 demo folder entirely
- Remove italy-2025 and italy-2026-demo dailies and stories (replaced in later tasks)
- Rename 4 long GPX filenames to short slugs (day-1 through day-4)
- Update trip.md title to 'Tuscany 2026'
- Add dailies/dailies.md index page
- Recreate empty dailies and 04.stories directories
2026-06-20 21:14:57 +02:00
m038 c403ea9593 fix(a11y): fix active filter button contrast (color-ink on accent-light bg) 2026-06-20 20:49:08 +02:00
m038 a2cdbd7506 feat(a11y): add unique aria-label to GPX delete buttons 2026-06-20 20:36:46 +02:00
m038 f463eadbef feat(a11y): add keyboard prev/next to photo strip and region landmark 2026-06-20 20:32:12 +02:00
m038 ce5d520817 feat(a11y): add aria-pressed to filter buttons and aria-expanded to stats/cycling toggles 2026-06-20 20:27:45 +02:00
m038 b1e1a5cb9a feat(a11y): fix --color-ink-muted and --color-accent contrast ratios 2026-06-20 20:23:03 +02:00
m038 a7786f263f feat(a11y): add skip-to-main link and main landmark id 2026-06-20 20:19:27 +02:00
m038 ffcf156289 feat: add story markers to trip map (white diamond); extend flash highlight to story cards 2026-06-20 17:53:56 +00:00
m038 d923f3eb46 feat: generate AI titles for 36 Pixelfed-imported entries 2026-06-20 16:38:28 +00:00
m038 075a8fa9d4 fix: restore system media types in media.yaml; gpx was silently overriding all built-in types 2026-06-20 18:31:48 +02:00
m038 20212fee25 perf: skip hero media lookup for journal entries — only story cards use it 2026-06-20 18:27:45 +02:00
m038 229532ab8b fix(story): fall back to direct URL when page.media fails due to media.types config override
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
2026-06-20 16:31:26 +02:00
m038 138649c8e5 docs: clarify intentional sort omission in dailies feed 2026-06-20 15:47:37 +02:00
m038 728a43c4c3 fix: add missing 2024-05-28-pixelfed-1 Northern America entry 2026-06-20 13:41:49 +00:00
m038 850d2f5c50 feat: replace journal entry card with inline journal-post on home page 2026-06-20 13:34:22 +00:00
m038 6283c840ff feat: import 36 Pixelfed posts into central-asia-2023, us-canada-mex-2024, italy-2025
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 13:33:35 +00:00
m038 7a9cd0f269 feat: add central-asia-2023 and us-canada-mex-2024 trip page trees 2026-06-20 13:25:57 +00:00
m038 cf5e1ecb2d chore: move italy demo to italy-2026-demo; clean japan and italy-2025 demo content 2026-06-20 13:18:31 +00:00
m038 da7fbaf5b1 feat: replace journal entry card with inline journal-post in trip feed 2026-06-20 13:04:09 +00:00
m038 e7482e5bdd feat: replace journal entry card with inline journal-post in dailies feed 2026-06-20 12:50:14 +00:00
m038 f829da10ec feat: add journal-post CSS component and dot-sync JS; remove stale journal-card-only rules 2026-06-20 14:32:04 +02:00
m038 fb5ae6732c fix: remove redundant background declaration from .trip-card:hover 2026-06-20 12:53:31 +02:00
m038 a398bcb737 feat: add map-to-card flash highlight on marker click 2026-06-20 12:47:51 +02:00
m038 9365f46440 fix: apply flat entry-card structure to home.html.twig 2026-06-20 12:43:58 +02:00
m038 246fbfde76 fix: add missing data-type attributes to entry cards in dailies.html.twig 2026-06-20 12:40:58 +02:00
m038 2a151b710c refactor: collapse entry card article+a to flat <a>, unify hover targets across card types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM
2026-06-20 12:38:28 +02:00
m038 ca283d621a fix(story): restore datetime type, use date-only format; strip times from demo stories
Reverts text→datetime change. Uses format:'Y-m-d' (date-only) so the
datepicker omits the time component. Strips HH:MM from all Italy demo
story dates for consistency with the new date-only format.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 12:36:52 +02:00
m038 ca920a9fe8 feat: add fixed top and footer back pills to entry page 2026-06-20 12:31:09 +02:00
m038 26182ec363 feat: apply back-pill class to story footer back link
Apply the .back-pill class to the story footer back link and add
:not(.back-pill) guard to .story-footer a rule to prevent the accent
color override. This ensures the back pill maintains its design system
styling (border, background, text color) in story footers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM
2026-06-20 12:23:53 +02:00
m038 d0c821588e fix(story): replace datetime pickers with text fields for date inputs
Admin2's datetime widget ignores the blueprint format parameter and renders
dates in US MM/DD/YYYY locale format regardless. Switching both Start Date
and End Date to type:text eliminates the datepicker entirely — existing ISO
values (2025-09-06 20:00) display as typed, no locale interference.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 12:21:50 +02:00
m038 3edc18fe28 feat: add back-pill class, card hover lift, flash keyframe; remove duplicate story-escape 2026-06-20 12:20:14 +02:00
m038 5bc8d008df fix(story): end_date format Y-m-d H:i; fix guard comparison; remove test data
Blueprint: end_date format changed to Y-m-d H:i (same as start date) so
Admin2 uses the identical datepicker — avoids ambiguous d-m-Y input being
misread by PHP as m-d-Y.

Template guard: was comparing end_date string against page.date|date('Y-m-d')
which can never match. Now compares date-only parts of both fields:
page.header.end_date|date('Y-m-d') != page.date|date('Y-m-d')

Montalcino live page: removed test end_date '12-09-2026 00:00'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 12:10:21 +02:00
m038 5eca310bd8 fix(story): remove spurious end_date from Montalcino; guard start==end range
Montalcino demo story had end_date: 2025-09-06 matching its start date,
causing a '06 – 06 Sep' range display. Removed from both the live page
and the demo source.

Template: added guard so end_date equal to the start date never renders
as a range, even if it appears in frontmatter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 11:54:31 +02:00
m038 13d6576a2c fix(story): smart date range formatting + blueprint end_date format fix
Blueprint: end_date format changed from 'Y-m-d H:i' to 'Y-m-d' — the
demo frontmatter stores dates without a time component so Admin was
failing to parse it and showing the field empty.

Template: three-case smart range formatting:
- Same month & year  → 01 – 03 Sep 2025
- Same year only     → 01 Jan – 03 Sep 2025
- Different years    → 01 Jan 2025 – 03 Feb 2026

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 11:50:44 +02:00
m038 bc67a0ee88 fix(story): add end_date blueprint field; fix date range display
Blueprint: renamed 'Date' to 'Start Date', added optional 'End Date'
field (header.end_date) so it's editable in Admin.

Template: single date → 'd M Y'; range → 'd M Y – d M Y' (full year
on both sides — the old format silently dropped the start year).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 11:45:38 +02:00
m038 46c8a76633 fix(story): scrolly step cards — canvas surface + teal left accent bar
Card background was rgba(26,24,20,0.92) — identical to the page background
(--color-paper #1A1814), making cards invisible. Changed to --color-canvas
(#22201B) for a lifted surface. Added border-left: 3px solid --color-accent
as an editorial marker; other three sides keep the subtle --color-border.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 11:36:24 +02:00
m038 cc341cc944 fix(story): nav title cross-fades scroll-driven as hero content exits viewport
Replaced IntersectionObserver (discrete threshold) with a scroll RAF loop
using getBoundingClientRect. Opacity is computed from the fraction of
.story-hero__content still visible above the viewport top — so the nav title
fades in gradually as the hero title slides off the top edge, reaching full
opacity only when the element is completely gone.

Removed CSS transition (no longer needed; per-frame JS update is smooth).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 11:28:12 +02:00
m038 f4ee63282b fix(story): nav title hidden on load, DM Serif Display typography
Was visible on load because story-hero__content starts below the
viewport fold (bottom:18% of 140vh hero) — IO fired immediately
as 'not intersecting'. Fixed with hasBeenVisible flag: nav title
only appears after the element has entered then exited the viewport.

Typography changed from DM Sans/500 to DM Serif Display/400 at
--text-lg (1.375rem) with -0.01em tracking — same face as the
hero title, scaled down for the 60px nav bar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 11:24:36 +02:00
m038 326f28e4ac feat(story): sticky nav title + floating back-to-top pill
Nav title: absolutely centered in the site-header, fades in via
IntersectionObserver when .story-hero__content scrolls above the fold.
Hidden on mobile (< 640px) where there is no room.

Back-to-top: fixed bottom-right pill, appears after 80% of the hero
viewport is scrolled past, smooth-scrolls to top on click.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 11:15:06 +02:00
m038 6e5caf33ad fix(story): soft pill for scrolly caption — rounded edges with faded halo
border-radius: 9999px for a true pill shape.
box-shadow matching the background alpha creates the feathered edge fade
all the way around without any extra elements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 11:09:37 +02:00
m038 49c4ab0341 fix(story): smooth hero overlay fade-out and add scrolly caption background
Overlay previously hard-hid with display:none when scrollY = 100vh, while
the title was still visible (it exits at ~115vh). Now fades in 0→0.65 over
the first 70vh then fades back out to 0 by 140vh (full hero height).

Scrolly caption changed from full-width to centered pill with
rgba(0,0,0,0.45) background — readable against any image regardless of tone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-20 10:32:22 +02:00
530 changed files with 808922 additions and 5508 deletions
+7 -2
View File
@@ -1,4 +1,9 @@
/plugins/
!/plugins/
/plugins/*
!/plugins/.gitkeep
!/plugins/cache-on-save/
!/plugins/story-blocks/
/data/
/pages/01.trips/italy-2026-demo/
/pages/02.post/*ui-test*/
/config/plugins/git-sync.yaml
/config/security.yaml
+1
View File
@@ -0,0 +1 @@
/* @copyright Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved. */
+13 -2
View File
@@ -1,10 +1,21 @@
login: mischa
state: enabled
title: Mischa
title: ''
email: mischa@gorinskat.nl
fullname: Mischa
hashed_password: $2y$10$koiWKjhhipph4uTbm7fWjOj79uwxfE/mYSXGKANrAvUrSqezY3xL2
hashed_password: $2y$10$xyV7bAUWEo75K6LbatUuYe/6x2Tj9nT6YnIjaDvESAhU2hJ7tjG2.
language: en
modified: 1782051328
admin_next:
preferences:
pluginsViewMode: cards
colorMode: dark
accentHue: 271
accentSaturation: 91
fontFamily: inter
pagesViewMode: tree
content_editor: ''
groups: { }
access:
admin:
login: true
+20
View File
@@ -0,0 +1,20 @@
form:
validation: loose
fields:
active_trip:
type: pages
label: Active Trip
start_route: '/trips'
show_root: false
show_slug: true
travelling:
type: toggle
label: Currently Travelling
highlight: 1
default: false
options:
1: 'Yes'
0: 'No'
validate:
type: bool
-118
View File
@@ -1,118 +0,0 @@
name: Daily Entry
extends@: default
form:
fields:
tabs:
type: tabs
active: 1
fields:
content:
type: tab
title: Entry
fields:
header.title:
type: text
label: Title
validate:
required: true
header.date:
type: datetime
label: Date
format: 'Y-m-d H:i'
validate:
required: true
content:
type: markdown
label: Content
validate:
required: true
header.hero_image:
type: text
label: 'Hero Image Filename'
help: 'Filename of the main photo for this entry (e.g. photo.jpg). Upload photos via the Media tab.'
location:
type: tab
title: Location
fields:
header.location_city:
type: text
label: City
placeholder: 'e.g. Kyoto'
header.location_country:
type: text
label: Country
placeholder: 'e.g. Japan'
header.lat:
type: number
label: Latitude
help: 'GPS latitude (for map)'
placeholder: '48.8566'
step: any
header.lng:
type: number
label: Longitude
help: 'GPS longitude (for map)'
placeholder: '2.3522'
step: any
weather:
type: tab
title: Weather
fields:
header.weather_temp_c:
type: number
label: 'Temperature (°C)'
help: 'Auto-filled from post form. Edit if needed.'
step: 1
header.weather_desc:
type: select
label: 'Weather Condition'
options:
Sunny: '☀️ Sunny'
'Partly cloudy': '⛅ Partly cloudy'
Cloudy: '☁️ Cloudy'
Foggy: '🌫️ Foggy'
Drizzle: '🌦️ Drizzle'
Rain: '🌧️ Rain'
Snow: '❄️ Snow'
Thunderstorm: '⛈️ Thunderstorm'
publishing:
type: tab
title: Publishing
fields:
header.published:
type: toggle
label: Published
highlight: 1
default: 1
options:
1: 'Yes'
0: 'No'
validate:
type: bool
header.publish_date:
type: datetime
label: 'Publish Date'
help: 'Schedule future publication (leave blank to publish immediately)'
format: 'Y-m-d H:i'
header.unpublish_date:
type: datetime
label: 'Unpublish Date'
help: 'Automatically unpublish at this date/time'
format: 'Y-m-d H:i'
+23
View File
@@ -0,0 +1,23 @@
ui:
defaults:
colorMode: ''
accentHue: 271
accentSaturation: 91
fontFamily: inter
fontSize: normal
editorMode: normal
editorStickyToolbar: true
editorFixedHeight: 0
adminLanguage: en-US
pagesPerPage: 20
pagesViewMode: tree
usersViewMode: cards
groupsViewMode: cards
pluginsViewMode: cards
themesViewMode: cards
settings:
autoSaveEnabled: false
autoSaveToolbarUndo: true
autoSaveBatchWindowMs: 0
collabEnabled: false
menubarLinks: { }
+221
View File
@@ -1,4 +1,225 @@
types:
defaults:
type: file
thumb: media/thumb.png
mime: application/octet-stream
image:
filters:
default:
- enableProgressive
jpg:
type: image
thumb: media/thumb-jpg.png
mime: image/jpeg
jpe:
type: image
thumb: media/thumb-jpg.png
mime: image/jpeg
jpeg:
type: image
thumb: media/thumb-jpg.png
mime: image/jpeg
png:
type: image
thumb: media/thumb-png.png
mime: image/png
webp:
type: image
thumb: media/thumb-webp.png
mime: image/webp
avif:
type: image
thumb: media/thumb.png
mime: image/avif
gif:
type: animated
thumb: media/thumb-gif.png
mime: image/gif
svg:
type: vector
thumb: media/thumb-svg.png
mime: image/svg+xml
mp4:
type: video
thumb: media/thumb-mp4.png
mime: video/mp4
mov:
type: video
thumb: media/thumb-mov.png
mime: video/quicktime
m4v:
type: video
thumb: media/thumb-m4v.png
mime: video/x-m4v
swf:
type: video
thumb: media/thumb-swf.png
mime: video/x-flv
flv:
type: video
thumb: media/thumb-flv.png
mime: video/x-flv
webm:
type: video
thumb: media/thumb-webm.png
mime: video/webm
ogv:
type: video
thumb: media/thumb-ogg.png
mime: video/ogg
mp3:
type: audio
thumb: media/thumb-mp3.png
mime: audio/mp3
ogg:
type: audio
thumb: media/thumb-ogg.png
mime: audio/ogg
wma:
type: audio
thumb: media/thumb-wma.png
mime: audio/wma
m4a:
type: audio
thumb: media/thumb-m4a.png
mime: audio/m4a
wav:
type: audio
thumb: media/thumb-wav.png
mime: audio/wav
aiff:
type: audio
thumb: media/thumb-aif.png
mime: audio/aiff
aif:
type: audio
thumb: media/thumb-aif.png
mime: audio/aiff
txt:
type: file
thumb: media/thumb-txt.png
mime: text/plain
xml:
type: file
thumb: media/thumb-xml.png
mime: application/xml
doc:
type: file
thumb: media/thumb-doc.png
mime: application/msword
docx:
type: file
thumb: media/thumb-docx.png
mime: application/vnd.openxmlformats-officedocument.wordprocessingml.document
xls:
type: file
thumb: media/thumb-xls.png
mime: application/vnd.ms-excel
xlsx:
type: file
thumb: media/thumb-xlsx.png
mime: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
ppt:
type: file
thumb: media/thumb-ppt.png
mime: application/vnd.ms-powerpoint
pptx:
type: file
thumb: media/thumb-pptx.png
mime: application/vnd.openxmlformats-officedocument.presentationml.presentation
pps:
type: file
thumb: media/thumb-pps.png
mime: application/vnd.ms-powerpoint
rtf:
type: file
thumb: media/thumb-rtf.png
mime: application/rtf
bmp:
type: file
thumb: media/thumb-bmp.png
mime: image/bmp
tiff:
type: file
thumb: media/thumb-tiff.png
mime: image/tiff
mpeg:
type: file
thumb: media/thumb-mpg.png
mime: video/mpeg
mpg:
type: file
thumb: media/thumb-mpg.png
mime: video/mpeg
mpe:
type: file
thumb: media/thumb-mpe.png
mime: video/mpeg
avi:
type: file
thumb: media/thumb-avi.png
mime: video/msvideo
wmv:
type: file
thumb: media/thumb-wmv.png
mime: video/x-ms-wmv
html:
type: file
thumb: media/thumb-html.png
mime: text/html
htm:
type: file
thumb: media/thumb-html.png
mime: text/html
ics:
type: iCal
thumb: media/thumb-ics.png
mime: text/calendar
pdf:
type: file
thumb: media/thumb-pdf.png
mime: application/pdf
ai:
type: file
thumb: media/thumb-ai.png
mime: image/ai
psd:
type: file
thumb: media/thumb-psd.png
mime: image/psd
zip:
type: file
thumb: media/thumb-zip.png
mime: application/zip
7z:
type: file
thumb: media/thumb-7z.png
mime: application/x-7z-compressed
gz:
type: file
thumb: media/thumb-gz.png
mime: application/x-gzip
tar:
type: file
thumb: media/thumb-tar.png
mime: application/x-tar
css:
type: file
thumb: media/thumb-css.png
mime: text/css
js:
type: file
thumb: media/thumb-js.png
mime: text/javascript
json:
type: file
thumb: media/thumb-json.png
mime: application/json
vcf:
type: file
thumb: media/thumb-vcf.png
mime: text/x-vcard
gpx:
type: file
mime: application/gpx+xml
+4
View File
@@ -0,0 +1,4 @@
popularity:
salt: 671ae9ab4f792c7dc860dbe8be288f2fdebdb3b4615f3c0f43211ecb95aaeeb3
auth:
jwt_secret: 61a84160bdd430768c82c4fe153e151a7a6f68f993c3779a5f36d32ee9293653
+7
View File
@@ -0,0 +1,7 @@
<?php
// Auto-generated private secret. Do NOT commit to version control.
// Used for CSRF nonce signing and admin rate-limit hashing. Regenerate by
// deleting this file; the next request will write a new value.
return 'lsUHWFkCwvGZrL';
-1
View File
@@ -1 +0,0 @@
{ }
+3 -3
View File
@@ -1,9 +1,9 @@
title: 'Into the East'
description: 'A travel blog by Mischa'
author:
name: Mischa
email: mischa@gorinskat.nl
taxonomies: [category, tag]
metadata:
description: 'Into the East — travel journal'
active_trip: japan-korea-2026
description: 'A travel blog by Mischa'
active_trip: /trips/us-canada-mex-2024
travelling: false
+1 -1
View File
@@ -5,7 +5,7 @@ wrapped_site: false
reverse_proxy_setup: false
force_ssl: false
force_lowercase_urls: true
custom_base_url: 'http://100.96.115.96:8081'
custom_base_url: ''
username_regex: '^[a-z0-9_-]{3,16}$'
pwd_regex: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}'
intl_enabled: true
-136
View File
@@ -1,136 +0,0 @@
# Bugs & Fixes
Backlog of confirmed bugs with root cause analysis and implementation spec for the fix.
---
## BUG-001 — New entry not visible after form submission
**Status:** fixed 2026-06-18
**Reported:** 2026-06-18
### Symptom
After submitting a new post via `/post`, the entry page file is created correctly on disk but does not appear in the `/tracker` feed or in the Grav Admin panel until the cache is manually flushed.
### Root cause
Grav's page-tree cache (`cache/doctrine/`) is not invalidated when `add-page-by-form` writes a new page to disk. The tracker template uses `page.children`, which Grav serves from cache — so the new child page is invisible until the cache is cleared.
### Workaround (manual)
Run in terminal after each submission:
```bash
docker exec intotheeast_grav bash -c "cd /app/www/public && php bin/grav clearcache"
```
### Fix spec
Wire cache-clear into the form process so it happens automatically on every successful submission.
**Approach — custom Grav plugin event hook:**
1. Create a small plugin `user/plugins/cache-on-save/` with one event listener:
- Listen on `onFormProcessed`
- When the form name is `new-entry`, call `$this->grav['cache']->deleteAll()` (note: `clear()` does not exist on `Grav\Common\Cache` in Grav 1.7)
2. Enable the plugin in `user/config/plugins/cache-on-save.yaml`
This is the cleanest approach: it fires exactly once per successful submission, requires no changes to `post-form.md`, and works for any future forms too.
**Alternative — disable page cache entirely:**
Set `cache: { enabled: false }` in `system.yaml`. Simpler but degrades frontend performance; not recommended for production.
### Files to create/modify
| File | Change |
|------|--------|
| `user/plugins/cache-on-save/cache-on-save.php` | New plugin, ~30 lines |
| `user/plugins/cache-on-save/cache-on-save.yaml` | Plugin manifest, enabled: true |
| `user/config/plugins/cache-on-save.yaml` | Runtime config, enabled: true |
### Acceptance criteria
1. Submit a new post via `/post`
2. Navigate to `/tracker` — the new entry is visible immediately, no manual cache flush needed
3. Grav Admin also shows the new page immediately
---
## BUG-002 — Stale Twig cache after theme file changes
**Status:** fixed 2026-06-18
**Reported:** 2026-06-18
### Symptom
After theme template files are added or modified (e.g., creating `partials/base.html.twig`), Grav's Twig compiled-template cache still holds the old compiled version. Pages that extend the changed file throw 500 errors like "Template partials/base.html.twig is not defined" even though the file exists on disk.
### Root cause
Grav caches compiled Twig templates in `cache/twig/`. When a new file is added, existing templates that reference it don't know to recompile — their cache entries are still valid from their own mtime perspective.
### Workaround (manual)
Run after any theme file is added or changed:
```bash
docker exec intotheeast_grav bash -c "cd /app/www/public && php bin/grav clearcache"
```
### Fix spec
Disable Twig template caching in development via `user/config/system.yaml`:
```yaml
twig:
cache: false
```
Acceptable for a single-user dev setup — eliminates both BUG-001's side-effect and this bug entirely. Performance cost is negligible at one-user scale. On production, leave Twig cache enabled (it's fine there because template files don't change at runtime).
**Files to change:**
| File | Change |
|------|--------|
| `user/config/system.yaml` | Add `twig: { cache: false }` under development section |
### Acceptance criteria
1. Add a new theme template file
2. Reload any page — no 500 error, template works immediately without manual cache flush
---
## BUG-003 — One post per day limit; silent failure on duplicate date
**Status:** fixed 2026-06-18
**Reported:** 2026-06-18
### Symptom
Submitting a second post with the same date as an existing entry shows "Entry posted successfully!" but creates no file. The user's post is silently discarded.
### Root cause
The `add-page-by-form` plugin built the page slug from date only (`Y-m-d`), producing folder names like `2026-06-18.entry`. With `overwrite_mode: false`, if that folder already exists the plugin skips page creation but does not abort — the `message` process step runs regardless, showing a false success.
### Fix
Change the slug template in `user/pages/02.post/post-form.md` to include time and title:
```twig
{{ form.value.date|date('Y-m-d-Hi') }}-{{ form.value.title|lower|regex_replace('/[^a-z0-9]+/', '-')|trim('-') }}
```
Example: title "Arrived in Tokyo" at 14:30 on 2026-06-18 → `2026-06-18-1430-arrived-in-tokyo`
The slug is locked at creation time. Renaming the title afterwards does not change the URL.
### Acceptance criteria
1. Submit two posts on the same day with different times or titles — both appear in `/tracker` as separate entries
2. Renaming a post's title in the frontmatter does not break its URL
---
Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

@@ -1,28 +0,0 @@
---
title: The Val d'Orcia at Dawn
date: '2025-09-05 10:00'
location_name: Val d'Orcia
location_country: Italy
lat: 43.078
lng: 11.676
hero_image: hero.jpg
hero_alt: Cypress-lined dirt road at first light, Tuscany
published: true
---
We left camp before the heat arrived. At six in the morning the Val d'Orcia belongs entirely to the light — long shadows, pale gold, not a car on the white roads. The kind of silence that has texture.
[snap-gallery images="hero.jpg,photo.jpg" captions="First light on the valley floor,The hills fold endlessly east" alts="Wide valley at dawn with golden light,Rolling green hills under morning sky" /]
We stopped twice before nine. Once for a puncture, once because the view demanded it.
[chapter-break image="hero.jpg" title="The Hour Before Heat" alt="Hazy hillside shimmering in early morning warmth" /]
By ten the temperature had shifted. The colours changed too — softer, more diffuse, the sky turning white at the edges. We dropped into the lower valley and the road surface changed from gravel to packed earth, then back again.
[snap-gallery images="photo.jpg,hero.jpg" captions="The texture of Tuscan gravel — coarser than it looks,The road ahead disappears into the heat" alts="Close-up of pale gravel surface,Road vanishing into bright haze" /]
[pull-quote]
The best hours of a cycling day are the ones nobody sees. Four in the morning to ten. Then it belongs to the sun.
[/pull-quote]
We made Pienza by noon. It was already thirty degrees and the ice cream queue was six deep.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

@@ -1,55 +0,0 @@
---
title: The Long Climb to Montalcino
date: '2025-09-06 20:00'
end_date: 2025-09-06
location_name: Montalcino
location_country: Italy
lat: 43.058
lng: 11.489
hero_image: hero.jpg
hero_alt: Hairpin road climbing through olive groves towards a hilltop town
published: true
---
The profile showed fourteen kilometres at an average of six percent. In practice it was steeper at the bottom and gentler at the top, which is the worst possible arrangement. We started climbing at two in the afternoon, which was also the worst possible decision.
[scrolly-section image="hero.jpg" alt="Empty road rising steeply through olive groves" caption="SP55 — 14km, 840m elevation gain"]
The first kilometre is the most honest. You find out immediately whether your legs have anything to say.
---
By the halfway point the olive groves had given way to scrub oak and the road had narrowed to a single lane. No cars had passed in forty minutes. The silence was absolute except for breathing.
---
Then, at the last bend before the top, the town appeared. Just the outline of it — a tower, a wall, rooftops. It was enough.
[/scrolly-section]
[chapter-break image="photo.jpg" title="Montalcino" number="II" alt="Medieval town gate with stone archway" /]
[pull-quote image="photo.jpg" alt="Rows of Brunello vines descending from hilltop town"]
From the top you could see the whole valley we had spent two days riding through. It looked completely flat from up here.
[/pull-quote]
We found a bar in the main piazza. The owner brought two glasses of water without being asked. Then two more. Then a small plate of bread and oil that nobody ordered. We sat there for an hour.
[scrolly-section image="photo.jpg" alt="Shaded medieval piazza with stone buildings" caption="Piazza del Popolo, Montalcino"]
The piazza at five in the afternoon is a different place from the piazza at noon. People have returned from wherever they go during the heat.
---
A wine shop with barrels in the window and a handwritten list on a chalkboard. We looked at it for a long time and bought nothing. The prices were very reasonable and this felt suspicious.
---
A cat on a warm stone wall, watching traffic that did not exist. It had clearly been watching this traffic for years.
---
The fortress walls turn amber just before sunset. You could photograph this from a hundred different angles and it would look the same in all of them: very good.
---
The descent back to the valley takes twenty minutes. The climb took two and a half hours. This ratio never stops feeling wrong.
[/scrolly-section]
We found the agriturismo by following a handwritten sign nailed to a cypress tree. It was exactly what it promised to be.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

@@ -1,32 +0,0 @@
---
title: One Evening in Siena
date: '2025-09-05 22:00'
location_name: Siena
location_country: Italy
lat: 43.318
lng: 11.330
hero_image: hero.jpg
hero_alt: The Piazza del Campo at dusk, terracotta rooftops fading to blue
published: true
---
[pull-quote]
Siena is not a city that tries to impress you. It has been here for a thousand years and intends to be here for a thousand more. You fit around it.
[/pull-quote]
We rolled in at half past six, legs finished, panniers heavier than they started. The Campo appeared without warning at the end of a narrow street and we both stopped pedalling at exactly the same moment.
[scrolly-section image="hero.jpg" alt="Piazza del Campo seen from the upper rim, sloping shell-shaped square"]
The square fills from the edges inward as evening comes. First the locals — people who have been here before and know which bench faces west. Then the tourists, then the pigeons, then the shadows.
---
A busker with a cello at the top of the slope. A couple arguing quietly in a language I couldn't place. Three children running in a circle for reasons nobody questioned. The ordinary business of a city at the end of a summer day.
[/scrolly-section]
[snap-gallery images="hero.jpg,photo.jpg" captions="The Campo at the moment the light goes warm,A doorway on Via di Città — every doorway in Siena looks like this" alts="Wide shot of Campo at golden hour,Ornate stone doorway with iron lantern" /]
We found a place to eat down three flights of stairs in a basement that appeared to have no ventilation and no menu. It was perfect. The relief of sitting down after eight hours on a bike is a specific physical sensation that is difficult to describe to anyone who has not experienced it.
[pull-quote image="photo.jpg" alt="Sunset view over Siena rooftops from high vantage point"]
Cycling makes you earn every place you arrive at. Siena earned.
[/pull-quote]
@@ -1,13 +0,0 @@
---
title: "Rolling through Val d'Orcia"
template: entry
date: '2025-09-05 08:00'
lat: 43.078
lng: 11.676
location_city: Pienza
location_country: Italy
weather_temp_c: 24
weather_desc: Sunny
published: true
---
Cypress trees lining dirt roads, heat already rising. The Val d'Orcia is everything they say it is.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

@@ -1,13 +0,0 @@
---
title: "Siena at dusk"
template: entry
date: '2025-09-05 19:00'
lat: 43.318
lng: 11.335
location_city: Siena
location_country: Italy
weather_temp_c: 21
weather_desc: Clear
published: true
---
Rolled in just before sunset. The Piazza del Campo was still warm from the day's heat.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

@@ -1,13 +0,0 @@
---
title: "Towers of San Gimignano"
template: entry
date: '2025-09-06 12:00'
lat: 43.546
lng: 11.321
location_city: 'San Gimignano'
location_country: Italy
weather_temp_c: 26
weather_desc: Hot and sunny
published: true
---
Ate lunch in the shadow of the medieval towers. Legs tired, gelato mandatory.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

@@ -1,13 +0,0 @@
---
title: "Into Florence"
template: entry
date: '2025-09-06 18:00'
lat: 43.767
lng: 11.253
location_city: Florence
location_country: Italy
weather_temp_c: 28
weather_desc: Warm
published: true
---
City traffic after days of gravel roads. Dodged trams and found the hotel.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

@@ -1,13 +0,0 @@
---
title: "Tyrrhenian coast"
template: entry
date: '2025-09-08 09:00'
lat: 43.553
lng: 10.313
location_city: Livorno
location_country: Italy
weather_temp_c: 23
weather_desc: Sea breeze
published: true
---
The sea appeared suddenly between two hills. Eight days of riding ends here.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

@@ -0,0 +1,43 @@
---
title: 'Sorano: Rock and Time'
date: '2026-09-03'
location_name: Sorano
location_country: Italy
lat: 42.683
lng: 11.715
hero_image: hero.jpg
hero_alt: Medieval town of Sorano clinging to pale tufa cliffs at dusk
published: true
---
The road from Orbetello climbs inland through scrubland and heat. For most of the afternoon there is nothing on the horizon except sky and the occasional electricity pylon. Then, at the top of a ridge, Sorano appears — and the word "appears" does not quite cover it. The town has been carved from a cliff of tufa, a pale volcanic rock so soft you can score it with a fingernail. The buildings are the cliff and the cliff is the buildings.
[scrolly-section image="hero.jpg" alt="Medieval town of Sorano seen from the approach road, perched on pale tufa cliffs" caption="Sorano — tufa cliff town, Grosseto province"]
The approach by bike gives you an unusually long time to study it. The descent into the valley and the climb back up take perhaps forty minutes, and the town is visible for most of that time, doing nothing, requiring nothing.
---
Close up the rock is extraordinary. Hundreds of tomb niches cut into the cliff face — Etruscan graves, most of them open to the sky now, their contents long removed. The people who built this town chose to live surrounded by the evidence of their own mortality. This seems either very brave or very sensible.
---
The gate into the old town is fifteenth century and narrow enough that loaded bikes don't fit without turning sideways. Inside, the air is noticeably cooler and the alleys are steep, paved with the same pale tufa, worn smooth by centuries of feet.
[/scrolly-section]
We found a wall to lean the bikes against and sat looking south over the valley we had come from. The light was going amber. Below us, the road we had ridden was already in shadow.
[chapter-break image="photo-1.jpg" title="After Dark" number="II" alt="Narrow medieval alley in Sorano at dusk, pale stone walls glowing warm" /]
[pull-quote image="photo-1.jpg" alt="Stone alley in Sorano lit by a single lantern at night"]
A town built on rock, carved from rock, returning slowly to rock. Two thousand years of human effort and the cliff remains indifferent.
[/pull-quote]
[scrolly-section image="photo-2.jpg" alt="View south from the tufa cliff walls of Sorano at dusk" caption="Val di Fiora, from the old walls"]
One restaurant was open. The menu was four items. We had the pasta with wild boar and the pasta with truffles and a carafe of local wine that cost six euros and was excellent.
---
The owner sat at the next table watching a football match on his phone without headphones. Nobody minded. The town outside was completely quiet.
[/scrolly-section]
We were in bed before nine. Sorano at night is absolutely silent. It has been this quiet, in approximately this configuration, for a very long time.
Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

@@ -0,0 +1,30 @@
---
title: "Val d'Orcia at Dawn"
date: '2026-09-05'
location_name: Val d'Orcia
location_country: Italy
lat: 43.078
lng: 11.676
hero_image: hero.jpg
hero_alt: Wide Tuscan valley at dawn, long cypress shadows across pale gravel road
published: true
featured: true
---
We left before the heat arrived. The alarm was five-thirty and the sky outside the tent was still more grey than blue. The valley was invisible in the dark except as an absence — a vast silence below us where the shapes of hills ought to be. By six the light had changed. The Val d'Orcia is one of those landscapes that photographers wait years to shoot at this hour, and you can see why: the light arrives at an angle that makes everything look like something from a different century.
[snap-gallery images="hero.jpg,photo-1.jpg,photo-2.jpg" captions="Six in the morning: the valley belongs entirely to the light,The Cypress Road — every photograph of Tuscany was taken here or somewhere like it,A farmhouse that has been sitting on this hill for four hundred years" alts="Wide misty Tuscan valley at dawn with long shadows,Straight road lined by tall cypress trees in morning light,Stone farmhouse on a hilltop with rolling landscape behind" /]
The roads down here are white gravel — strade bianche — and the tyres make a particular sound on them that you don't get anywhere else. We rode for two hours without seeing a car. The only other people were two elderly men walking a dog in the opposite direction. They waved.
[chapter-break image="photo-1.jpg" title="The Hour Before Heat" alt="Cypress road vanishing into a hazy summer morning" /]
By nine the temperature had already shifted. The quality of the light changed — softer, more diffuse, the sky turning white at the edges. The windows of the farmhouses began to open. Dogs that had been invisible in the dark became visible on walls and in doorways, watching us with professional detachment.
[snap-gallery images="photo-2.jpg,hero.jpg" captions="The road changes from asphalt to gravel to packed earth and back again without warning,The valley floor at nine: the shadows have shortened, the colours have flattened" alts="Farmhouse detail with terracotta roof and single cypress tree,Tuscan valley road in mid-morning haze" /]
[pull-quote]
The best hours of a cycling day are the ones nobody else sees. Before the heat arrives, before the cafes open, before the traffic comes. Everything belongs to you then.
[/pull-quote]
We reached Pienza at eleven-thirty. The ice-cream queue was eight deep and entirely justified.
Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

@@ -0,0 +1,33 @@
---
title: 'One Evening in Siena'
date: '2026-09-05'
location_name: Siena
location_country: Italy
lat: 43.318
lng: 11.330
hero_image: hero.jpg
hero_alt: Piazza del Campo at dusk, terracotta paving fading from gold to shadow
published: true
---
[pull-quote image="hero.jpg" alt="Piazza del Campo seen from the upper rim at golden hour"]
Siena is not a city that tries to impress you. It has been here for a thousand years and intends to be here for a thousand more. You fit around it, not the other way.
[/pull-quote]
We rolled in at half past six, legs finished, panniers heavier than they started. The Campo appeared without warning at the end of a narrow street and we both stopped pedalling at exactly the same moment. That particular square does something to people. It is partly the shape — a shallow bowl, a scallop shell, the way it holds you — and partly the light at that hour, which turns the terracotta pavement the colour of old copper.
[chapter-break image="photo-1.jpg" title="The Campo" number="I" alt="Detail of Siena's herringbone brick pavement catching the last light" /]
[scrolly-section image="hero.jpg" alt="Piazza del Campo filling with people as evening comes" caption="Campo, 19:00 — the square fills from the edges inward"]
The locals arrive first. They know which spot faces west and which benches stay in the shade longest. Then the tourists, then the pigeons, then the long shadows.
---
A busker with an accordion near the Fonte Gaia. A group of students lying on the slope reading. Three children running in a circle for reasons nobody questioned.
---
We sat on the pavement with our backs against the warm brickwork of the Palazzo Pubblico and did not move for forty minutes. The relief of sitting still after eight hours on a bike is a specific physical sensation. It travels upward from your legs and settles somewhere just behind the sternum.
[/scrolly-section]
We found a place for dinner three streets away, down a flight of steps with no sign outside. The pasta was handmade, the wine was local, the bill was reasonable. We were in bed by ten. Tomorrow: Florence.
Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

@@ -0,0 +1,35 @@
---
title: 'Florence Without a Map'
date: '2026-09-07'
location_name: Florence
location_country: Italy
lat: 43.769
lng: 11.255
hero_image: hero.jpg
hero_alt: Arno river at midday with Ponte Vecchio, ochre buildings reflected in still water
published: true
---
No route today. No GPS, no distance target, no reason to be anywhere by any particular time. After six days of forward motion this felt almost wrong — the instinct to check the elevation profile arriving at nothing. We put the bikes in the hotel basement and walked out into Florence on foot.
[chapter-break image="hero.jpg" title="Day Seven" number="VII" alt="Arno river and Ponte Vecchio from Ponte Santa Trinita at midday" /]
[snap-gallery images="hero.jpg,photo-1.jpg" captions="The Arno at noon — greener than expected, the bridges older than you remember,Via dei Servi: washing lines, shutters, a cat on a warm stone ledge that had been warm since morning" alts="Arno river with Ponte Vecchio reflected in still water at midday,Narrow Florence street with laundry strung between buildings" /]
[pull-quote]
Cycling makes you earn every city you arrive at. Florence, we got for free. It felt like a gift and a debt simultaneously.
[/pull-quote]
[scrolly-section image="photo-1.jpg" alt="Narrow Oltrarno street in afternoon light" caption="Oltrarno, 14:00"]
The Uffizi had a queue that stretched around two corners and disappeared into a side street. We looked at it for a moment and went to find coffee instead. This felt correct.
---
A covered market in the Oltrarno that nobody had told us about. A man selling leather goods from a table he clearly reassembled each morning from identical components. A small dog sleeping under a fruit stall in a precisely calculated patch of shade.
---
We crossed the Ponte Vecchio at two in the afternoon, which is exactly the wrong time to cross the Ponte Vecchio, and it was still worth it. The light off the Arno at that hour is genuinely extraordinary and all the photographs in the world do not prepare you for it.
[/scrolly-section]
Dinner near the apartment, early. Feet sore in a different way from legs sore — a smaller, more concentrated complaint. Tomorrow: the last day. The coast road home.
Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

@@ -0,0 +1,16 @@
---
title: 'Setting Off from Campiglia'
date: '2026-09-01 07:00'
template: entry
published: true
featured: true
hero_image: ''
lat: 43.024
lng: 10.603
location_city: Campiglia Marittima
location_country: Italy
weather_temp_c: 27
weather_desc: Sunny
---
Seven in the morning and the coast road is still cool. We loaded the bikes in the car park below the old town, the panniers heavier than they should be and the weather forecast saying nine consecutive days of sun. The route heads south first — down into the Maremma, then east, then a long loop back. Eight days. Nobody goes this way in September except cyclists and people who have got lost.
Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

@@ -0,0 +1,15 @@
---
title: 'Maremma in Full Sun'
date: '2026-09-02 11:30'
template: entry
published: true
hero_image: ''
lat: 42.612
lng: 11.171
location_city: Maremma
location_country: Italy
weather_temp_c: 29
weather_desc: Sunny
---
Eleven-thirty and already thirty degrees. The Maremma is agricultural land and scrubland and very little else, and in September it has the quality of a landscape that has given up trying. The road is straight, the sun is direct, the shadows are almost vertical. We stopped at a petrol station and drank two cans of something cold each. The man at the counter looked at us like people who had made a series of questionable decisions.
Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

@@ -0,0 +1,15 @@
---
title: 'The Lagoon at Dusk'
date: '2026-09-02 19:00'
template: entry
published: true
hero_image: ''
lat: 42.442
lng: 11.218
location_city: Orbetello
location_country: Italy
weather_temp_c: 24
weather_desc: Partly cloudy
---
Orbetello sits on a causeway between two lagoons and at dusk the light does something remarkable to the water. Pink flamingos — real ones, not ornamental — were standing in the shallows on the western side, perfectly still. We ate at a table outside overlooking the eastern lagoon. The sky turned orange and then purple and then a deep blue that was almost indistinguishable from the water. The wine was cold and the pasta had clams.
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

@@ -0,0 +1,15 @@
---
title: 'Orbetello Morning'
date: '2026-09-03 08:00'
template: entry
published: true
hero_image: ''
lat: 42.442
lng: 11.217
location_city: Orbetello
location_country: Italy
weather_temp_c: 22
weather_desc: Sunny
---
The lagoon at eight in the morning is a different thing from the lagoon at eight in the evening. Flat, silver, nearly silent. A single fisherman in a small boat about two hundred metres out, not appearing to fish. We left before the town had properly woken up, heading northeast on roads that climbed immediately and steeply into a landscape of oak and limestone that felt nothing like the coast we had left behind twenty minutes before.
Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

@@ -0,0 +1,15 @@
---
title: 'Tufa and Towers'
date: '2026-09-03 17:00'
template: entry
published: true
hero_image: ''
lat: 42.683
lng: 11.715
location_city: Sorano
location_country: Italy
weather_temp_c: 26
weather_desc: Sunny
---
Sorano appears on the horizon an hour before you reach it: a cluster of towers and walls on a pale cliff, floating above the valley. The closer you get the stranger it becomes. The town is not built on rock — the town is rock, volcanic tufa carved and inhabited over two thousand years. The Etruscans started it. Everyone since has just kept adding floors. We are staying the night and it already feels like somewhere that requires more time than we have.
Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

@@ -0,0 +1,15 @@
---
title: 'The Long Climb North'
date: '2026-09-04 15:00'
template: entry
published: true
hero_image: ''
lat: 43.077
lng: 11.678
location_city: "Val d'Orcia"
location_country: Italy
weather_temp_c: 23
weather_desc: Partly cloudy
---
Today was the hardest day. The route from Sorano to the Val d'Orcia crosses the eastern slope of Monte Amiata, which sounds manageable on a map and is not manageable at all. By noon we had climbed eleven hundred metres. By two we were somewhere above Seggiano in thin cloud, the views long gone, legs complaining in a language that had become very specific. Then the cloud lifted and the Val d'Orcia was simply there below us: pale roads, dark cypress, the whole thing exactly as advertised. Sometimes the landscapes that have been photographed to death are still worth arriving at.
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

@@ -0,0 +1,15 @@
---
title: 'Before the Heat Arrives'
date: '2026-09-05 08:30'
template: entry
published: true
hero_image: ''
lat: 43.078
lng: 11.676
location_city: Pienza
location_country: Italy
weather_temp_c: 21
weather_desc: Sunny
---
Six o'clock and the valley below Pienza is still in shadow. We left camp early on purpose — the route to Siena is long and September sun waits for no one. On the strade bianche the tyres make a sound like distant applause. No cars for the first two hours. Just the road and the light doing things to the cypress trees that would be embarrassing to describe in any other context.
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

@@ -0,0 +1,15 @@
---
title: 'Into Siena'
date: '2026-09-05 18:00'
template: entry
published: true
hero_image: ''
lat: 43.318
lng: 11.335
location_city: Siena
location_country: Italy
weather_temp_c: 25
weather_desc: Sunny
---
The approach to Siena by bike is through streets that get progressively older and steeper until suddenly the Campo is there. We had both seen it in photographs and the photographs are accurate in every way except one: they do not tell you how the square smells — stone and frying onions and the particular warm stillness of a Sienese summer evening. We sat on the pavement with our backs against the Palazzo Pubblico for forty minutes and did not want to be anywhere else.
Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

@@ -0,0 +1,15 @@
---
title: 'Florence by Nightfall'
date: '2026-09-06 20:00'
template: entry
published: true
hero_image: ''
lat: 43.767
lng: 11.253
location_city: Florence
location_country: Italy
weather_temp_c: 21
weather_desc: Cloudy
---
A long day. Siena to Florence is ninety kilometres and involves two significant climbs before you reach the Chianti hills, after which it becomes more manageable but you have already used the legs you needed. We came in from the south as the light was going, the city materialising from a distance as a density of rooftops and towers. The Arno appeared between buildings and we crossed it and then we were in, which is always a slightly surprising moment after a long day.
Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

@@ -0,0 +1,15 @@
---
title: 'One Rest Day'
date: '2026-09-07 14:00'
template: entry
published: true
hero_image: ''
lat: 43.769
lng: 11.255
location_city: Florence
location_country: Italy
weather_temp_c: 22
weather_desc: Partly cloudy
---
The bikes stayed in the basement. We walked instead, which after six days of cycling felt simultaneously easier and harder — easier on the legs, harder on the feet, which are used to being passive. Florence does not require a plan. Every street contains something. We crossed the Arno four times from different bridges, each one giving a slightly different version of the same view, all of them good.
Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

@@ -0,0 +1,15 @@
---
title: 'Dawn on the Cecina Coast'
date: '2026-09-08 07:30'
template: entry
published: true
hero_image: ''
lat: 43.553
lng: 10.313
location_city: Cecina
location_country: Italy
weather_temp_c: 20
weather_desc: Sunny
---
The last day starts on the coast road south of Cecina, the sea visible between the pine trees. We have been inland for most of the week and the smell of salt water is a surprise. The road is flat, which after eight days of Tuscan hills feels almost suspicious. We rode in silence for the first hour. There was nothing that needed saying.
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

@@ -0,0 +1,15 @@
---
title: 'Home'
date: '2026-09-08 16:30'
template: entry
published: true
hero_image: ''
lat: 43.017
lng: 10.587
location_city: Campiglia Marittima
location_country: Italy
weather_temp_c: 26
weather_desc: Sunny
---
The old town of Campiglia was visible on its hill for the last twenty kilometres, appearing and disappearing between the trees the way it had appeared on the horizon eight days ago when we left. The loop is complete: same car park, same view across the coast, different legs. The bikes went back in the car and we sat on a wall and counted the countries and the kilometres and the pasta dishes. Eight days, one loop, Tuscany in September. It was exactly what it was supposed to be.
@@ -0,0 +1,11 @@
---
title: Journal
template: dailies
content:
items: '@self.children'
order:
by: date
dir: desc
filter:
published: true
---
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
---
title: 'Tuscany 2026'
template: trip
date: '2026-09-01'
date_start: '2026-09-01'
date_end: '2026-09-08'
cover_image: ''
---
Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

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