Compare commits

...
6 Commits
Author SHA1 Message Date
m038andClaude Opus 4.8 d7a772ae02 feat(header): mobile hamburger menu + alignment & typography pass
- Header: offset the 4px green ::before bar so content centres in the
  visible dark area, not the geometric box (fixes wordmark/nav/icon
  sitting ~2px high — the root cause behind the whole alignment saga)
- Mobile (≤768px): collapse nav into a slide-down hamburger panel
  (standalone js/nav.js); inline nav retained on desktop
- Nav: use the display serif in sentence case so it shares the
  wordmark's anatomy and aligns naturally (no optical nudge needed)
- Typography tokens: three-tier display tracking (--tracking-display,
  --tracking-display-sm), --tracking-caps, --color-error, paper-glass
  overlays, --text-story/--leading-story; applied across components
- trip-dates: move inline styles to a class

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BftDn9vu9SonFAY4vxu4uk
2026-06-27 18:54:56 +02:00
m038andClaude Sonnet 4.6 ac38ae2a54 fix(home): wrap map init in DOMContentLoaded to fix broken homepage map
After the CDN elimination refactor, maplibregl moved from an inline head
script to the bottom-group bundle — but home.html.twig still called
new maplibregl.Map() immediately, before map.js had loaded. Wraps both
the active-trip and between-trips script blocks in DOMContentLoaded,
matching the pattern already used in trip/map/feed-map templates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 18:34:17 +02:00
m038 9fa1988211 chore: remove 39 ui-test entries from italy-2026-demo dailies 2026-06-25 07:29:14 +02:00
m038 f6ee4944f9 feat: complete CDN elimination + SVG icon build pipeline
CDN cleanup:
- entry.html.twig: remove PhotoSwipe CDN + duplicate JS (main.js handles it)
- dailies.html.twig: remove PhotoSwipe CDN + duplicate JS
- home.html.twig: remove PhotoSwipe CDN + MapLibre CDN + maplibre-utils.js
  script tags; add {% block map_assets %} to register map bundle

SVG build pipeline:
- Add lucide-static devDependency; gen-weather-icons.js reads icon SVGs
  from node_modules and emits templates/partials/weather-icons.html.twig
- package.json build script now runs gen-weather-icons.js before esbuild
- entry-journal.html.twig includes generated partial instead of hardcoded map
- Rebuild: main.js 83.7kb, map.js 860.7kb, all fonts; zero CDN requests remain
2026-06-25 00:13:17 +02:00
m038 b017d2ff64 fix: weather icon case mismatch + pin/weather icon alignment
- Lowercase all weather_icons map keys to match stored entry values
  ('sunny', 'partly cloudy', etc.); add |lower filter as safety net
- Remove vertical-align hack from location pin SVG
- Add display:inline-flex + align-items:center to .journal-post-location
  and .journal-post-weather so icons and text share a true center axis
