Compare commits

..

221 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
m038 7dcaa703e0 ux: merge journey fields into entry/location tabs; unstack lat/lng and weather fields 2026-06-20 10:15:02 +02:00
m038 a3565677a5 demo: add hero images to all daily entries and Japan story; simplify Japan story image refs 2026-06-20 09:53:14 +02:00
m038 37c38e925a fix: add transport_mode to entry JSON serialisation in all three map templates; note bbox approach in isNearTrack 2026-06-20 00:54:04 +02:00
m038 3301f049cc feat: apply GPX connector algorithm to dailies feed mini-map 2026-06-20 00:47:39 +02:00
m038 b1665dad80 feat: use buildJourneySegments in trip.html.twig mini-map 2026-06-20 00:45:01 +02:00
m038 d9fd5eb74c feat: use buildJourneySegments in map.html.twig — suppress connectors covered by GPX 2026-06-20 00:42:34 +02:00
m038 dfca8ef6e2 feat: add GPX proximity algorithm to MapUtils (buildJourneySegments, extractTrackpoints)
Adds haversineKm, extractTrackpoints, isNearTrack, buildJourneySegments, and
addJourneySegments to the shared MapLibre GL IIFE. Updates MapUtils export to
expose the new functions. ES5-only; no arrow functions, const/let, or modules.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM
2026-06-20 00:39:39 +02:00
m038 6ce77d7be7 fix: restore entry.yaml original structure, keep only Journey tab addition 2026-06-20 00:36:48 +02:00
m038 2adf06831c feat: add force_connect and transport_mode fields to entry and story blueprints 2026-06-20 00:33:07 +02:00
m038 3772a64a0e fix: story back button uses history.back(); add demo images; fix story dates for chronological interleaving 2026-06-20 00:05:53 +02:00
m038 3bd1e61817 docs: add three Tuscany demo stories (gallery-led, scrollytelling-led, mood-fragment) 2026-06-19 23:41:48 +02:00
m038 14e386a122 fix: remove 1m per-step elevation threshold — Komoot data is pre-smoothed, threshold filtered nearly all gain/loss 2026-06-19 23:34:39 +02:00
m038 8152fe79b6 fix: compute GPX stats per-file to avoid spurious inter-track segments
Both stats.html.twig and trip.html.twig previously flattened all GPX
trackpoints into a single masterPts array before computing haversine
distance, elevation, and moving time. This caused the junction between
file N's last point and file N+1's first point to be treated as a real
segment — e.g. Florence→coast (~79 km, ~42 h) for Italy's 3-file demo
data, overstating distance and moving time significantly.

Fix: compute all metrics within each file independently and sum the
results. fileResults collection and callback consumption are unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM
2026-06-19 23:13:08 +02:00
m038 1a247e1889 fix: story template-story class, datetime attr, imageName escaping, raw content comments
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-19 23:12:47 +02:00
m038 103ceb62b9 fix: deterministic GPX ordering in parseGpxFiles (trip.html.twig) 2026-06-19 23:06:28 +02:00
m038 3845d1b5e4 docs: add demo story content (The Thousand Gates, all four shortcode blocks) 2026-06-19 23:04:11 +02:00
m038 c123a035ce feat: expand trip inline stats to 6 stats + add cycling panel with GPX parsing
- Expanded stats block from 4 to 6 stats (days, entries, countries, cities, distance, temp range)
- Added date_end-aware days calculation (uses header.date_end when available)
- Added cities dedup logic (seen_city_lower) matching Task 1 pattern
- Added temperature range computation (temp_min / temp_max)
- Added has_gpx boolean flag
- Distance label is conditional: km cycled (GPX) vs km roamed (no GPX)
- Stats note text is conditional to match distance mode
- Cycling button added to filter bar (only rendered when has_gpx)
- Cycling panel (7 stat blocks) added after stats block (hidden by default, toggled independently)
- Replaced old haversine IIFE with unified haversineKm + parseGpxFiles + IIFE
- GPX Mode A: fetches GPX files, sums trackpoint distances, populates cycling panel
- GPX Mode B: haversine between entry GPS points (no GPX)
- Updated .trip-stats-grid from repeat(4) to repeat(3) columns
- Added .trip-cycling-block, .trip-cycling-header, .trip-cycling-icon, .trip-cycling-title, .trip-cycling-grid CSS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM
2026-06-19 23:01:42 +02:00
m038 dfd1c38396 feat: add stories listing page and all story/shortcode CSS 2026-06-19 22:59:17 +02:00
m038 48b877c439 fix: deterministic multi-GPX trackpoint ordering and catch-path completion
Pre-allocate fileResults[idx] slots so GPX files always concatenate in URL
order regardless of fetch arrival order (Bug 1). Both .then and .catch now
call computeDistance() after decrementing pending so a failed last fetch no
longer leaves the distance element permanently blank (Bug 2).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM
2026-06-19 22:56:38 +02:00
m038 0dc9095b4b feat: add story.html.twig with hero scroll effect and shortcode JS 2026-06-19 22:56:00 +02:00
m038 fcdb3de387 feat: add pull-quote and snap-gallery shortcodes 2026-06-19 22:50:27 +02:00
m038 3b5dc18ec6 feat: expand stats page to 6 stats — cities, temp range, distance mode detection 2026-06-19 22:50:12 +02:00
m038 a06f744ec1 feat: add scrolly-section shortcode (Scrollama-driven sticky image steps) 2026-06-19 22:47:23 +02:00
m038 c514bfd4a9 feat: add story-blocks plugin with chapter-break shortcode 2026-06-19 22:43:54 +02:00
m038 916969c96f feat: journey line — Catmull-Rom spline curve, dotted subordinate style under GPX tracks 2026-06-19 22:33:48 +02:00
m038 3ef8d48ee2 feat: add entry url to map_entries data and as data-url attribute on all markers 2026-06-19 22:18:04 +02:00
m038 997baf4cc3 fix: marker click scrolls to card on home/trip pages instead of navigating (no url field) 2026-06-19 22:14:58 +02:00
m038 456fc94c8e fix: bump MapLibre CSS specificity to 020 — CDN loads after style.css so same-specificity rules lost 2026-06-19 22:11:21 +02:00
m038 044e74f5d3 feat: hover-only title tooltip on map markers; click navigates to entry 2026-06-19 22:05:52 +02:00
m038 f7df6ef37e fix: remove cooperativeGestures, increase fitBounds padding, add popups to embedded maps 2026-06-19 22:01:54 +02:00
m038 a363052f5f feat: migrate trip overview map to MapLibre GL (removes last Leaflet reference) 2026-06-19 21:57:12 +02:00
m038 b431cfc0ac feat: migrate mini-map and home map to MapLibre GL 2026-06-19 21:49:52 +02:00
m038 87a782ae12 feat: migrate full map page to MapLibre GL with animated journey line 2026-06-19 21:46:23 +02:00
m038 12c5b2c4a1 feat: add shared MapLibre GL utilities (journey line, markers) 2026-06-19 21:43:45 +02:00
m038 0d1688c6c4 Revert "revert: remove out-of-scope stats block (belongs in separate task)"
This reverts commit a9043f711e.
2026-06-19 21:40:09 +02:00
m038 a9043f711e revert: remove out-of-scope stats block (belongs in separate task) 2026-06-19 21:39:42 +02:00
m038 93005bd7cd fix: replace raw rgba with color-mix token in MapLibre attribution style 2026-06-19 21:39:21 +02:00
m038 fe0aa669bc style: swap Leaflet CSS override for MapLibre design-token styles 2026-06-19 21:36:35 +02:00
m038 897da36a21 feat: add inline stats block with toggle to trip page
Adds Twig computation for days on road, countries visited, and GPS
points; an expandable stats panel (hidden by default) with haversine
distance calculation; and toggle JS that activates the Stats button.
2026-06-19 21:35:54 +02:00
m038 eb739d80ab feat: wire up feed filter — All content / Journal / Stories
Added JavaScript to the trip.html.twig template that:
- Adds event listeners to filter buttons (.trip-filter-btn)
- Shows/hides article cards based on data-type attribute (journal/story)
- Manages active state of filter buttons
- Displays empty state message when no results match the filter
- Uses ES5 syntax (no arrow functions, const/let, or template literals)