2026-06-24 23:59:11 +02:00
m038 3345c076a9 style: replace weather emoji with Lucide SVG glyph icons
8 conditions mapped to stroke-based 14×14 SVGs (sun, cloud-sun, cloud,
cloud-fog, cloud-drizzle, cloud-rain, cloud-snow, cloud-lightning).
Icons use currentColor and aria-hidden; text label retained alongside.
2026-06-24 23:52:40 +02:00
15 changed files with 410 additions and 433 deletions
+212 -154
View File
@@ -39,21 +39,34 @@ body::after {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--space-5);
box-sizing: border-box;
/* 4px top offsets the green ::before gradient (top 4px of the bar) so all
content centres in the VISIBLE dark area, not the geometric header box. */
padding: 4px var(--space-5) 0;
height: var(--site-header-height);
background: var(--color-canvas);
border-top: 3px solid var(--color-accent);
border-bottom: 1px solid var(--color-border);
position: sticky;
top: 0;
z-index: 100;
}
.site-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--color-accent), var(--color-accent-hover));
pointer-events: none;
}
.site-title {
font-family: var(--font-display);
font-size: var(--text-lg);
font-size: var(--text-xl);
font-weight: 400;
letter-spacing: -0.01em;
letter-spacing: var(--tracking-display-sm);
text-decoration: none;
color: var(--color-ink);
line-height: 1;
@@ -66,9 +79,12 @@ body::after {
}
.site-nav a {
font-family: var(--font-ui);
font-size: var(--text-sm);
font-weight: 500;
/* Nav uses the display serif (not the UI sans) so it shares the wordmark's
anatomy: with one typeface, logo and nav centre identically — no optical
nudge needed. Sentence case keeps it quiet support beneath the wordmark. */
font-family: var(--font-display);
font-size: var(--text-base);
font-weight: 400;
color: var(--color-ink-2);
text-decoration: none;
padding: var(--space-2) var(--space-3);
@@ -80,11 +96,69 @@ body::after {
}
.site-nav a:hover { color: var(--color-ink); background: var(--color-paper); }
.site-nav a[aria-current="page"] { color: var(--color-accent); font-weight: 600; }
.site-nav a[aria-current="page"] { color: var(--color-accent); }
@media (max-width: 380px) {
.site-title { font-size: var(--text-md); }
.site-nav a { padding: var(--space-2); font-size: 0.8rem; }
/* ── Hamburger toggle ────────────────────────────────────────────────────────── */
.nav-toggle {
display: none; /* shown only on mobile, via the media query below */
flex-direction: column;
justify-content: center;
gap: 5px;
width: 44px;
height: 44px;
padding: 11px 10px;
margin-right: -10px; /* cancel inner padding so the bars align to the bar edge */
background: none;
border: 0;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.nav-toggle__bar {
display: block;
width: 100%;
height: 2px;
border-radius: 2px;
background: var(--color-ink-2);
transition: transform 0.25s ease, opacity 0.2s ease, background 0.15s;
}
.nav-toggle:hover .nav-toggle__bar { background: var(--color-ink); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bar:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
@media (max-width: 768px) {
.nav-toggle { display: flex; }
/* Nav collapses into a slide-down panel beneath the header bar. */
.site-nav {
position: absolute;
top: 100%;
left: 0;
right: 0;
flex-direction: column;
align-items: stretch;
gap: 0;
padding: var(--space-2) var(--space-5) var(--space-4);
background: var(--color-canvas);
border-bottom: 1px solid var(--color-border);
box-shadow: var(--shadow-lg);
transform: translateY(-8px);
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s;
}
.site-nav[data-open="true"] {
transform: translateY(0);
opacity: 1;
visibility: visible;
}
.site-nav a {
font-size: var(--text-lg);
padding: var(--space-3) 0;
min-height: 0;
border-radius: 0;
}
.site-nav a:hover { background: none; color: var(--color-ink); }
}
/* ── Feed ────────────────────────────────────────────────────────────────────── */
@@ -132,6 +206,7 @@ body::after {
font-size: var(--text-xl);
font-weight: 400;
line-height: var(--leading-snug);
letter-spacing: var(--tracking-display);
color: var(--color-ink);
margin-bottom: var(--space-3);
transition: color 0.15s;
@@ -140,21 +215,6 @@ body::after {
.entry-card:hover .entry-title { color: var(--color-accent); }
/* Location & weather badges (single entry page) */
.entry-location {
font-size: var(--text-sm);
color: var(--color-ink-2);
display: inline-flex;
align-items: center;
gap: var(--space-1);
}
.entry-weather {
font-size: var(--text-sm);
color: var(--color-ink-2);
}
@keyframes card-highlight {
0% { background-color: color-mix(in srgb, var(--color-accent) 12%, transparent); }
100% { background-color: transparent; }
@@ -178,6 +238,7 @@ body::after {
font-size: var(--text-xl);
font-weight: 400;
line-height: var(--leading-snug);
letter-spacing: var(--tracking-display);
color: var(--color-ink);
margin-bottom: var(--space-2);
}
@@ -203,6 +264,9 @@ body::after {
.journal-post-location,
.journal-post-weather {
color: var(--color-ink-muted);
display: inline-flex;
align-items: center;
gap: var(--space-1);
}
.journal-photo-wrap {
@@ -346,21 +410,6 @@ body::after {
/* ── Single entry ────────────────────────────────────────────────────────────── */
.entry-hero {
width: 100%;
max-height: 480px;
overflow: hidden;
margin-bottom: var(--space-8);
border-radius: var(--radius-md);
}
.entry-hero img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.entry-back-fixed {
position: fixed;
top: calc(var(--site-header-height) + var(--space-3));
@@ -368,91 +417,10 @@ body::after {
z-index: 100;
}
.entry-header { margin-bottom: var(--space-8); }
.entry-header-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-4);
}
.entry-header .entry-date {
font-size: var(--text-sm);
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--color-ink-muted);
display: inline;
margin-bottom: 0;
}
.entry .entry-title {
font-family: var(--font-display);
font-size: var(--text-2xl);
font-weight: 400;
line-height: var(--leading-snug);
color: var(--color-ink);
margin-bottom: var(--space-4);
}
@media (min-width: 520px) {
.entry .entry-title { font-size: var(--text-3xl); }
}
.entry-title-rule {
height: 1px;
background: var(--color-border);
margin-bottom: var(--space-8);
}
.entry-body { margin-bottom: var(--space-10); }
.entry-body p { margin-bottom: 1.4em; font-size: var(--text-md); line-height: var(--leading-normal); color: var(--color-ink-2); }
.entry-body img { max-width: 100%; height: auto; border-radius: var(--radius-sm); }
.entry-footer { border-top: 1px solid var(--color-border); padding-top: var(--space-5); }
.entry-footer a { color: var(--color-accent); text-decoration: none; font-size: var(--text-sm); font-weight: 500; }
.entry-footer a:hover { color: var(--color-accent-hover); }
/* ── Photo gallery ───────────────────────────────────────────────────────────── */
.entry-gallery {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 3px;
margin-bottom: var(--space-10);
border-radius: var(--radius-md);
overflow: hidden;
}
@media (min-width: 520px) {
.entry-gallery { grid-template-columns: repeat(3, 1fr); }
}
.gallery-thumb {
background: none;
border: none;
padding: 0;
cursor: pointer;
display: block;
aspect-ratio: 1;
overflow: hidden;
}
.gallery-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: opacity 0.15s;
}
.gallery-thumb:hover img,
.gallery-thumb:focus img { opacity: 0.82; }
.gallery-thumb:focus { outline: 2px solid var(--color-accent); outline-offset: 2px; }
/* ── PhotoSwipe overrides ─────────────────────────────────────────────────────── */
.pswp__bg { background: #000; }
@@ -577,6 +545,8 @@ body::after {
font-family: var(--font-display);
font-size: var(--text-2xl);
font-weight: 400;
line-height: var(--leading-tight);
letter-spacing: var(--tracking-display);
margin-bottom: var(--space-8);
color: var(--color-ink);
}
@@ -593,12 +563,9 @@ body::after {
}
.stat-block {
background: var(--color-canvas);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-6) var(--space-5);
text-align: center;
box-shadow: var(--shadow-sm);
border-left: 2px solid var(--color-accent);
padding: var(--space-2) 0 var(--space-2) var(--space-4);
text-align: left;
}
.stat-value {
@@ -606,7 +573,7 @@ body::after {
font-family: var(--font-display);
font-size: clamp(2rem, 6vw, var(--text-3xl));
font-weight: 400;
color: var(--color-accent);
color: var(--color-ink);
line-height: 1.1;
margin-bottom: var(--space-2);
font-variant-numeric: tabular-nums;
@@ -618,7 +585,7 @@ body::after {
font-weight: 600;
color: var(--color-ink-muted);
text-transform: uppercase;
letter-spacing: 0.07em;
letter-spacing: var(--tracking-caps);
}
.stats-countries {
@@ -635,7 +602,7 @@ body::after {
color: var(--color-ink);
text-transform: uppercase;
font-size: var(--text-xs);
letter-spacing: 0.07em;
letter-spacing: var(--tracking-caps);
}
.stats-note {
@@ -763,6 +730,8 @@ body::after {
font-family: var(--font-display);
font-size: var(--text-xl);
font-weight: 400;
line-height: var(--leading-snug);
letter-spacing: var(--tracking-display);
margin-bottom: var(--space-6);
color: var(--color-ink);
}
@@ -832,13 +801,13 @@ body::after {
/* Inline field validation */
.post-form-wrap .field-invalid {
border-color: #c0392b !important;
outline-color: #c0392b !important;
border-color: var(--color-error) !important;
outline-color: var(--color-error) !important;
}
.post-form-wrap .field-error {
display: block;
color: #c0392b;
color: var(--color-error);
font-size: var(--text-sm);
margin-top: var(--space-1);
}
@@ -876,7 +845,7 @@ body::after {
}
.form-status--ok { color: var(--color-accent); }
.form-status--err { color: #B44A2A; }
.form-status--err { color: var(--color-error); }
/* ── Accessibility & motion ──────────────────────────────────────────────────── */
@@ -964,10 +933,18 @@ body::after {
font-family: var(--font-display);
font-size: var(--text-2xl);
font-weight: 400;
line-height: var(--leading-tight);
letter-spacing: var(--tracking-display);
color: var(--color-ink);
margin-bottom: var(--space-2);
}
.trip-dates {
font-size: var(--text-sm);
color: var(--color-ink-muted);
margin: var(--space-1) 0 var(--space-2);
}
.home-trip-counts {
font-size: var(--text-sm);
color: var(--color-ink-muted);
@@ -1004,7 +981,7 @@ body::after {
color: var(--color-ink-muted);
background: transparent;
border: 1px solid var(--color-border);
border-radius: var(--radius-full);
border-radius: var(--radius-sm);
padding: var(--space-1) var(--space-4);
cursor: pointer;
transition: color 0.15s, border-color 0.15s, background 0.15s;
@@ -1076,6 +1053,8 @@ body::after {
font-family: var(--font-display);
font-size: var(--text-2xl);
font-weight: 400;
line-height: var(--leading-tight);
letter-spacing: var(--tracking-display);
color: var(--color-ink);
margin-bottom: var(--space-8);
}
@@ -1107,10 +1086,30 @@ body::after {
background: var(--color-surface-raised);
}
.trip-card-cover {
aspect-ratio: 3 / 1;
overflow: hidden;
border-radius: var(--radius-md) var(--radius-md) 0 0;
background: var(--color-border);
margin: calc(-1 * var(--space-6)) calc(-1 * var(--space-6)) var(--space-5);
}
.trip-card-cover img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.45s ease;
}
.trip-card:hover .trip-card-cover img { transform: scale(1.04); }
.trip-card-title {
font-family: var(--font-display);
font-size: var(--text-xl);
font-weight: 400;
line-height: var(--leading-snug);
letter-spacing: var(--tracking-display);
color: var(--color-ink);
margin-bottom: var(--space-2);
}
@@ -1152,7 +1151,7 @@ body::after {
font-size: var(--text-xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
letter-spacing: var(--tracking-caps);
color: var(--color-ink-muted);
margin-bottom: var(--space-3);
}
@@ -1202,7 +1201,7 @@ body::after {
font-size: var(--text-xs);
font-weight: 600;
font-variant: small-caps;
letter-spacing: 0.08em;
letter-spacing: var(--tracking-caps);
color: var(--color-accent);
margin-bottom: var(--space-2);
}
@@ -1299,6 +1298,8 @@ body::after {
.trip-cycling-title {
font-family: var(--font-display);
font-size: var(--text-lg);
line-height: var(--leading-snug);
letter-spacing: var(--tracking-display-sm);
color: var(--color-ink);
}
@@ -1328,7 +1329,7 @@ body::after {
font-size: var(--text-sm);
font-weight: 500;
color: rgba(255,255,255,0.85);
background: rgba(26,24,20,0.55);
background: var(--color-paper-glass-high);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
border: 1px solid rgba(255,255,255,0.12);
@@ -1337,7 +1338,7 @@ body::after {
text-decoration: none;
transition: background 0.2s;
}
.story-escape:hover { background: rgba(26,24,20,0.8); }
.story-escape:hover { background: var(--color-paper-glass-hover); }
/* Hero */
.story-hero { position: relative; }
@@ -1386,7 +1387,7 @@ body::after {
.story-hero__title {
font-family: var(--font-display);
font-size: clamp(2.2rem, 6vw, 4.5rem);
font-weight: 900;
font-weight: 400;
line-height: 1.08;
letter-spacing: -0.02em;
margin-bottom: var(--space-3);
@@ -1437,13 +1438,27 @@ body::after {
}
.story-body p {
font-family: var(--font-ui);
font-size: 1.0625rem;
line-height: 1.85;
font-size: var(--text-story);
line-height: var(--leading-story);
color: var(--color-ink-2);
margin-bottom: var(--space-6);
}
.story-body h2, .story-body h3 {
.story-body h2 {
font-family: var(--font-display);
font-size: var(--text-xl);
font-weight: 400;
line-height: var(--leading-snug);
letter-spacing: var(--tracking-display);
color: var(--color-ink);
margin-top: var(--space-10);
margin-bottom: var(--space-4);
}
.story-body h3 {
font-family: var(--font-display);
font-size: var(--text-lg);
font-weight: 400;
line-height: var(--leading-snug);
letter-spacing: var(--tracking-display-sm);
color: var(--color-ink);
margin-top: var(--space-10);
margin-bottom: var(--space-4);
@@ -1460,6 +1475,41 @@ body::after {
text-decoration: none;
}
/* ── Story progress bar ──────────────────────────────────── */
.story-progress {
position: fixed;
top: var(--site-header-height);
left: 0;
height: 2px;
width: 0%;
background: var(--color-accent);
z-index: 200;
pointer-events: none;
will-change: width;
}
/* ── Story opener (location + date eyebrow) ──────────────── */
.story-opener {
text-align: center;
padding-bottom: var(--space-12);
margin-bottom: var(--space-12);
border-bottom: 1px solid var(--color-border);
opacity: 0;
animation: storyReveal 0.9s cubic-bezier(.16,1,.3,1) 0.8s both;
}
.story-opener__text {
font-family: var(--font-ui);
font-size: var(--text-sm);
color: var(--color-ink-muted);
letter-spacing: var(--tracking-caps);
text-transform: uppercase;
}
@media (prefers-reduced-motion: reduce) {
.story-opener { opacity: 1; animation: none; }
}
/* ── Story nav title (desktop: fades into header center) ─────── */
.story-nav-title {
position: absolute;
@@ -1469,7 +1519,7 @@ body::after {
font-family: var(--font-display);
font-size: var(--text-lg);
font-weight: 400;
letter-spacing: -0.01em;
letter-spacing: var(--tracking-display-sm);
color: var(--color-ink);
white-space: nowrap;
max-width: 40%;
@@ -1553,7 +1603,7 @@ body::after {
align-items: center;
gap: var(--space-2);
padding: var(--space-10) var(--space-12);
background: rgba(26,24,20,0.25);
background: var(--color-paper-glass-mid);
backdrop-filter: blur(18px) saturate(1.4);
-webkit-backdrop-filter: blur(18px) saturate(1.4);
border: 1px solid rgba(255,255,255,0.12);
@@ -1581,8 +1631,9 @@ body::after {
.chapter-break__title {
font-family: var(--font-display);
font-size: clamp(1.6rem, 4vw, 2.6rem);
font-weight: 700;
font-weight: 400;
line-height: 1.15;
letter-spacing: var(--tracking-display);
color: #fff;
text-shadow: 0 2px 16px rgba(0,0,0,0.4);
margin: 0;
@@ -1671,8 +1722,8 @@ body::after {
}
.scrolly-step__inner p {
font-family: var(--font-ui);
font-size: 1.05rem;
line-height: 1.8;
font-size: var(--text-story);
line-height: var(--leading-story);
color: var(--color-ink-2);
margin-bottom: var(--space-3);
}
@@ -1719,7 +1770,7 @@ body::after {
padding: var(--space-10) var(--space-8);
backdrop-filter: blur(14px) saturate(1.3);
-webkit-backdrop-filter: blur(14px) saturate(1.3);
background: rgba(26,24,20,0.08);
background: var(--color-paper-glass-low);
text-align: center;
}
.pull-quote__inner--no-image {
@@ -1740,6 +1791,7 @@ body::after {
font-family: var(--font-display);
font-size: clamp(1.2rem, 3vw, 1.6rem);
font-style: italic;
letter-spacing: var(--tracking-display-sm);
color: #fff;
line-height: 1.5;
margin: var(--space-4) 0;
@@ -1891,6 +1943,8 @@ body::after {
font-family: var(--font-display);
font-size: var(--text-2xl);
font-weight: 400;
line-height: var(--leading-tight);
letter-spacing: var(--tracking-display);
color: var(--color-ink);
margin-bottom: 0;
}
@@ -1923,7 +1977,7 @@ body::after {
font-size: var(--text-xs);
color: var(--color-ink-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
letter-spacing: var(--tracking-caps);
margin-bottom: var(--space-1);
}
.story-card__location {
@@ -1937,9 +1991,10 @@ body::after {
font-family: var(--font-display);
font-size: var(--text-lg);
font-weight: 400;
line-height: var(--leading-snug);
letter-spacing: var(--tracking-display-sm);
color: var(--color-ink);
margin-bottom: var(--space-3);
line-height: var(--leading-snug);
}
.story-card__cta {
font-family: var(--font-ui);
@@ -1964,6 +2019,8 @@ body::after {
font-family: var(--font-display);
font-size: var(--text-2xl);
font-weight: 400;
line-height: var(--leading-tight);
letter-spacing: var(--tracking-display);
color: var(--color-ink);
margin-bottom: var(--space-2);
}
@@ -2021,7 +2078,7 @@ body::after {
font-size: var(--text-xs);
font-weight: 600;
font-variant: small-caps;
letter-spacing: 0.08em;
letter-spacing: var(--tracking-caps);
color: var(--color-accent);
}
@@ -2033,9 +2090,10 @@ body::after {
font-family: var(--font-display);
font-size: var(--text-lg);
font-weight: 400;
line-height: var(--leading-snug);
letter-spacing: var(--tracking-display-sm);
color: var(--color-ink);
text-decoration: none;
line-height: 1.3;
}
.home-highlight-title:hover { color: var(--color-accent); }
+22 -8
View File
@@ -13,25 +13,39 @@
--color-accent-on: #FFFFFF; /* text on accent surfaces */
--color-surface-raised: #2A2720; /* elevated surfaces: tooltips, hover */
--color-ink-inverse: #17171A; /* text on accent-coloured buttons */
--color-error: #c0392b; /* validation errors, form error status */
/* ── Glass overlays (paper colour at opacity, for story components) ── */
--color-paper-glass-low: color-mix(in srgb, var(--color-paper) 8%, transparent);
--color-paper-glass-mid: color-mix(in srgb, var(--color-paper) 25%, transparent);
--color-paper-glass-high: color-mix(in srgb, var(--color-paper) 55%, transparent);
--color-paper-glass-hover: color-mix(in srgb, var(--color-paper) 80%, transparent);
/* ── Fonts ───────────────────────────────────────────────── */
--font-display: 'DM Serif Display', Georgia, serif;
--font-ui: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
/* ── Type scale ──────────────────────────────────────────── */
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-md: 1.125rem;
--text-lg: 1.375rem;
--text-xl: 1.75rem;
--text-2xl: 2.25rem;
--text-3xl: 3rem;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-md: 1.125rem;
--text-lg: 1.375rem;
--text-xl: 1.75rem;
--text-2xl: 2.25rem;
--text-3xl: 3rem;
--text-story: 1.0625rem; /* long-form reading size: story body + scrolly steps */
/* ── Leading ─────────────────────────────────────────────── */
--leading-tight: 1.2;
--leading-snug: 1.35;
--leading-normal: 1.65;
--leading-story: 1.85; /* relaxed reading rhythm for story long-form */
/* ── Tracking ────────────────────────────────────────────── */
--tracking-caps: 0.07em; /* uppercase UI labels: dates, badges, sidebar headings */
--tracking-display: -0.015em; /* display headings: text-xl and text-2xl */
--tracking-display-sm: -0.01em; /* display headings: text-lg and smaller; site logotype */
/* ── Spacing (4px grid) ──────────────────────────────────── */
--space-1: 0.25rem;
+37
View File
@@ -0,0 +1,37 @@
/* Mobile nav: hamburger toggle for the slide-down menu panel.
Standalone (not part of the esbuild bundle) — a small progressive
enhancement for the header. Loaded via base.html.twig. */
(function () {
var toggle = document.querySelector('.nav-toggle');
var nav = document.getElementById('site-nav');
if (!toggle || !nav) return;
function setOpen(open) {
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
toggle.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
nav.setAttribute('data-open', open ? 'true' : 'false');
}
toggle.addEventListener('click', function (e) {
e.stopPropagation();
setOpen(toggle.getAttribute('aria-expanded') !== 'true');
});
/* Tapping a link closes the panel before navigating. */
nav.addEventListener('click', function (e) {
if (e.target.closest('a')) setOpen(false);
});
/* Close on outside tap or Escape. */
document.addEventListener('click', function (e) {
if (!nav.contains(e.target) && !toggle.contains(e.target)) setOpen(false);
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') setOpen(false);
});
/* Reset to closed if the viewport grows back to desktop. */
window.addEventListener('resize', function () {
if (window.innerWidth > 768) setOpen(false);
});
})();
+9 -1
View File
@@ -13,7 +13,8 @@
"scrollama": "^3"
},
"devDependencies": {
"esbuild": "^0.21"
"esbuild": "^0.21",
"lucide-static": "latest"
}
},
"node_modules/@esbuild/aix-ppc64": {
@@ -747,6 +748,13 @@
"node": ">=0.10.0"
}
},
"node_modules/lucide-static": {
"version": "1.21.0",
"resolved": "https://registry.npmjs.org/lucide-static/-/lucide-static-1.21.0.tgz",
"integrity": "sha512-6248z2/4sEyKkYAPPUYxOPiB2RCfMmLdMHuoOhsTFnoD40ixAoHmTVhOPux8ADa1NTBmzpEKF7WNePm+Ms503Q==",
"dev": true,
"license": "ISC"
},
"node_modules/maplibre-gl": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz",
+3 -2
View File
@@ -1,7 +1,7 @@
{
"private": true,
"scripts": {
"build": "esbuild js/src/main.js --bundle --minify --format=iife --outfile=js/main.js --loader:.woff2=file --loader:.woff=file --asset-names=../fonts/[name] && esbuild js/src/map.js --bundle --minify --format=iife --outfile=js/map.js && mkdir -p css-compiled fonts && { mv js/main.css css-compiled/main.css 2>/dev/null || true; } && { mv js/map.css css-compiled/map.css 2>/dev/null || true; }"
"build": "node scripts/gen-weather-icons.js && esbuild js/src/main.js --bundle --minify --format=iife --outfile=js/main.js --loader:.woff2=file --loader:.woff=file --asset-names=../fonts/[name] && esbuild js/src/map.js --bundle --minify --format=iife --outfile=js/map.js && mkdir -p css-compiled fonts && { mv js/main.css css-compiled/main.css 2>/dev/null || true; } && { mv js/map.css css-compiled/map.css 2>/dev/null || true; }"
},
"dependencies": {
"@fontsource-variable/dm-sans": "latest",
@@ -12,6 +12,7 @@
"scrollama": "^3"
},
"devDependencies": {
"esbuild": "^0.21"
"esbuild": "^0.21",
"lucide-static": "latest"
}
}
@@ -0,0 +1,47 @@
#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const iconMap = {
'sunny': 'sun',
'partly cloudy': 'cloud-sun',
'cloudy': 'cloud',
'foggy': 'cloud-fog',
'drizzle': 'cloud-drizzle',
'rain': 'cloud-rain',
'snow': 'cloud-snow',
'thunderstorm': 'cloud-lightning'
};
const wrapAttrs = 'width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"';
function buildSvg(iconName) {
const src = path.resolve(__dirname, '..', 'node_modules', 'lucide-static', 'icons', iconName + '.svg');
const raw = fs.readFileSync(src, 'utf8');
const inner = raw
.replace(/^[\s\S]*?<svg[^>]*>/, '')
.replace(/<\/svg>[\s\S]*$/, '')
.replace(/\s+/g, ' ')
.trim();
return `<svg ${wrapAttrs}>${inner}</svg>`;
}
const entries = Object.entries(iconMap);
const lines = entries.map(([condition, iconName], i) => {
const svg = buildSvg(iconName).replace(/'/g, "\\'");
const pad = ' '.repeat(Math.max(0, 16 - condition.length));
const comma = i < entries.length - 1 ? ',' : '';
return ` '${condition}':${pad}'${svg}'${comma}`;
});
const twig = [
'{# Auto-generated by scripts/gen-weather-icons.js — do not edit directly #}',
'{% set weather_icons = {',
...lines,
'} %}'
].join('\n') + '\n';
const dest = path.resolve(__dirname, '..', 'templates', 'partials', 'weather-icons.html.twig');
fs.writeFileSync(dest, twig);
console.log('Generated ' + path.relative(path.resolve(__dirname, '..'), dest));
@@ -6,7 +6,6 @@
{% endblock %}
{% block content %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe.css">
{% set journal_entries = page.collection() %}
{% set stories_page = grav.pages.find(page.parent().route ~ '/stories') %}
{% set story_entries = stories_page ? stories_page.children.published() : [] %}
@@ -68,67 +67,4 @@
{% endif %}
</div>
<script type="module">
import PhotoSwipeLightbox from 'https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe-lightbox.esm.min.js';
const lightbox = new PhotoSwipeLightbox({
gallery: '.pswp-gallery',
children: 'a.journal-photo-slide',
pswpModule: () => import('https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe.esm.min.js')
});
lightbox.on('afterOpen', function () {
var pswp = lightbox.pswp;
var keyDir = 0;
var clearTimer = null;
function onKey(e) {
if (e.key === 'ArrowRight') keyDir = 1;
else if (e.key === 'ArrowLeft') keyDir = -1;
else keyDir = 0;
}
document.addEventListener('keydown', onKey, true);
pswp.on('change', function () {
if (!keyDir) return;
var dir = keyDir;
keyDir = 0;
var el = pswp.currSlide && pswp.currSlide.container;
if (!el) return;
el.classList.remove('pswp-key-from-left', 'pswp-key-from-right');
el.offsetWidth;
el.classList.add(dir > 0 ? 'pswp-key-from-right' : 'pswp-key-from-left');
clearTimeout(clearTimer);
clearTimer = setTimeout(function () { el.classList.remove('pswp-key-from-left', 'pswp-key-from-right'); }, 400);
});
pswp.on('close', function () {
document.removeEventListener('keydown', onKey, true);
clearTimeout(clearTimer);
});
});
lightbox.init();
/* Per-strip: dot sync + expand button → tap the visible slide to trigger pswp */
document.querySelectorAll('.journal-photo-wrap').forEach(function (wrap) {
var strip = wrap.querySelector('.journal-photo-strip');
var slides = Array.from(strip.querySelectorAll('a.journal-photo-slide'));
var expandBtn = wrap.querySelector('.journal-photo-expand');
var article = wrap.closest('article');
var dots = article ? Array.from(article.querySelectorAll('.journal-photo-dot')) : [];
var visibleIdx = 0;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
visibleIdx = slides.indexOf(e.target);
dots.forEach(function (d) { d.classList.remove('is-active'); });
if (dots[visibleIdx]) dots[visibleIdx].classList.add('is-active');
});
}, { root: strip, threshold: 0.5 });
slides.forEach(function (s) { io.observe(s); });
if (expandBtn && slides.length) {
expandBtn.addEventListener('click', function () {
slides[visibleIdx].dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
});
}
});
</script>
{% endblock %}
+6 -123
View File
@@ -1,129 +1,12 @@
{% extends 'default.html.twig' %}
{% extends 'partials/base.html.twig' %}
{% block content %}
{% set weather_icons = {
'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️',
'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️',
'Snow': '❄️', 'Thunderstorm': '⛈️'
} %}
{% set hero = null %}
{% if page.header.hero_image and page.media[page.header.hero_image] is defined %}
{% set hero = page.media[page.header.hero_image] %}
{% elseif page.media.images|length > 0 %}
{% set hero = page.media.images|first %}
{% endif %}
<a class="back-pill entry-back-fixed" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
<article class="entry">
{% if hero %}
<div class="entry-hero">
<img src="{{ hero.cropResize(1440, 720).url }}" alt="{{ page.title }}" loading="eager">
</div>
{% endif %}
{% set entry = page %}
{% include 'partials/entry-journal.html.twig' %}
<header class="entry-header">
<div class="entry-header-meta">
<time class="entry-date" datetime="{{ page.date|date('Y-m-d') }}">
{{ page.date|date('l, d F Y') }}
</time>
{% if page.header.location_city or page.header.location_country %}
<p class="entry-location">
{%- set _loc = [] -%}
{%- if page.header.location_city -%}{%- set _loc = _loc|merge([page.header.location_city]) -%}{%- endif -%}
{%- if page.header.location_country -%}{%- set _loc = _loc|merge([page.header.location_country]) -%}{%- endif -%}
📍 {{ _loc|join(', ') }}
</p>
{% endif %}
{% if page.header.weather_desc or page.header.weather_temp_c %}
<p class="entry-weather">
{% if page.header.weather_desc %}
{{ weather_icons[page.header.weather_desc] ?? '🌡️' }} {{ page.header.weather_desc }}
{% endif %}
{% if page.header.weather_temp_c %}
· {{ page.header.weather_temp_c|round }}°C
{% endif %}
</p>
{% endif %}
</div>
<h1 class="entry-title">{{ page.title }}</h1>
<div class="entry-title-rule"></div>
</header>
<div class="entry-body">
{{ page.content|raw }}
</div>
{% set images = page.media.images %}
{% if images|length > 0 %}
<div class="entry-gallery" id="entry-gallery">
{% for image in images %}
<button class="gallery-thumb" data-full="{{ image.url }}" data-alt="{{ image.filename }}" aria-label="View {{ image.filename }}">
<img src="{{ image.cropResize(300, 300).url }}" alt="{{ image.filename }}" loading="lazy">
</button>
{% endfor %}
</div>
<div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-label="Photo viewer" hidden>
<button class="lightbox-close" id="lb-close" aria-label="Close">✕</button>
<button class="lightbox-prev" id="lb-prev" aria-label="Previous"></button>
<img class="lightbox-img" id="lb-img" src="" alt="">
<button class="lightbox-next" id="lb-next" aria-label="Next"></button>
</div>
<script>
(function() {
var gallery = document.getElementById('entry-gallery');
var lightbox = document.getElementById('lightbox');
var lbImg = document.getElementById('lb-img');
var thumbs = Array.from(gallery.querySelectorAll('.gallery-thumb'));
var current = 0;
function open(index) {
current = index;
var btn = thumbs[index];
lbImg.src = btn.dataset.full;
lbImg.alt = btn.dataset.alt;
lightbox.hidden = false;
document.body.style.overflow = 'hidden';
document.getElementById('lb-close').focus();
}
function close() {
lightbox.hidden = true;
document.body.style.overflow = '';
thumbs[current].focus();
}
function prev() { open((current - 1 + thumbs.length) % thumbs.length); }
function next() { open((current + 1) % thumbs.length); }
thumbs.forEach(function(btn, i) {
btn.addEventListener('click', function() { open(i); });
});
document.getElementById('lb-close').addEventListener('click', close);
document.getElementById('lb-prev').addEventListener('click', prev);
document.getElementById('lb-next').addEventListener('click', next);
lightbox.addEventListener('click', function(e) {
if (e.target === lightbox) close();
});
document.addEventListener('keydown', function(e) {
if (lightbox.hidden) return;
if (e.key === 'Escape') close();
if (e.key === 'ArrowLeft') prev();
if (e.key === 'ArrowRight') next();
});
})();
</script>
{% endif %}
<footer class="entry-footer">
<a class="back-pill" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
</footer>
</article>
<footer class="entry-footer">
<a class="back-pill" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
</footer>
{% endblock %}
+9 -72
View File
@@ -1,7 +1,11 @@
{% extends 'partials/base.html.twig' %}
{% block map_assets %}
{% do assets.addCss('theme://css-compiled/map.css') %}
{% do assets.addJs('theme://js/map.js', {group: 'bottom'}) %}
{% endblock %}
{% block content %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe.css">
{% set trip_route = config.site.active_trip %}
{% set trip = grav.pages.find(trip_route) %}
@@ -80,18 +84,13 @@
</div>
{% if map_entries|length > 0 %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
{% if home_gpx_urls|length > 0 %}
<script src="https://cdn.jsdelivr.net/npm/@mapbox/togeojson@0.16.2/togeojson.min.js"></script>
{% endif %}
<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>
<script>
var HOME_ENTRIES = {{ map_entries|json_encode|raw }};
var HOME_GPX_URLS = {{ home_gpx_urls|json_encode|raw }};
var USE_GPX = {{ trip and trip.header.use_gpx is not null ? (trip.header.use_gpx ? 'true' : 'false') : 'true' }};
var AUTOCONNECT = "{{ trip ? (trip.header.autoconnect ?? 'on') : 'on' }}";
document.addEventListener('DOMContentLoaded', function() {
var homeMap = new maplibregl.Map({
container: 'home-map',
style: MapUtils.MAP_STYLE,
@@ -135,71 +134,10 @@ homeMap.on('load', function () {
MapUtils.renderGpxJourney(homeMap, USE_GPX ? HOME_GPX_URLS : [], HOME_ENTRIES, 'home-gpx', 'home-journey', { connectMode: AUTOCONNECT });
});
}); // DOMContentLoaded
</script>
{% endif %}
<script type="module">
import PhotoSwipeLightbox from 'https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe-lightbox.esm.min.js';
const lightbox = new PhotoSwipeLightbox({
gallery: '.pswp-gallery',
children: 'a.journal-photo-slide',
pswpModule: () => import('https://cdn.jsdelivr.net/npm/photoswipe@5/dist/photoswipe.esm.min.js')
});
lightbox.on('afterOpen', function () {
var pswp = lightbox.pswp;
var keyDir = 0;
var clearTimer = null;
function onKey(e) {
if (e.key === 'ArrowRight') keyDir = 1;
else if (e.key === 'ArrowLeft') keyDir = -1;
else keyDir = 0;
}
document.addEventListener('keydown', onKey, true);
pswp.on('change', function () {
if (!keyDir) return;
var dir = keyDir;
keyDir = 0;
var el = pswp.currSlide && pswp.currSlide.container;
if (!el) return;
el.classList.remove('pswp-key-from-left', 'pswp-key-from-right');
el.offsetWidth;
el.classList.add(dir > 0 ? 'pswp-key-from-right' : 'pswp-key-from-left');
clearTimeout(clearTimer);
clearTimer = setTimeout(function () { el.classList.remove('pswp-key-from-left', 'pswp-key-from-right'); }, 400);
});
pswp.on('close', function () {
document.removeEventListener('keydown', onKey, true);
clearTimeout(clearTimer);
});
});
lightbox.init();
document.querySelectorAll('.journal-photo-wrap').forEach(function (wrap) {
var strip = wrap.querySelector('.journal-photo-strip');
var slides = Array.from(strip.querySelectorAll('a.journal-photo-slide'));
var expandBtn = wrap.querySelector('.journal-photo-expand');
var article = wrap.closest('article');
var dots = article ? Array.from(article.querySelectorAll('.journal-photo-dot')) : [];
var visibleIdx = 0;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
visibleIdx = slides.indexOf(e.target);
dots.forEach(function (d) { d.classList.remove('is-active'); });
if (dots[visibleIdx]) dots[visibleIdx].classList.add('is-active');
});
}, { root: strip, threshold: 0.5 });
slides.forEach(function (s) { io.observe(s); });
if (expandBtn && slides.length) {
expandBtn.addEventListener('click', function () {
slides[visibleIdx].dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
});
}
});
</script>
{% else %}
{# ══════════════════════════════════════════════════════ BETWEEN-TRIPS MODE #}
@@ -305,12 +243,10 @@ document.querySelectorAll('.journal-photo-wrap').forEach(function (wrap) {
</div>
{% if highlights_map_entries|length > 0 %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
<script src="{{ url('theme://js/maplibre-utils.js') }}"></script>
<script>
var HIGHLIGHTS_ENTRIES = {{ highlights_map_entries|json_encode|raw }};
document.addEventListener('DOMContentLoaded', function() {
var homeMap = new maplibregl.Map({
container: 'home-map',
style: MapUtils.MAP_STYLE,
@@ -349,6 +285,7 @@ homeMap.on('load', function () {
setTimeout(function () { homeMap.resize(); }, 100);
});
}); // DOMContentLoaded
</script>
{% endif %}
@@ -8,6 +8,7 @@
{% do assets.addCss('theme://css/style.css') %}
{% do assets.addCss('theme://css-compiled/main.css') %}
{% do assets.addJs('theme://js/main.js', {group: 'bottom'}) %}
{% do assets.addJs('theme://js/nav.js', {group: 'bottom'}) %}
{% block map_assets %}{% endblock %}
{{ assets.css()|raw }}
{{ assets.js()|raw }}
@@ -17,7 +18,12 @@
<header class="site-header">
<a class="site-title" href="{{ base_url_absolute }}">into the east</a>
{% block nav %}
<nav class="site-nav" aria-label="Main navigation">
<button class="nav-toggle" type="button" aria-label="Open menu" aria-controls="site-nav" aria-expanded="false">
<span class="nav-toggle__bar"></span>
<span class="nav-toggle__bar"></span>
<span class="nav-toggle__bar"></span>
</button>
<nav class="site-nav" id="site-nav" aria-label="Main navigation">
<a href="{{ base_url_absolute }}"{% if page.template == 'home' %} aria-current="page"{% endif %}>Home</a>
<a href="{{ base_url_absolute }}/trips"{% if page.template == 'trips' %} aria-current="page"{% endif %}>Past Trips</a>
</nav>
@@ -1,8 +1,4 @@
{% set weather_icons = {
'Sunny': '☀️', 'Partly cloudy': '⛅', 'Cloudy': '☁️',
'Foggy': '🌫️', 'Drizzle': '🌦️', 'Rain': '🌧️',
'Snow': '❄️', 'Thunderstorm': '⛈️'
} %}
{% include 'partials/weather-icons.html.twig' %}
<article class="journal-post" id="entry-{{ entry.slug }}" data-type="journal" data-lat="{{ entry.header.lat }}" data-lng="{{ entry.header.lng }}">
<header class="journal-post-header">
<h2 class="journal-post-title">{{ entry.title }}</h2>
@@ -12,7 +8,7 @@
</a>
{% if entry.header.location_city or entry.header.location_country %}
<span class="journal-post-location">
· 📍
· <svg width="11" height="13" viewBox="0 0 12 14" fill="currentColor" aria-hidden="true"><path d="M6 0C3.24 0 1 2.24 1 5c0 3.75 5 9 5 9s5-5.25 5-9c0-2.76-2.24-5-5-5zm0 6.75A1.75 1.75 0 1 1 6 3.25a1.75 1.75 0 0 1 0 3.5z"/></svg>
{%- set _loc = [] -%}
{%- if entry.header.location_city -%}{%- set _loc = _loc|merge([entry.header.location_city]) -%}{%- endif -%}
{%- if entry.header.location_country -%}{%- set _loc = _loc|merge([entry.header.location_country]) -%}{%- endif -%}
@@ -20,7 +16,7 @@
</span>
{% endif %}
{% if entry.header.weather_desc %}
<span class="journal-post-weather">· {{ weather_icons[entry.header.weather_desc] ?? '' }} {{ entry.header.weather_desc }}</span>
<span class="journal-post-weather">· {{ weather_icons[entry.header.weather_desc|lower]|default('')|raw }} {{ entry.header.weather_desc }}</span>
{% endif %}
</p>
</header>
@@ -0,0 +1,11 @@
{# Auto-generated by scripts/gen-weather-icons.js — do not edit directly #}
{% set weather_icons = {
'sunny': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4" /> <path d="M12 2v2" /> <path d="M12 20v2" /> <path d="m4.93 4.93 1.41 1.41" /> <path d="m17.66 17.66 1.41 1.41" /> <path d="M2 12h2" /> <path d="M20 12h2" /> <path d="m6.34 17.66-1.41 1.41" /> <path d="m19.07 4.93-1.41 1.41" /></svg>',
'partly cloudy': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2v2" /> <path d="m4.93 4.93 1.41 1.41" /> <path d="M20 12h2" /> <path d="m19.07 4.93-1.41 1.41" /> <path d="M15.947 12.65a4 4 0 0 0-5.925-4.128" /> <path d="M13 22H7a5 5 0 1 1 4.9-6H13a3 3 0 0 1 0 6Z" /></svg>',
'cloudy': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" /></svg>',
'foggy': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" /> <path d="M16 17H7" /> <path d="M17 21H9" /></svg>',
'drizzle': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" /> <path d="M8 19v1" /> <path d="M8 14v1" /> <path d="M16 19v1" /> <path d="M16 14v1" /> <path d="M12 21v1" /> <path d="M12 16v1" /></svg>',
'rain': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" /> <path d="M16 14v6" /> <path d="M8 14v6" /> <path d="M12 16v6" /></svg>',
'snow': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" /> <path d="M8 15h.01" /> <path d="M8 19h.01" /> <path d="M12 17h.01" /> <path d="M12 21h.01" /> <path d="M16 15h.01" /> <path d="M16 19h.01" /></svg>',
'thunderstorm': '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 16.326A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 .5 8.973" /> <path d="m13 12-3 5h4l-3 5" /></svg>'
} %}
@@ -6,6 +6,7 @@
{% endblock %}
{% block content %}
<div class="story-progress" id="story-progress"></div>
{% import 'macros/date-range.html.twig' as dr_m %}
{% set hero_url = null %}
{% if page.header.hero_image %}
@@ -46,6 +47,15 @@
</div>
<div class="story-body">
{% if location or date_str %}
<div class="story-opener">
<span class="story-opener__text">
{{- date_str -}}
{%- if location and date_str %} · {% endif -%}
{{- location -}}
</span>
</div>
{% endif %}
{{ page.content|raw }}
<footer class="story-footer">
@@ -187,6 +197,25 @@
});
})();
/* ── Reading progress bar ────────────────────────────────── */
(function () {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var bar = document.getElementById('story-progress');
var body = document.querySelector('.story-body');
if (!bar || !body) return;
function update() {
var rect = body.getBoundingClientRect();
var total = body.offsetHeight - window.innerHeight;
var scrolled = -rect.top;
var pct = total > 0 ? Math.min(100, Math.max(0, (scrolled / total) * 100)) : 0;
bar.style.width = pct.toFixed(1) + '%';
}
window.addEventListener('scroll', update, { passive: true });
update();
})();
/* ── ScrollySection (Scrollama) ──────────────────────────── */
(function () {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+1 -1
View File
@@ -71,7 +71,7 @@
<div class="home-trip-header">
<h1 class="home-trip-name">{{ page.title }}</h1>
{% if page.header.date_start %}
<p class="trip-dates" style="font-size:var(--text-sm);color:var(--color-ink-muted);margin:var(--space-1) 0 var(--space-2);">
<p class="trip-dates">
{{ page.header.date_start|date('d M Y') }}
{% if page.header.date_end %}{{ page.header.date_end|date('d M Y') }}{% else %} — Ongoing{% endif %}
</p>
@@ -13,6 +13,20 @@
{% set journal_count = dailies_page ? dailies_page.children.published()|length : 0 %}
{% set story_count = stories_page ? stories_page.children.published()|length : 0 %}
<a class="trip-card" href="{{ trip.url }}">
{% set cover = null %}
{% if trip.header.cover_image and trip.media[trip.header.cover_image] is defined %}
{% set cover = trip.media[trip.header.cover_image] %}
{% elseif dailies_page %}
{% set first_entry = dailies_page.children.published()|first %}
{% if first_entry and first_entry.media.images|length > 0 %}
{% set cover = first_entry.media.images|first %}
{% endif %}
{% endif %}
{% if cover %}
<div class="trip-card-cover">
<img src="{{ cover.cropResize(720, 240).url }}" alt="{{ trip.title }}" loading="lazy">
</div>
{% endif %}
<div class="trip-card-title">{{ trip.title }}</div>
<div class="trip-card-meta">
{% if trip.header.date_start %}