Also added hidden feed-filter-empty element to display appropriate
empty messages for each filter type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB86BaJBG3eGiMdfhmHRrQ
2026-06-19 21:32:13 +02:00
m038 0478a18fa8 feat: add filter bar markup and pill button styles to trip page
Replace the old trip-nav links with a new filter bar component featuring:
- Three pill buttons for filtering (All content, Journal, Stories)
- "All content" button active by default with teal accent styling
- Separate Stats button with matching pill styling
- CSS for buttons with hover and active states
- Responsive flexbox layout that wraps on narrow screens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB86BaJBG3eGiMdfhmHRrQ
2026-06-19 21:29:33 +02:00
m038 2508936928 feat: add data-type attributes to feed cards; restyle story card with full border 2026-06-19 21:26:50 +02:00
m038 650e97883b demo: add placeholder hero images to Tuscany Gravel 2025 entries (QA)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB86BaJBG3eGiMdfhmHRrQ
2026-06-19 20:57:36 +02:00
m038 2eef8fbf9a fix: Leaflet void background corrected to actual CartoDB ocean color (#282828)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB86BaJBG3eGiMdfhmHRrQ
2026-06-19 20:54:22 +02:00
m038 11224289de fix: Leaflet void background matches CartoDB ocean color (#0d0d17)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB86BaJBG3eGiMdfhmHRrQ
2026-06-19 20:50:58 +02:00
m038 69c9f4f939 feat: trip page matches home layout — sticky map + feed, GPX route, no sidebar
- Same home-layout (45% sticky map / 55% scrollable feed) on every trip page
- GPX route overlay loaded from trip page media
- Marker click scrolls to entry card (same as home page)
- Map sub-nav link removed (map is now embedded)
- Separate /map page remains accessible by URL but has no nav link

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB86BaJBG3eGiMdfhmHRrQ
2026-06-19 20:42:41 +02:00
m038 010478b3fa fix: sort past trips descending by date (newest first)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB86BaJBG3eGiMdfhmHRrQ
2026-06-19 17:32:47 +02:00
m038 49d10f4816 fix: home map visible on mobile, invalidateSize on both maps
- Explicit height: 40vh on .home-map (not just 100% of parent) so Leaflet
  can measure the container reliably before CSS inheritance is resolved
- align-self: stretch on .home-map-col so it spans full width in flex column
- setTimeout invalidateSize(100ms) on home and dailies maps as safety net

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB86BaJBG3eGiMdfhmHRrQ
2026-06-19 17:29:52 +02:00
m038 a9eda558c0 fix: nav slash, back button context, home page max-width
- Past Trips nav link: add missing / (base_url_absolute has no trailing slash)
- Entry back link: history.back() with journal fallback, label → "← Back"
- Home page: max-width 1400px instead of none — narrows layout on wide screens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB86BaJBG3eGiMdfhmHRrQ
2026-06-19 17:01:45 +02:00
m038 16b44513f2 fix: stories escape link goes to trip page not archive 2026-06-19 15:51:51 +02:00
m038 ab8a5138dd feat: dailies merges stories, id attrs for map sync; stories escape link 2026-06-19 15:47:42 +02:00
m038 b66f1cdb2d feat: trip page — entry counts, merged feed, sticky sidebar index 2026-06-19 15:45:06 +02:00
m038 a78236bf3b feat: home page template — sticky map + merged feed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB86BaJBG3eGiMdfhmHRrQ
2026-06-19 15:42:43 +02:00
m038 a9843a0a2d feat: past trips archive with trip cards and entry counts 2026-06-19 15:40:15 +02:00
m038 5c98bf239a fix: add missing .trip-feed and .trip-sidebar-section CSS classes 2026-06-19 15:37:58 +02:00
m038 86b2778a47 feat: CSS for home layout, story cards, trip sidebar, escape link 2026-06-19 15:36:38 +02:00
m038 035c92f293 feat: home page routing — real / route, new nav (Home + Past Trips) 2026-06-19 15:34:12 +02:00
m038 fbc4fc195b fix: slice File to Blob before append so 3rd-arg filename is always used 2026-06-19 15:33:13 +02:00
m038 597add6c1d fix: use fd.append 3rd arg to set slugified filename in multipart upload 2026-06-19 15:13:00 +02:00
m038 1c9a6711b3 fix: slugify uploaded GPX filename before sending to API 2026-06-19 15:11:29 +02:00
m038 537f443cf1 feat: gpx-manager list, upload, delete via Grav API session auth 2026-06-19 14:58:25 +02:00
m038 e4451857c2 feat: gpx-manager template layout with trip sections 2026-06-19 14:57:59 +02:00
m038 feeef865aa feat: add gpx-manager page definition (access-protected) 2026-06-19 14:57:24 +02:00
m038 5c02432ce0 fix: use !important to override Leaflet default grey background 2026-06-19 13:22:53 +02:00
m038 d3ef42f04f fix: set leaflet-container background to match dark tile color, prevent grey flash 2026-06-19 13:21:50 +02:00
m038 bae9d68943 fix: switch map tiles to CartoDB dark (no API key required) 2026-06-19 13:18:36 +02:00
m038 dc162ff58c feat: switch to Stadia Alidade Smooth Dark map tiles 2026-06-19 13:11:42 +02:00
m038 3d5e29e26c feat: add paper grain texture, fix hardcoded colors, improve typography 2026-06-19 13:11:36 +02:00
m038 ba3a2ea9e7 feat: switch to warm-dark color tokens 2026-06-19 13:11:32 +02:00
m038 64b7fcc166 feat: Grav 2.0 compat — flex accounts/pages, api.super permission
- accounts.type: flex (required by admin2 API)
- pages.type: flex (required for admin2 pages API)
- Add access.api.super + api.access to mischa account (admin2 uses api.* permissions, not admin.*)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 13:02:25 +02:00
m038 0bb3b3bcce chore: consolidate all docs/plans/specs into main repo docs/
Moved from user/ repo: milestone specs, design spec, QA docs, research,
posting pipeline, bugs log, UI redesign plan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 10:36:47 +02:00
m038 a1acabbf17 chore: move docs/plans/specs to main repo 2026-06-19 10:36:39 +02:00
m038 70b4e1ca7a fix: use trip-relative URL for entry back-link, add active_trip sync comment
- entry.html.twig: replace hardcoded /tracker href with page.parent().url
- post-form.md: add comment to keep pageconfig.parent in sync with active_trip in site.yaml
2026-06-19 02:01:06 +02:00
m038 24f3c14d77 feat: add Admin blueprint for trip page type with date range, cover image, and album URL fields 2026-06-19 01:54:59 +02:00
m038 d1066d7eb3 chore: remove stale docs/demo/tracker (moved to docs/demo/trips/) 2026-06-19 01:52:36 +02:00
m038 ffda4568ab fix: update post form parent and add Italy 2025 demo trip with GPX routes
- Change pageconfig.parent from '/tracker' to '/trips/japan-korea-2026/dailies'
- Move japan-korea-2026 demo entries to docs/demo/trips/japan-korea-2026/dailies/
- Add Italy 2025 (Tuscany Gravel) demo trip: 5 entries with real Tuscany
  coordinates, plus trip.md, map/stats/stories stubs, and 3 GPX routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 01:50:02 +02:00
m038 86997cb878 feat: add GPX route rendering to trip map via leaflet-gpx
Adds leaflet-gpx@2.1.2 CDN script to map template, collects *.gpx
media files from the trip page, and renders them as teal polylines
beneath entry pins. Also fixes user/config/media.yaml to use the
required types: key so Grav's Media class correctly discovers .gpx
files. Map remains functional when no GPX files are present.
2026-06-19 01:38:36 +02:00
m038 50a5f2d178 feat: add trip/trips/stories templates, update nav and map/stats to use trip-relative paths
- Rename tracker.html.twig to dailies.html.twig; update dailies.md template key
- Fix map.html.twig and stats.html.twig: find dailies via page.parent().route
- Update base.html.twig nav to use config.site.active_trip for all hrefs
- Fix dailies.html.twig mini-map link to use page.parent().url/map
- Create trip.html.twig, trips.html.twig, stories.html.twig

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 01:27:39 +02:00
m038 2a32917568 refactor: rename tracker to dailies (URL slug), keep nav label as Journal 2026-06-19 01:24:43 +02:00
m038 24acae2a85 feat: restructure pages under trips/japan-korea-2026 entity
- Create trips/japan-korea-2026/{tracker,map,stats,stories} hierarchy
- Move 8 entry folders from 01.tracker into trips/.../01.tracker/
- Add active_trip: japan-korea-2026 to site.yaml
- Whitelist GPX file type in media.yaml
2026-06-19 01:19:41 +02:00
m038 534b9a96f1 feat: add demo entries with images + fix form upload action
- New demo entry: Arashiyama with single hero image (bamboo.jpg)
- New demo entry: Gyeongbokgung with four gallery images (palace-gate,
  throne-hall, hanok-rooftops, bugaksan)
- post-form.md: add upload: true to process block so filepond photo
  uploads are handled after page creation; simplify list-of-maps to
  flat map (Symfony YAML preserves insertion order)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 00:12:55 +02:00
m038 3a79fe2cc7 fix: use ISO date format for page date storage
Changes dateformat.default from 'd M Y' to 'Y-m-d H:i' so dates
submitted via the post form are stored as '2026-06-19 10:30' rather
than '18 Jun 2026'. This ensures add-page-by-form generates slugs in
YYYY-MM-DD-HHmm-title order, preserving chronological sort.

All templates use explicit |date() filters so display is unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 00:10:49 +02:00
m038 9a9220e066 docs: add posting pipeline reference and Admin entry blueprint
- posting-pipeline.md: full frontmatter reference, frontend form flow,
  Admin backend flow, page folder structure
- blueprints/entry.yaml: Admin form fields for city, country, lat/lng,
  weather condition dropdown, temperature, hero image

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 23:24:37 +02:00
m038 c05b9e3400 feat: add Grav 2.0 compat flag for cache-on-save plugin and switch GPM to testing channel
- Add blueprints.yaml for cache-on-save plugin with Grav 2.0 support
- Update system.yaml GPM setting from stable to testing channel
- Update .gitignore to allow cache-on-save plugin tracking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 23:19:27 +02:00
m038 4fcd74df8a Merge pull request 'Tracker ordering fix + March–April fixture entries' (#1) from experimental-polar-steps into main
Reviewed-on: #1
2026-06-18 22:41:53 +02:00
500 changed files with 989108 additions and 4709 deletions
+7 -1
View File
@@ -1,3 +1,9 @@
/plugins/ /plugins/*
!/plugins/.gitkeep
!/plugins/cache-on-save/ !/plugins/cache-on-save/
!/plugins/story-blocks/
/data/ /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. */
+20 -6
View File
@@ -1,13 +1,27 @@
login: mischa login: mischa
state: enabled
title: ''
email: mischa@gorinskat.nl
fullname: Mischa
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: access:
admin: admin:
login: true login: true
super: true super: true
site: site:
login: true login: true
state: enabled api:
title: Mischa super: true
email: mischa@gorinskat.nl access: true
fullname: Mischa
hashed_password: '$2y$10$dUEYTopGEDouFoAa/Wxw6.vsOA71yr3gSStfDvr10aKm4ih9ObQ7m'
language: en
+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: { }
+225
View File
@@ -0,0 +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 @@
salt: lsUHWFkCwvGZrL
+3 -2
View File
@@ -1,8 +1,9 @@
title: 'Into the East' title: 'Into the East'
description: 'A travel blog by Mischa'
author: author:
name: Mischa name: Mischa
email: mischa@gorinskat.nl email: mischa@gorinskat.nl
taxonomies: [category, tag]
metadata: metadata:
description: 'Into the East — travel journal' description: 'Into the East — travel journal'
description: 'A travel blog by Mischa'
active_trip: /trips/us-canada-mex-2024
travelling: false
+6 -6
View File
@@ -5,7 +5,7 @@ wrapped_site: false
reverse_proxy_setup: false reverse_proxy_setup: false
force_ssl: false force_ssl: false
force_lowercase_urls: true force_lowercase_urls: true
custom_base_url: 'http://100.96.115.96:8081' custom_base_url: ''
username_regex: '^[a-z0-9_-]{3,16}$' username_regex: '^[a-z0-9_-]{3,16}$'
pwd_regex: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}' pwd_regex: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}'
intl_enabled: true intl_enabled: true
@@ -28,10 +28,10 @@ languages:
pages_fallback_only: false pages_fallback_only: false
debug: false debug: false
home: home:
alias: /tracker alias: /home
hide_in_urls: false hide_in_urls: false
pages: pages:
type: regular type: flex
dirs: dirs:
- 'page://' - 'page://'
theme: intotheeast theme: intotheeast
@@ -41,7 +41,7 @@ pages:
list: list:
count: 20 count: 20
dateformat: dateformat:
default: 'd M Y' default: 'Y-m-d H:i'
short: 'D, d M Y G:i:s' short: 'D, d M Y G:i:s'
long: 'D, d M Y G:i:s' long: 'D, d M Y G:i:s'
publish_dates: true publish_dates: true
@@ -210,7 +210,7 @@ session:
domain: null domain: null
path: null path: null
gpm: gpm:
releases: stable releases: testing
official_gpm_only: true official_gpm_only: true
http: http:
method: curl method: curl
@@ -221,7 +221,7 @@ http:
verify_peer: true verify_peer: true
verify_host: true verify_host: true
accounts: accounts:
type: regular type: flex
storage: file storage: file
avatar: gravatar avatar: gravatar
flex: flex:
-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
---
@@ -1,23 +0,0 @@
---
title: 'Wheels Down at Narita'
date: '2026-03-25 15:40'
template: entry
published: true
hero_image: ''
lat: '35.7720'
lng: '140.3929'
location_city: 'Tokyo'
location_country: 'Japan'
weather_temp_c: 16
weather_desc: 'Sunny'
---
Eleven hours of flight time, two mediocre films, and one surprisingly good noodle dish from the trolley. Then the descent through scattered cloud, the first glimpse of grey-green patchwork below, and that particular feeling when the wheels finally touch down on a continent you have never stood on before.
Narita is large and orderly and very, very calm. Immigration moved faster than any airport I have ever been through. The officer looked at my passport, looked at me, stamped it once, and handed it back without a word. That was it. Entry to Japan.
The Narita Express runs direct to Shinjuku. I found a window seat and spent 90 minutes watching the city materialise from the outside in — rice fields giving way to low housing, then arterial roads, then the sudden verticality of central Tokyo rising up all at once as if someone just switched a setting.
The hotel is small but perfect. A room roughly the width of my arms outstretched, a window looking onto a grey concrete wall, and a bed that feels like sleeping on a cloud. I went out for ramen at a place around the corner where you order from a vending machine and sit at a counter alone with a small wooden partition between you and the next person. Nobody spoke. It was the best meal I have had in months.
Tomorrow: Ueno. The forecast says the cherry blossoms may finally be open.
@@ -1,23 +0,0 @@
---
title: 'Sakura in Ueno Park'
date: '2026-03-26 10:00'
template: entry
published: true
hero_image: ''
lat: '35.7155'
lng: '139.7753'
location_city: 'Tokyo'
location_country: 'Japan'
weather_temp_c: 14
weather_desc: 'Partly cloudy'
---
I arrived at Ueno Park at ten in the morning thinking I would beat the crowds. I was wrong. Several thousand people had the same idea, and the same Instagram instinct. But here is the thing about cherry blossom season in Japan — the crowds are almost part of it. Families with picnic sheets. Couples with matching outfits. Office workers in suits sitting on blue tarps eating convenience-store onigiri. Everyone doing the same thing: looking up at the same trees.
The blossoms were at maybe seventy percent. Enough to understand what the fuss is about.
I walked the park from one end to the other and then sat under a particularly generous tree for about an hour just watching people react to something beautiful. There is a Japanese word for it — *hanami* — which translates roughly as "flower viewing" and is more or less an entire cultural practice. You do not rush past the blossoms. You sit with them.
Later I found the Tokyo National Museum at the top of the park. Three floors of Japanese history, almost entirely in Japanese, which I cannot read, but context is its own language. A display case of Edo-period swords. Painted screens showing mountains I now recognise. A reconstructed tea house in the garden, closed for the season but visible through the glass.
Dinner: tonkatsu on a side street off Ueno-Okachimachi station. The woman who runs the counter has been there for at least thirty years by the look of it. She refilled my miso soup without being asked, twice.
@@ -1,27 +0,0 @@
---
title: 'Summit Clouds and Snow'
date: '2026-03-27 07:15'
template: entry
published: true
hero_image: ''
lat: '35.5095'
lng: '138.7646'
location_city: 'Kawaguchiko'
location_country: 'Japan'
weather_temp_c: 1
weather_desc: 'Snow'
---
Nobody told me it would snow.
I took the early bus from Shinjuku at 6:45am because the forecast for the Fuji Five Lakes region said "clear morning, clouds by noon." That is the window you want — Fuji is notorious for hiding inside its own weather system, and most visitors spend an entire day staring at a blank white sky where a mountain ought to be.
I got the mountain. For about forty minutes.
By the time the bus pulled into Kawaguchiko, the first flakes were already coming down. Light at first — the decorative kind that you hold your hand out for. Then, steadily, not decorative at all. I walked down to the lake with my bag under my jacket and stood at the water's edge while the snow thickened and Fuji turned from a sharply defined white cone into a suggestion, and then into nothing.
The lake surface was perfectly still. The snow fell straight down. There were no other tourists on the path, or if there were I could not see them. It was one of those moments of completely accidental solitude that you cannot plan for and would not trade.
I sat on a wooden bench on the lakefront for longer than made any meteorological sense. The snow kept falling. A single cormorant sat on a rock offshore and did not move the entire time I was there.
Caught the bus back to Shinjuku in the afternoon. The mountain never reappeared. I do not mind even slightly.
@@ -1,23 +0,0 @@
---
title: 'A Thousand Torii Gates'
date: '2026-03-28 11:30'
template: entry
published: true
hero_image: ''
lat: '34.9671'
lng: '135.7727'
location_city: 'Kyoto'
location_country: 'Japan'
weather_temp_c: 18
weather_desc: 'Sunny'
---
The Shinkansen from Tokyo to Kyoto takes two hours and twelve minutes. You travel at 285km/h. At one point Fuji appears out the right-hand window, clear and enormous and completely snow-covered, and the entire carriage rotates slightly to look at it. The mountain is visible for about four minutes. Then it is gone.
Kyoto is everything Tokyo is not: low, slow, wooden. The streets around Fushimi Inari were already warm with tourists at 11am but the shrine itself is large enough to absorb them. You walk under a tunnel of orange torii gates — thousands of them, each donated by a business and engraved with the donor's name — up a hillside through cedar forest, and the further you climb the more the crowd thins out.
I walked for two hours. Most visitors turn back at the first lookout. I kept going, past smaller shrines and stone fox statues and mossy steps worn down by a century of feet. Near the top the path was almost empty. The air smelled of pine and incense.
The city below spread out in all directions. Very few tall buildings — there are strict height regulations to preserve the sightlines. The Kamo River was a thin silver line running south. Distant mountains still wearing snow.
Dinner at a kaiseki restaurant in Gion, the old entertainment district. Eight small courses, each plated like a small still life. I ate slowly and said nothing and it was the right approach.
@@ -1,21 +0,0 @@
---
title: 'The Deer of Nara'
date: '2026-03-29 14:00'
template: entry
published: true
hero_image: ''
lat: '34.6851'
lng: '135.8048'
location_city: 'Nara'
location_country: 'Japan'
weather_temp_c: 17
weather_desc: 'Partly cloudy'
---
The deer at Nara are not afraid of you. This is the first thing you notice — not just that they tolerate humans, but that they regard you with a kind of benign indifference that borders on contempt. They walk into traffic. They push their noses into your pockets. They bow, which sounds enchanting and is, in practice, a manoeuvre to knock crackers out of your hand faster.
I bought a small bundle of *shika senbei* — deer crackers — from a vendor at the park entrance. They were gone in about forty-five seconds to a small gang of deer who appeared from nowhere and surrounded me in a tight semicircle. One bit my sleeve. Another headbutted a woman walking past who was not even involved.
Todai-ji temple is at the far end of the park and contains the largest bronze Buddha in Japan. The building is immense — apparently it was rebuilt at two-thirds the original size in the 18th century and is still the largest wooden structure in the world. The Buddha sits in the dim interior looking calm about this. There is a wooden pillar near the back with a hole cut through its base the same width as one of the Buddha's nostrils. Schoolchildren queue to crawl through it. Wisdom awaits on the other side.
The train back to Kyoto takes 45 minutes through flat agricultural land. The deer do not follow you.
@@ -1,25 +0,0 @@
---
title: 'Dotonbori After Dark'
date: '2026-03-30 18:00'
template: entry
published: true
hero_image: ''
lat: '34.6687'
lng: '135.5017'
location_city: 'Osaka'
location_country: 'Japan'
weather_temp_c: 19
weather_desc: 'Cloudy'
---
Osaka is louder than Kyoto and prouder of it. Kyoto has temples and restraint. Osaka has neon and takoyaki and a sign the size of a building advertising a restaurant with a mechanical crab on the front. Both are correct.
I arrived from Kyoto mid-afternoon, dropped my bag, and went directly to Dotonbori to get my bearings before the evening crowd descended. The canal runs through the entertainment district, and on both sides there are restaurants stacked six floors high with illuminated signs competing for your attention so aggressively that after ten minutes you start to tune out the sensory overload and just walk.
At six in the evening the neon started properly. The famous running man billboard. The Glico sign. Streets full of people eating while walking — takoyaki (octopus balls, better than they sound), skewered meats, cones of spicy shrimp. Osaka has a word for its own food philosophy: *kuidaore*, which means "eat until you drop."
I took it as guidance.
Three hours of eating across four separate establishments. Kushikatsu — battered and deep-fried everything — at a counter in an alley so narrow that diners on opposite sides can shake hands across the table. Soft-serve matcha ice cream on the street. Okonomiyaki from a woman who pressed the pancake flat with a heavy iron tool and would not let me touch anything.
The canal was dark and the lights were reflected in it and for a while I just stood on the bridge watching people eat.
@@ -1,23 +0,0 @@
---
title: 'Seoul Calling'
date: '2026-04-01 09:00'
template: entry
published: true
hero_image: ''
lat: '37.5635'
lng: '126.9851'
location_city: 'Seoul'
location_country: 'South Korea'
weather_temp_c: 10
weather_desc: 'Rain'
---
The flight from Osaka to Seoul takes one hour and forty minutes. Shorter than some commutes I have had. At Incheon I changed SIM cards, changed currency, changed alphabet, and walked out into a grey April morning with rain coming in off the Yellow Sea.
Korea hits differently than Japan. Japan felt deliberate and enclosed, every surface managed, every system timed to the second. Seoul feels faster and more argumentative, as if things are still being decided. The streets around Myeongdong were already busy at 9am: coffee shops the size of ballrooms, street vendors selling *hotteok* (sweet pancakes) from portable griddles, and the particular energy of a city that moves at one speed regardless of the weather.
My guesthouse is in Mapo-gu, a neighbourhood that turns out to be significantly cooler than anywhere the guidebooks sent me. Independent coffee roasters. Record shops. A gallery in a converted printing house showing black-and-white photography of the Han River in the 1970s.
I spent the afternoon walking the Han River itself — a massive green ribbon running through the city with dedicated cycling paths, outdoor fitness equipment, and Koreans doing every possible outdoor activity despite the rain. A group of older men playing badminton with very serious expressions. Two people kayaking. A family of five sharing a communal barbecue under an umbrella.
Dinner: Korean fried chicken at a place that opened at 5pm and was full by 5:05. Beer so cold it was almost painful. Outside, the rain kept up steadily. I stayed longer than I meant to.
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
+5
View File
@@ -0,0 +1,5 @@
---
title: Stories
template: stories
published: true
---
+8
View File
@@ -0,0 +1,8 @@
---
title: 'Tuscany Gravel 2025'
template: trip
date: '2025-09-01'
date_start: '2025-09-01'
date_end: '2025-09-08'
cover_image: ''
---
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
+4
View File
@@ -0,0 +1,4 @@
---
title: 'Trip Map'
template: map
---
+4
View File
@@ -0,0 +1,4 @@
---
title: 'Trip Stats'
template: stats
---
@@ -0,0 +1,5 @@
---
title: Stories
template: stories
published: true
---
+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: ''
---
-368
View File
@@ -1,368 +0,0 @@
# Into the East — Design Spec
**Date:** 2026-06-18
**Status:** Approved for implementation
---
## 1. Direction
**The brief:** A personal travel journal, sole author, trip to East Asia. Three weeks to implement before departure. Audience is both friends/family and the occasional curious stranger.
**The position:** Neither Polarsteps nor FindPenguins. Both optimize for social sharing of travel data. This site optimizes for **the story** — and should feel like reading a well-edited travel journal, not using an app.
**What we steal from each:**
- Polarsteps: photography-first hierarchy, airy whitespace, map as the emotional spine of the trip
- FindPenguins: typography as brand identity, stats as trophy case, hierarchical trip → entry structure
**What we do better than both:**
- Web-native: fast, linkable, no install, works on any browser
- Single author = pure editorial voice, no social noise
- Full CSS control = real typographic identity, not generic app chrome
- Editorial feel: more travel magazine, less productivity dashboard
**Aesthetic direction:** Field notes. The kind of journal a thoughtful traveler would carry — clean, direct, lets the photography speak. Sophisticated without effort.
**The one aesthetic risk:** Full-bleed hero photography with a translucent date+location overlay at the bottom of each card. The photo IS the entry card — not a thumbnail beside text. This is the single element that distinguishes this design from both reference apps and from typical blog layouts.
---
## 2. Color System
### Palette
| Token | Hex | Usage |
|---|---|---|
| `--color-ink` | `#17171A` | Primary text (near-black with cool undertone, like ink) |
| `--color-ink-2` | `#4A4850` | Secondary text, body paragraphs |
| `--color-ink-muted` | `#9896A0` | Labels, timestamps, captions, placeholder text |
| `--color-paper` | `#F7F5F2` | Page background (warm paper white, not blue-white) |
| `--color-canvas` | `#FFFFFF` | Card backgrounds, modals, form surfaces |
| `--color-border` | `#E8E6E3` | Standard dividers, card borders |
| `--color-border-soft` | `#F0EDEA` | Subtle section dividers |
| `--color-accent` | `#1F6B5A` | Deep teal — brand color, links, CTAs, active states |
| `--color-accent-hover` | `#185647` | Darkened accent for hover/pressed states |
| `--color-accent-light` | `#EBF5F2` | Pale teal for highlight backgrounds |
| `--color-accent-on` | `#FFFFFF` | Text on accent-colored surfaces |
### Rationale for accent color
Deep teal `#1F6B5A` was chosen over:
- Blue (#0066cc current): too generic, too tech
- Orange/saffron: clichéd for "Asia" travel design
- Terracotta/cream: the most common default for lifestyle/travel blogs
Teal evokes bamboo, celadon porcelain, ancient jade, the color of temple gardens — all without being literal or kitsch. It works cleanly against both the warm paper background and white card surfaces.
---
## 3. Typography
### Fonts
| Role | Family | Fallback | Source |
|---|---|---|---|
| Display / Headings | DM Serif Display | Georgia, serif | Google Fonts |
| UI / Body / Labels | DM Sans | -apple-system, BlinkMacSystemFont, sans-serif | Google Fonts |
**Google Fonts URL:**
```
https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=DM+Serif+Display:ital@0;1&display=swap
```
**Why this pairing:**
DM Serif Display has a calligraphic quality — slightly editorial, authoritative but not stiff. Paired with DM Sans (its designed companion) the system is cohesive. DM Sans is neutral and highly legible at all sizes. Both are under-used relative to Inter/Lato/Playfair, so the combination has a distinctive voice without being trendy.
### Type Scale
| Token | Size | Line Height | Usage |
|---|---|---|---|
| `--text-xs` | 0.75rem (12px) | 1.5 | Badges, captions |
| `--text-sm` | 0.875rem (14px) | 1.5 | Meta, timestamps, labels |
| `--text-base` | 1rem (16px) | 1.65 | Body paragraphs |
| `--text-md` | 1.125rem (18px) | 1.55 | Lead text, intro paragraphs |
| `--text-lg` | 1.375rem (22px) | 1.35 | Subheadings, card titles (mobile) |
| `--text-xl` | 1.75rem (28px) | 1.25 | Entry card titles |
| `--text-2xl` | 2.25rem (36px) | 1.2 | Page headings, entry titles (desktop) |
| `--text-3xl` | 3rem (48px) | 1.1 | Hero entry title |
### Usage rules
- Entry titles: `--font-display`, `--text-xl` (mobile) / `--text-2xl` (desktop)
- Site title in header: `--font-display`, `--text-lg`
- All other UI text: `--font-ui`
- Body paragraphs: `--font-ui`, `--text-base`, `--leading-normal`
- Timestamps/badges: `--font-ui`, `--text-xs`, uppercase, `letter-spacing: 0.07em`
---
## 4. Spacing & Layout
### Spacing scale (4px base unit)
| Token | Value |
|---|---|
| `--space-1` | 0.25rem (4px) |
| `--space-2` | 0.5rem (8px) |
| `--space-3` | 0.75rem (12px) |
| `--space-4` | 1rem (16px) |
| `--space-5` | 1.25rem (20px) |
| `--space-6` | 1.5rem (24px) |
| `--space-8` | 2rem (32px) |
| `--space-10` | 2.5rem (40px) |
| `--space-12` | 3rem (48px) |
| `--space-16` | 4rem (64px) |
### Layout
- Content max-width: `720px` (comfortable reading at any font size)
- Page horizontal padding: `1.25rem` (mobile), `1.5rem` (desktop ≥520px)
- Header height: `60px` (fixed, for JS offset calculations)
- Map page: full viewport, no content max-width constraint
### Border radius
| Token | Value | Usage |
|---|---|---|
| `--radius-sm` | 4px | Photo corners, small chips |
| `--radius-md` | 8px | Cards, buttons, inputs |
| `--radius-lg` | 12px | Large cards, modals |
| `--radius-full` | 9999px | Pills, badges |
### Shadows
| Token | Value | Usage |
|---|---|---|
| `--shadow-sm` | `0 1px 3px rgba(0,0,0,0.08)` | Stat blocks, subtle elevation |
| `--shadow-md` | `0 4px 12px rgba(0,0,0,0.10)` | Cards on hover, dropdowns |
| `--shadow-lg` | `0 8px 24px rgba(0,0,0,0.14)` | Lightbox, modals |
---
## 5. Component Inventory
### 5.1 Site Header
```
[ into the east ] [ Journal Map Stats ]
← accent bar across top (3px) ───────────────────────────────
```
- Top border: `3px solid var(--color-accent)` — thin accent bar signals the brand color without decorating
- Site title: DM Serif Display, `--text-lg`, no decoration
- Nav links: DM Sans, `--text-sm`, weight 500, `--color-ink-2`
- Active nav link: `--color-accent`, weight 600
- Mobile: same layout, title slightly smaller, nav links compact
- Background: `--color-canvas` (white), bottom border `1px solid var(--color-border)`
### 5.2 Entry Feed Card — With Photo
```
┌─────────────────────────────────────┐
│ │
│ [photo] │ ← full-width, 16:9, rounded corners
│ │
│ 18 JUN · 📍 Kyoto, Japan │ ← overlaid at bottom, gradient mask
└─────────────────────────────────────┘
Arrived in Tokyo ← DM Serif Display, --text-xl
After 14 hours of flying I finally ← body excerpt, --color-ink-2
set foot on Japanese soil...
Read entry → ← --color-accent, --text-sm
```
- Photo: `aspect-ratio: 16/9`, `object-fit: cover`, `border-radius: var(--radius-md)`
- Photo has a `linear-gradient(to top, rgba(0,0,0,0.55), transparent)` overlay at the bottom 40%
- Date + location sit on top of gradient in white text (`rgba(255,255,255,0.92)`)
- On hover: photo scales to 1.03 (subtle zoom, 0.4s ease)
- Title below photo: DM Serif Display, hover turns `--color-accent`
- Card separation: `padding-bottom: var(--space-12)` + `border-bottom: 1px solid var(--color-border)`
### 5.3 Entry Feed Card — No Photo
When no photo is available, fall back to a text-only layout:
```
18 JUN 2026 · 📍 Kyoto, Japan ← meta row, --text-sm, --color-ink-muted
Arrived in Tokyo ← DM Serif Display, --text-xl
After 14 hours of flying...
Read entry →
```
- No photo container
- Meta (date + location) on one line above title, small + muted
### 5.4 Single Entry Page
```
Wednesday, 18 June 2026 ← --text-sm, --color-ink-muted, uppercase
📍 Kyoto, Japan · ⛅ Partly cloudy · 22°C
Arrived in Tokyo ← DM Serif Display, --text-2xl / --text-3xl
─────────────────────────────────────
Body text content... ← --font-ui, --text-base/md
[Photo gallery — 2 or 3 col grid]
← Back to journal
```
- The entry title uses `--font-display` at largest scale
- A thin `--color-border` rule separates the header from the body
- Body text is `--text-md` (18px) for comfortable long-form reading
- Full-bleed hero option: if a `hero_image` is set, it spans the full content width with a bottom margin
### 5.5 Post Form (Author View)
```
New Entry
Title * [________________________]
Date & Time [2026-06-18 14:30 ]
What happened [ ]
today? [ ]
[ ]
Photos [ + Add photos (max 4) ]
City [________________________]
Country [________________________]
[ 📍 Get Location ] [ 🌤 Get Weather ]
✓ Location captured: Kyoto, Japan ← status line
[ Post Entry ]
```
UX changes from current:
- Lat/lng inputs **hidden from the UI** (remain in the form as `display:none` for data capture, filled by JS)
- Location status shows captured city/country + coordinates in a single line (not separate status paragraphs)
- Photo upload area: larger touch target, visual indication of count
- "Post Entry" button: `--color-accent` background, full-width on mobile, `min-height: 52px`
- Form fields: `--radius-md` corners, `--color-border` border, focus ring in `--color-accent`
- Section spacing: generous vertical rhythm on mobile
### 5.6 Stats Page
```
┌────────────┐ ┌────────────┐
│ 42 │ │ 18 │
│ days on │ │ entries │
│ the road │ │ posted │
└────────────┘ └────────────┘
┌────────────┐ ┌────────────┐
│ 6 │ │ ~14,200 │
│ countries │ │ km │
│ visited │ │ traveled │
└────────────┘ └────────────┘
Countries visited
Japan · South Korea · Mongolia · Russia · Finland · Estonia
```
- Numbers: `--font-display`, `--text-3xl`, `--color-accent`
- Labels: `--font-ui`, `--text-xs`, uppercase, `--color-ink-muted`
- Cards: white, `--shadow-sm`, `--radius-md`, centered
### 5.7 Map Page
Minimal changes — the map itself is good. Style improvements:
- Leaflet popups: match the new design (DM Sans, `--radius-md`, `--shadow-md`)
- Markers: keep current circle style, update color to `--color-accent`
- Feed mini-map wrapper: match `--radius-md`, `--border`
---
## 6. UX Flows
### 6.1 Reader — First Visit
1. Land on `/tracker` (journal feed)
2. See mini-map above fold (if entries exist) — route tells the geographic story at a glance
3. First entry card: full-bleed hero photo with date/location overlay — immediate emotional pull
4. Scroll through chronological entries
5. Tap/click entry → entry detail page
6. Navigate back via "← Back to journal"
**Key principle:** The reader should understand the journey spatially (mini-map) and emotionally (hero photo) before reading a single word.
### 6.2 Reader — Navigation
- Journal: primary destination, the feed
- Map: geographic exploration mode
- Stats: quick numbers, satisfying progress indicator
- No account required, no social friction, no login prompt for readers
### 6.3 Author — Posting from Mobile
1. Navigate to `/post` (bookmark on home screen)
2. Already logged in (Grav session persists) — form loads directly
3. **Title**: tap → type (autofocused)
4. **Date & Time**: auto-filled to now, adjust if needed
5. **Content**: write what happened
6. **Photos**: tap "Add photos" → camera or gallery → select up to 4
7. **Location**: tap "📍 Get Location" → GPS fires → status shows "Kyoto, Japan · 34.985, 135.758" in one line
8. **Weather**: tap "🌤 Get Weather" (works only if location was captured) → status shows "Partly cloudy · 22°C"
9. **City/Country**: auto-populated from GPS is a nice-to-have for v2; in v1 type manually if needed
10. Tap "Post Entry" → success message → 2-second pause → redirect to /tracker (new entry visible at top)
**Key principles:**
- One-thumb operation for all critical actions on mobile
- Location/weather are conveniences, not blockers — can skip both
- Visual feedback is immediate (status line updates on GPS response)
- After submit: don't leave author on a success message page; redirect to see their new post
---
## 7. Mobile Specifics
### Touch targets
- All interactive elements: `min-height: 44px`, `min-width: 44px` (Apple HIG standard)
- Form buttons: `min-height: 52px` on the post form (primary CTA)
- Nav links: `padding: 0.5rem 0.75rem`
### Viewport concerns
- Map page: `height: calc(100vh - 60px)`, `touch-action: none` on map container — prevents scroll trap
- Photo lightbox: full viewport overlay, swipe-friendly (keyboard + click already implemented)
- Form on mobile: single-column, generous input padding `0.875rem 1rem`, `font-size: 1rem` (prevents iOS zoom on focus)
### Performance
- Google Fonts: loaded with `preconnect` hints
- Images: `loading="lazy"` on all non-above-fold images (already in place)
- Leaflet: loaded from CDN, only on pages that need it
- No new JS frameworks — vanilla JS throughout
---
## 8. Tech Stack Decision
**Keep Grav CMS.** With a 3-week timeline, replacing it would consume all available time on migration rather than design improvements.
| Layer | Decision | Rationale |
|---|---|---|
| Backend | Grav CMS (PHP, Twig) — unchanged | Works, flat-file, no DB |
| CSS | Vanilla CSS + custom properties (design tokens) | No build step, full control, ships as one file |
| JS | Vanilla JS — unchanged | Current JS is well-structured, scope doesn't justify a framework |
| Icons | Unicode + emoji (current) | No dependency, works everywhere |
| Fonts | Google Fonts via CDN | Two fonts, display-swap, negligible impact |
| Maps | Leaflet.js (current) | Already in use, no reason to change |
| Build | None — no build pipeline | Grav's asset pipeline handles minification if needed |
**No Alpine.js, no TypeScript, no Tailwind.** The site has clean vanilla JS and CSS today; a redesign is about visual quality, not framework migration. Introducing a build pipeline on a 3-week timeline is a distraction.
---
## 9. What Changes From Current Design
| Area | Current | New |
|---|---|---|
| Typography | System sans-serif only | DM Serif Display for headings + DM Sans for UI |
| Accent color | `#0066cc` (generic blue) | `#1F6B5A` (deep teal) |
| Background | `#ffffff` (pure white) | `#F7F5F2` (warm paper) |
| Entry cards | Thumbnail + text below | Full-bleed 16:9 photo with overlay |
| Header | No visual identity | Accent top-border, typographic title |
| Design tokens | Hardcoded values throughout | CSS custom properties throughout |
| Post form | Lat/lng visible inputs | Lat/lng hidden, single status line |
| Font loading | None | Google Fonts DM pairing |
| Hover states | Minimal | Photo zoom, title color change |
| Stat numbers | `#0066cc` | `--color-accent` (#1F6B5A) |
-193
View File
@@ -1,193 +0,0 @@
# Milestone 1 Spec — Entry Enrichment
**Goal:** Every entry is richer out of the box — location name shown, weather auto-captured, photos in a proper gallery, hero image visible on the feed.
---
## User Stories
- As a traveler (Mischa), when I submit the post form, I want my current weather conditions auto-filled so I don't have to look them up manually.
- As a traveler, I want to type my city and country once and have it appear on the entry and in the feed card, so readers know where I am without reading the whole post.
- As a reader, when I scan the feed, I want to see a thumbnail photo and location for each entry so I can quickly get a sense of where Mischa is and whether to read the full entry.
- As a reader, when I open an entry, I want to see all uploaded photos in a gallery I can browse, not a wall of raw images.
- As a traveler, when I submit a form without photos, the entry should still display cleanly with no broken image placeholders.
---
## Feature Details
### 1.1 — Location Name Field on Post Form
**What:** Add two text fields to the post form: `location_city` and `location_country`.
**Behavior:**
- Both are optional (GPS coordinates are also optional)
- Placeholder text: "e.g. Kyoto" and "e.g. Japan"
- Displayed below the lat/lng fields
- On submit, stored in entry frontmatter as `location_city` and `location_country`
- On the form, shown as a single labeled group "Location Name" with two side-by-side inputs on desktop, stacked on mobile
**Edge cases:**
- If left blank: entry shows no location badge. No error, no broken UI.
- Long city names (e.g. "Ulaanbaatar") must not overflow card layout.
- Special characters (accents, non-Latin) must render correctly.
**Mobile behavior:** Both fields full-width, stacked, 44px min touch targets.
---
### 1.2 — Weather Auto-Fetch on Post Form
**What:** A "Get Weather" button on the post form that calls the Open-Meteo free API (no API key) using the lat/lng already entered, and fills hidden weather fields.
**Fields to fetch and store:**
- `weather_temp_c` — temperature in Celsius (integer)
- `weather_desc` — short description: one of: Sunny, Partly cloudy, Cloudy, Foggy, Drizzle, Rain, Snow, Thunderstorm (derived from WMO weather code)
**WMO code mapping (Open-Meteo uses WMO codes):**
- 0 → Sunny
- 1,2 → Partly cloudy
- 3 → Cloudy
- 45,48 → Foggy
- 51,53,55,56,57 → Drizzle
- 61,63,65,66,67,80,81,82 → Rain
- 71,73,75,77,85,86 → Snow
- 95,96,99 → Thunderstorm
**API call:**
```
https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lng}&current=temperature_2m,weather_code&temperature_unit=celsius
```
**UX flow:**
1. User fills in lat/lng (manually or via "Get Location" button)
2. User taps "Get Weather" button
3. Button shows "Fetching…" while loading
4. On success: fills temp and desc fields (visible, editable text inputs)
5. On failure (no network, no lat/lng): shows inline error "Could not fetch weather — enter manually"
**Edge cases:**
- If lat/lng not filled when button tapped: show inline error "Enter coordinates first"
- Weather fields are always editable manually (auto-fill is a convenience, not mandatory)
- If weather fields left blank: entry shows no weather badge. No broken UI.
- Open-Meteo returns current conditions, not historical — this is fine for posting in real time
**Mobile behavior:** "Get Weather" button is full-width, 44px height, placed immediately below the lat/lng + location name fields.
---
### 1.3 — Weather Display on Entry Page
**What:** If `weather_temp_c` or `weather_desc` is present in frontmatter, display a weather badge on the entry page.
**Display format:** `☀️ Sunny · 28°C` (icon + description + temperature)
- Icon chosen from a small set based on `weather_desc`:
- Sunny → ☀️
- Partly cloudy → ⛅
- Cloudy → ☁️
- Foggy → 🌫️
- Drizzle → 🌦️
- Rain → 🌧️
- Snow → ❄️
- Thunderstorm → ⛈️
**Placement:** In the entry header, between the date and the body text. Same line as GPS coordinates if those are shown.
**Edge cases:**
- Only temp, no desc → show temp only
- Only desc, no temp → show desc only
- Neither → hide weather section entirely
- Temperature should always be integer (round if float)
---
### 1.4 — Location Badge on Feed Cards and Entry Page
**What:** Display `location_city, location_country` as a small badge on tracker feed cards and at the top of entry pages.
**Feed card:** Below the date, above the excerpt. Format: `📍 Kyoto, Japan`
**Entry page:** In the header below the date, above the content. Format: `📍 Kyoto, Japan`
**Edge cases:**
- Only city, no country → `📍 Kyoto`
- Only country, no city → `📍 Japan`
- Neither → location badge hidden entirely
- Long location names: truncate with ellipsis at 30 chars on cards (full text on entry page)
---
### 1.5 — Photo Gallery on Entry Page
**What:** Photos uploaded to an entry should display in a responsive grid gallery with lightbox (click to enlarge).
**Implementation approach:** Use Grav's native media collection for the entry page. Each `.entry` folder contains its photos. Render them in a grid in `entry.html.twig`. Use a minimal vanilla JS lightbox — no external framework.
**Gallery behavior:**
- Photos displayed in a 2-column grid on mobile, 3-column on desktop
- Each thumbnail is square-cropped, 150px on mobile
- Clicking/tapping a thumbnail opens a lightbox overlay
- Lightbox: dark overlay, full-size image centered, tap/click outside or press Escape to close
- Left/right navigation arrows in lightbox (swipe on mobile)
- No captions needed for v1
**Edge cases:**
- 0 photos: gallery section hidden entirely
- 1 photo: still uses grid (single item), lightbox works
- Many photos (>10): gallery still renders (no hard limit on display)
- Non-image files in the media folder: skip them (only render jpg, jpeg, png, webp, gif)
---
### 1.6 — Hero Image on Tracker Feed Cards
**What:** If an entry has photos, the first photo (or the one named in `hero_image` frontmatter) appears as a thumbnail on the tracker feed card.
**Implementation:** In `tracker.html.twig`, for each entry:
1. If `entry.header.hero_image` is set, use `entry.media[entry.header.hero_image]`
2. Else, use the first image in `entry.media` sorted by name
3. Render as a 16:9 aspect-ratio thumbnail, full width of card, above the title
**Edge cases:**
- No photos: card shows no image, just text. No broken `<img>` tag.
- `hero_image` set but file missing: fall back to first media file, or no image
- Very tall/wide images: CSS `object-fit: cover` maintains card aspect ratio
---
## Out of Scope (Milestone 1)
- Map features (Milestone 2)
- Statistics page (Milestone 3)
- Video support
- Comments or reactions
- Automated reverse geocoding (city name comes from form input, not auto-detected)
- Altitude display (data may not be present)
- Historical weather (Open-Meteo current endpoint only)
---
## Acceptance Criteria
1. Post form has `location_city` and `location_country` fields that save to entry frontmatter
2. Post form has "Get Weather" button that fills `weather_temp_c` and `weather_desc` via Open-Meteo when lat/lng are provided
3. Entry page shows weather badge when weather fields are present; hidden when absent
4. Entry page shows location badge `📍 City, Country` when location fields are present; hidden when absent
5. Tracker feed card shows location badge when present
6. Tracker feed card shows a hero image when photos exist for an entry
7. Entry page shows a 2-col (mobile) / 3-col (desktop) photo grid
8. Clicking any photo opens a full-screen lightbox with prev/next navigation
9. Pressing Escape or clicking outside lightbox closes it
10. All fields are optional — empty values produce no broken UI elements
11. All interactive elements meet 44px minimum touch target on mobile
12. Form submits correctly with all new fields populated or all blank
---
## Design Notes
- Weather and location badges should be subtle — small text, muted color, not the visual focus
- Use emoji icons for weather — universal, no icon font dependency
- Gallery grid: `gap: 4px` between thumbs, no borders, square crops
- Lightbox: `background: rgba(0,0,0,0.92)`, image centered with `max-height: 90vh`
- Feed card image: `aspect-ratio: 16/9`, `object-fit: cover`, rounded top corners matching card
-166
View File
@@ -1,166 +0,0 @@
# Milestone 2 Spec — Interactive Map
**Goal:** A `/map` page shows all entries as markers on an interactive Leaflet.js map, connected by a chronological route line, with popups linking to entries.
---
## User Stories
- As a reader, I want to see a world map showing where Mischa has been so I can understand the journey at a glance without reading every entry.
- As a reader, I want to click a map marker and see the entry date, title, and a thumbnail — and be able to click through to the full entry.
- As a reader on mobile, I want to pan and pinch-zoom the map with my fingers without the page scrolling underneath.
- As a traveler (Mischa), I want the map to automatically include every entry that has lat/lng data — I should not need to do any manual map maintenance.
- As a reader, I want the map to show the route line connecting stops in the order they were visited, so the journey makes narrative sense.
---
## Feature Details
### 2.1 — Map Page
**Route:** `/map`
**Template:** `map.html.twig` — extends `partials/base.html.twig`
**Page file:** `user/pages/03.map/map.md`
**Content:**
- Full-viewport-height map container below the site header
- Leaflet.js loaded from CDN (jsDelivr): `https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.js`
- Leaflet CSS from same CDN
- Tile layer: OpenStreetMap (free, no API key): `https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`
- Attribution: "© OpenStreetMap contributors"
**Map initialization:**
- Default zoom: auto-fit to bounds of all markers (use `map.fitBounds()`)
- If no entries with GPS data: show world view, zoom 2, centered at 0,0 with a message "No locations yet"
- Min zoom: 2, Max zoom: 18
---
### 2.2 — Entry Data Serialization
**How entries reach the map JS:**
In `map.html.twig`, Grav's Twig will iterate all published entries under `/tracker` and serialize them to a JSON array embedded in a `<script>` tag:
```js
var ENTRIES = [
{
"lat": 48.8566,
"lng": 2.3522,
"title": "Paris morning",
"date": "2026-06-18",
"url": "/tracker/2026-06-18",
"hero": "/path/to/thumb.jpg" // null if no photo
},
...
];
```
**Only entries with valid lat AND lng are included** (skip entries where either is empty/null).
Entries sorted ascending by date (oldest first) so the route line is drawn in travel order.
---
### 2.3 — Route Polyline
**What:** A colored line drawn between entry markers in chronological order.
**Style:**
- Color: `#0066cc` (brand blue, matches existing CSS)
- Weight: 3px
- Opacity: 0.7
- No arrow heads for v1
**Behavior:**
- Line drawn between consecutive entries (by date) that have valid GPS
- If only 1 entry: no line (just a single marker)
- If two consecutive entries are very far apart (>5000km): line still drawn — it's a flight, expected
---
### 2.4 — Entry Markers
**What:** One circular marker per entry with GPS coordinates.
**Marker design:**
- Custom circular marker (not default Leaflet teardrop)
- Color: `#0066cc` fill, white border, 2px border
- Size: 12px diameter on mobile, 14px on desktop
- Most recent entry: larger (18px) and brighter color to indicate "current location"
**Popup on click/tap:**
```
[thumbnail if available — 120px wide, 80px tall, cover cropped]
📅 18 June 2026
Paris morning
[Read entry →]
```
- Popup width: 180px max
- "Read entry →" links to the entry page
- Tapping outside popup closes it
**Edge cases:**
- Two entries at the same lat/lng: Leaflet clusters or offsets them slightly (use small offset to prevent exact overlap — just add 0.0001° offset per duplicate)
- Entry with GPS but no photo: popup shows no image, just date + title + link
---
### 2.5 — Mobile Map UX
**Problem:** On mobile, a map inside a scrollable page creates a scroll-trap (finger intended for page scroll gets captured by map pan).
**Solution:**
- Map container is `height: calc(100vh - 60px)` (full viewport minus header)
- Map is the primary content of the page — no scroll needed
- `touch-action: none` on the map container prevents page scroll interference
- Leaflet handles touch pan/zoom natively
---
### 2.6 — Navigation Link
**What:** "Map" link added to the site header navigation.
**Where:** `partials/base.html.twig` nav section — add `<a href="{{ base_url_absolute }}/map">Map</a>`
---
## Out of Scope (Milestone 2)
- Filtering markers by date range
- Clustering markers at low zoom levels
- Heatmap or density visualization
- Showing the route on the tracker feed page (Milestone 4)
- Showing elevation profile
- Country highlight/fill on the map
- Offline map tiles
---
## Acceptance Criteria
1. `/map` page exists and returns HTTP 200
2. Page renders a full-height interactive map
3. All published entries with valid lat/lng appear as markers
4. Markers are connected by a route line in date order
5. Clicking/tapping a marker shows a popup with date, title, and link
6. Popup link navigates to the correct entry page
7. Most recent entry marker is visually distinct (larger/brighter)
8. If no entries have GPS: map renders at world zoom with "No locations yet" message
9. Map is pannable and zoomable by touch on mobile
10. "Map" link appears in site navigation and routes to `/map`
11. Map auto-fits to show all markers on page load
12. Entries without lat/lng are silently excluded (no JS errors)
---
## Design Notes
- Map tile layer: OpenStreetMap default tiles. Clean, recognizable, free.
- Keep the Grav site header visible above the map — don't go full-screen (users need the nav)
- Popup design: minimal. White background, slight box-shadow, 8px border-radius
- Do not use any Leaflet plugins beyond the core library — keep the dependency footprint tiny
- The map page should load fast: Leaflet is ~42KB gzipped. Tile images load progressively. No blocking.
-182
View File
@@ -1,182 +0,0 @@
# Milestone 3 Spec — Statistics Page
**Goal:** A `/stats` page showing key trip numbers: days on the road, entries posted, countries visited, and approximate distance traveled.
---
## User Stories
- As a reader, I want to see a quick summary of how far Mischa has traveled and how many countries they've visited, without having to read every entry.
- As a traveler (Mischa), I want to see my own trip stats at a glance — a satisfying progress indicator while traveling.
- As a reader, I want stats that update automatically as new entries are posted — no manual maintenance.
---
## Feature Details
### 3.1 — Stats Page
**Route:** `/stats`
**Template:** `stats.html.twig` — extends `partials/base.html.twig`
**Page file:** `user/pages/04.stats/stats.md`
**Computed in Twig** (server-side, from published entries under `/tracker`):
---
### 3.2 — Stat: Days on the Road
**Definition:** Number of calendar days from the date of the first published entry to today.
**Formula (Twig):**
```twig
{% set first_entry = entries|first %}
{% set days = (now.timestamp - first_entry.date|date('U'))|round / 86400 %}
{% set days_on_road = [days|round(0, 'floor'), 0]|max %}
```
**Display:** `42 days on the road`
**Edge cases:**
- No entries: show `0 days on the road` or `Trip not started yet`
- Only one entry (today): show `1 day on the road`
---
### 3.3 — Stat: Entries Posted
**Definition:** Count of all published entries under `/tracker`.
**Display:** `17 entries posted`
**Edge cases:**
- 0 entries: `0 entries posted`
- 1 entry: `1 entry posted` (singular)
---
### 3.4 — Stat: Countries Visited
**Definition:** Unique values of `location_country` across all published entries, non-empty.
**Display:** Count + list
```
6 countries visited
Japan · South Korea · Mongolia · Russia · Finland · Estonia
```
**Edge cases:**
- No entries have `location_country`: show `Countries: —`
- Some entries missing `location_country`: count only those that have it; note "(based on X of Y entries)"
- Duplicate country names are de-duplicated (case-insensitive)
---
### 3.5 — Stat: Approximate Distance Traveled
**Definition:** Sum of great-circle (haversine) distances between consecutive entries that have valid lat/lng, in ascending date order.
**Implementation:** Computed in Twig using a haversine formula macro.
**Haversine in Twig:**
```twig
{% macro haversine(lat1, lng1, lat2, lng2) %}
{% set R = 6371 %}
{% set dLat = ((lat2 - lat1) * 3.14159265 / 180) %}
{% set dLng = ((lng2 - lng1) * 3.14159265 / 180) %}
{% set a = (dLat/2)|sin * (dLat/2)|sin + (lat1 * 3.14159265 / 180)|cos * (lat2 * 3.14159265 / 180)|cos * (dLng/2)|sin * (dLng/2)|sin %}
{% set c = 2 * a|sqrt|asin %}
{{ (R * c)|round }}
{% endmacro %}
```
Note: Twig does not have `sin`/`cos`/`asin`/`sqrt` built-in. Use a JavaScript-side calculation instead:
**Implementation:** Embed the entry GPS data as JSON in the template (same pattern as Milestone 2), compute distance in vanilla JS, and write the result into the DOM on page load.
```js
function haversine(lat1, lng1, lat2, lng2) {
var R = 6371;
var dLat = (lat2 - lat1) * Math.PI / 180;
var dLng = (lng2 - lng1) * Math.PI / 180;
var a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) * Math.sin(dLng/2)**2;
return R * 2 * Math.asin(Math.sqrt(a));
}
var total = 0;
for (var i = 1; i < GPS_POINTS.length; i++) {
total += haversine(GPS_POINTS[i-1][0], GPS_POINTS[i-1][1], GPS_POINTS[i][0], GPS_POINTS[i][1]);
}
document.getElementById('stat-distance').textContent = Math.round(total).toLocaleString() + ' km';
```
**Display:** `~3,400 km traveled`
**Edge cases:**
- 0 or 1 GPS points: `Distance: —`
- Very large numbers (trans-continental trip): use thousands separator: `12,400 km`
- Disclaimer note: "approximate — based on straight lines between entry locations"
---
### 3.6 — Visual Layout
**Layout:** 4 large stat blocks in a 2×2 grid on desktop, stacked on mobile.
Each block:
```
┌─────────────────┐
│ 42 │
│ days on road │
└─────────────────┘
```
- Number: large (3rem), bold, brand blue
- Label: small (0.85rem), muted grey
- Background: white, 1px border, 8px radius, subtle shadow
- Mobile: 2-col grid (2 stats per row)
Below the grid: list of countries visited (plain text, centered, muted).
---
### 3.7 — Navigation Link
Add "Stats" to the site navigation in `partials/base.html.twig`.
---
## Out of Scope (Milestone 3)
- Charts or graphs (bar charts, line graphs, etc.)
- World map with highlighted countries (that's a visual enhancement, deferred)
- Per-country breakdown (km in each country, days in each country)
- Speed statistics (km/day average)
- Elevation statistics
- Historical comparison (vs. last trip)
---
## Acceptance Criteria
1. `/stats` page exists and returns HTTP 200
2. "Days on the road" shows correct count from first entry date to today
3. "Entries posted" shows count of published entries
4. "Countries visited" shows correct count + list of unique non-empty `location_country` values
5. "Distance traveled" shows km sum of haversine distances between consecutive GPS entries
6. All four stats display in a 2×2 grid on desktop
7. On mobile (375px), stats stack into a 2-column responsive grid
8. Stats auto-update when new entries are published (no manual maintenance)
9. If no entries: all stats show 0 or `—`, no JS errors
10. "Stats" link in navigation routes to `/stats`
---
## Design Notes
- Stats should feel like a dashboard, not a table — big numbers, small labels
- Do not use any external charting library for v1
- Countries list below the grid: inline, separated by `·`, muted grey
- The "approximate" disclaimer for distance should be in small print below the distance stat
-91
View File
@@ -1,91 +0,0 @@
# Milestone 4 Spec — Mini-Map on Tracker Feed
**Goal:** Embed a compact interactive map above the entry feed on the tracker page, showing recent entry positions and the current location, giving readers immediate spatial context.
---
## User Stories
- As a reader landing on the tracker feed, I want to immediately see where Mischa currently is without having to navigate to the full map page.
- As a reader, I want to click a marker on the mini-map and jump to that entry.
- As a traveler (Mischa), I want the feed page to feel like a live travel dashboard, not just a blog list.
---
## Feature Details
### 4.1 — Mini-Map Placement
**Where:** At the top of `tracker.html.twig`, before the entry card list.
**Height:** 240px on mobile, 320px on desktop.
**Width:** Full width of content column (max 680px).
**Tile layer:** Same OpenStreetMap tiles as Milestone 2.
**No duplicate Leaflet load:** Leaflet is already loaded on the map page; on the tracker page, load it only if needed. Check with `if (typeof L === 'undefined')` before initializing. (In practice, the CSS and JS are loaded unconditionally from the same CDN — caching handles it.)
---
### 4.2 — What's Shown
- **All entries with GPS** shown as small markers (not just recent 10 — the map auto-fits to bounds)
- **Route line** connecting them in chronological order (same style as Milestone 2)
- **Most recent marker** highlighted (larger, brighter)
- **No popups by default** — tapping a marker links directly to the entry (no popup intermediary for the mini-map, keeps it fast)
- Map auto-fits bounds to all markers; if only 1 marker, zoom to 10
---
### 4.3 — Interaction
- Tap/click marker → navigate to entry URL directly
- Map is pannable and zoomable (same touch handling as M2)
- "View full map →" link below the mini-map → navigates to `/map`
---
### 4.4 — Entry Data
Same JSON serialization as Milestone 2 (embed `TRACKER_ENTRIES` in the Twig template). This can reuse the same data variable name if both map and tracker pages use the same template pattern.
---
### 4.5 — Empty State
If no entries have GPS coordinates:
- Mini-map hidden entirely (don't show an empty world map on the feed page)
- Entry list still shows normally
---
## Out of Scope (Milestone 4)
- Clustering markers at low zoom
- Filtering by date
- Satellite/terrain tile layers
- Search on the mini-map
---
## Acceptance Criteria
1. Mini-map appears above entry cards on the tracker feed page
2. All entries with valid lat/lng appear as markers on the mini-map
3. Route line connects markers in date order
4. Most recent marker is visually distinct
5. Clicking/tapping a marker navigates directly to that entry
6. "View full map →" link appears below the mini-map and routes to `/map`
7. If no entries have GPS, mini-map is hidden and entry list shows normally
8. Mini-map is pannable and zoomable by touch on mobile
9. Mini-map does not block page scrolling on mobile (map is fixed height, not full-screen)
---
## Design Notes
- Mini-map border-radius should match the card design (8px)
- Light 1px border or subtle shadow to separate from content
- "View full map →" in small muted text, right-aligned
- Keep the mini-map lightweight: same Leaflet instance, no additional plugins
-161
View File
@@ -1,161 +0,0 @@
# PM Analysis — What to Build (and What to Skip)
*Role: Senior Product Manager. Audience: one solo traveler (Mischa), platform: Grav CMS flat-file PHP, no native app.*
---
## Starting position
Polarsteps and FindPenguins are native mobile apps built around:
1. Background GPS tracking (requires OS-level access)
2. Social networks (followers, discovery, comments)
3. App-side video/reel processing
**None of these three pillars are reproducible in a web CMS.** Any plan that tries to replicate them wholesale is delusional. What we can do is cherry-pick the *outputs* — the things those apps display to readers — and build them into the blog in ways that add real value to both Mischa (the poster) and readers (friends/family following along).
---
## Feature-by-Feature Audit
| Feature | Makes sense solo? | Buildable in Grav+JS? | Value to readers? | Worth the cost? | Decision |
|---|---|---|---|---|---|
| Auto background GPS tracking | No — posting manually anyway | No — requires native app | — | — | **SKIP** |
| Interactive map of visited locations | Yes | Yes — Leaflet.js + frontmatter lat/lng | High | High | **BUILD** |
| Route line on map between entries | Yes | Yes — connect entry coords in order | High | Medium | **BUILD** |
| Entry location name (city, country) | Yes | Yes — manual input on form | High | Low | **BUILD** |
| Weather metadata per entry | Yes | Yes — Open-Meteo free API, no key needed | Medium | Medium | **BUILD** |
| Photo gallery per entry | Yes | Yes — shortcode-gallery-plusplus installed | High | Low | **BUILD** (already partial) |
| Hero image on feed cards | Yes | Yes — already in frontmatter | High | Low | **BUILD** |
| Trip statistics page | Yes | Yes — compute from frontmatter | Medium | Low | **BUILD** |
| Countries visited world map | Yes | Yes — highlight SVG or Leaflet layers | Medium | Medium | **BUILD** |
| Follower system | No — solo blog | Would need auth + DB | None | — | **SKIP** |
| Comments on entries | No — spam risk, no community | Would need plugin + moderation | Minimal | — | **SKIP** |
| Social discovery / explore | No — not a platform | Would need indexing infrastructure | None | — | **SKIP** |
| Group trip / travel buddies | No — solo trip | — | — | — | **SKIP** |
| Reactions / likes | No | — | — | — | **SKIP** |
| 3D flyover video | No — proprietary pipeline | No | Nice | — | **SKIP** |
| Trip reels / short video | No — app-side processing | No | Nice | — | **SKIP** |
| Travel book / print | No — out of scope | No | — | — | **SKIP** |
| AI itinerary builder | No — trip already started | No | — | — | **SKIP** |
| Flight detection | No — requires native app sensors | No | — | — | **SKIP** |
| Delayed sharing / live location | No — blog posts after the fact | Irrelevant | — | — | **SKIP** |
| Offline posting | Already works | Already works (Grav form offline) | — | — | **ALREADY EXISTS** |
| Scheduled / draft posts | Already exists | Already exists (publish_date) | — | — | **ALREADY EXISTS** |
| Step suggestions / nudges | No — push notifications not possible | No | — | — | **SKIP** |
| Eebook / export | No — out of scope | Possible but niche | — | — | **SKIP** |
---
## What to Build — Summary
### Keep (already exists, just needs to work reliably)
- Login-gated mobile posting form ✓
- Draft and scheduled publishing ✓
### Build
**1. Entry enrichment** — make each entry richer with zero extra effort from Mischa:
- Location name (city, country) captured at post time
- Weather auto-fetched via Open-Meteo at post time using lat/lng
- Photos displayed in a proper gallery (lightbox)
- Hero image shown on feed card
**2. Interactive map** — the single most "Polarsteps-like" thing that's genuinely achievable:
- `/map` page with Leaflet.js
- Marker per entry (lat/lng from frontmatter)
- Route line connecting entries in date order
- Popup with title, date, thumbnail, link to entry
- Mobile-friendly (touch pan/zoom)
**3. Trip statistics** — a simple stats page:
- Days on the road (count of entries with distinct dates)
- Entries posted
- Countries/regions visited (derived from location name field)
- Approx distance traveled (sum of haversine distances between GPS points)
---
## What to Skip — with reasons
| Feature | Reason skipped |
|---|---|
| Background GPS tracking | Requires native app. Grav runs on a server. |
| Social features (followers, comments, likes) | Adds spam risk, moderation burden, zero value for a solo travel blog with a personal audience. A "share link" is enough. |
| Video reels | App-side video processing pipeline, not available in a web CMS. |
| 3D flyover | Proprietary rendering. Not worth building from scratch. |
| Travel book printing | Out of scope. Mischa can use Polarsteps or FindPenguins for this if desired. |
| AI itinerary builder | Trip is already in progress. Out of scope. |
| Discovery / explore | Not a platform. No community. |
| Group trips | Solo traveler. |
| Flight detection | Requires native OS sensor access. |
| Delayed sharing | Moot — we don't broadcast real-time location at all. |
---
## Milestone Plan
### Milestone 1 — Entry Enrichment (23 days)
**Goal:** Every entry is richer out of the box — photo gallery works, location name shown, weather captured, hero image on feed.
Features:
- Location name field (city + country) added to post form and displayed on entries/cards
- Weather auto-fetch on post form (JS call to Open-Meteo using entered lat/lng, fills hidden fields)
- Weather displayed on entry page
- Photo gallery working (shortcode-gallery-plusplus or native media display)
- Hero image shown on tracker feed cards
**Value:** Immediate. Makes each entry feel like a real travel log entry, not just a text post.
---
### Milestone 2 — Interactive Map (23 days)
**Goal:** A `/map` page shows all entries as markers on an interactive Leaflet.js map, connected by a route line, with popups.
Features:
- New `map` page and template
- Leaflet.js loaded from CDN (no build step)
- Entries serialized to JSON in the template (lat/lng, title, date, url, hero_image)
- Route polyline in chronological order
- Marker popup: date, title, thumbnail, "Read entry →" link
- Map added to site navigation
**Value:** High for readers — gives a bird's-eye view of the trip. The single most compelling "where is Mischa?" feature.
---
### Milestone 3 — Statistics Page (12 days)
**Goal:** A `/stats` page with key trip numbers.
Features:
- Days on the road (first entry date to today)
- Total entries posted
- Unique countries visited (derived from location names)
- Approximate distance traveled (haversine between consecutive entry GPS points)
- Simple, scannable layout — no charts needed for v1
**Value:** Medium — nice context for readers, satisfying for Mischa to see progress.
---
### Milestone 4 — Map on Tracker Feed (1 day)
**Goal:** A mini-map showing recent positions above or alongside the feed, so the first thing readers see is "where is Mischa now?"
Features:
- Small embedded Leaflet map on the tracker/feed page
- Shows last 10 entries as markers, with the most recent highlighted
- Route line between them
- Tapping a marker opens the entry
**Value:** Medium — gives context to the feed without navigating away. Nice "current location" feel.
---
## Milestone Priority Order
**M1 first** — entry quality affects every post Mischa makes from day 1 of the trip. Get this right immediately.
**M2 second** — the map is the headline feature that makes this feel like a Polarsteps-style blog. Technically independent from M1 (uses lat/lng already in frontmatter).
**M3 third** — stats are a nice-to-have. Easy to add once M1 and M2 are stable.
**M4 fourth** — the mini-map on the feed is polish. Only worth doing once the full map (M2) is solid.

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