5eca310bd8
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
268 lines
11 KiB
Twig
268 lines
11 KiB
Twig
{% extends 'partials/base.html.twig' %}
|
||
|
||
{% block nav %}
|
||
<a class="story-escape" href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
||
<span class="story-nav-title" id="story-nav-title" aria-hidden="true">{{ page.title }}</span>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
{% 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] %}
|
||
{% endif %}
|
||
|
||
{% set date_str = page.date|date('d M Y') %}
|
||
{% if page.header.end_date and page.header.end_date != page.date|date('Y-m-d') %}
|
||
{% set sd = page.date|date('d') %}
|
||
{% set sm = page.date|date('M') %}
|
||
{% set sy = page.date|date('Y') %}
|
||
{% set ed = page.header.end_date|date('d') %}
|
||
{% set em = page.header.end_date|date('M') %}
|
||
{% set ey = page.header.end_date|date('Y') %}
|
||
{% if sy == ey and sm == em %}
|
||
{% set date_str = sd ~ ' – ' ~ ed ~ ' ' ~ em ~ ' ' ~ ey %}
|
||
{% elseif sy == ey %}
|
||
{% set date_str = sd ~ ' ' ~ sm ~ ' – ' ~ ed ~ ' ' ~ em ~ ' ' ~ ey %}
|
||
{% else %}
|
||
{% set date_str = sd ~ ' ' ~ sm ~ ' ' ~ sy ~ ' – ' ~ ed ~ ' ' ~ em ~ ' ' ~ ey %}
|
||
{% endif %}
|
||
{% endif %}
|
||
|
||
{% set location = page.header.location_name ?? '' %}
|
||
|
||
<div class="story-hero" id="story-hero">
|
||
<div class="story-hero__img-wrap">
|
||
{% if hero %}
|
||
<img src="{{ hero.url }}" alt="{{ page.header.hero_alt ?? page.title }}" class="story-hero__img" loading="eager">
|
||
{% else %}
|
||
<div class="story-hero__img-placeholder"></div>
|
||
{% endif %}
|
||
</div>
|
||
<div class="story-hero__overlay" id="story-overlay" aria-hidden="true"></div>
|
||
<div class="story-hero__content">
|
||
<h1 class="story-hero__title">{{ page.title }}</h1>
|
||
<p class="story-hero__meta">
|
||
<time datetime="{{ page.date|date('Y-m-d') }}">{{ date_str }}</time>
|
||
{% if location %} · {{ location }}{% endif %}
|
||
</p>
|
||
</div>
|
||
<div class="story-hero__scroll-cue" id="story-scroll-cue" aria-hidden="true">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<polyline points="6 9 12 15 18 9"/>
|
||
</svg>
|
||
</div>
|
||
<div class="story-hero__spacer"></div>
|
||
</div>
|
||
|
||
<div class="story-body">
|
||
{{ page.content|raw }}
|
||
|
||
<footer class="story-footer">
|
||
<a href="{{ page.parent().url }}" onclick="if(history.length > 1){ history.back(); return false; }">← Back</a>
|
||
</footer>
|
||
</div>
|
||
|
||
<button class="story-totop" id="story-totop" aria-label="Back to top">↑ Top</button>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/scrollama@3/build/scrollama.min.js"></script>
|
||
<script>
|
||
/* ── Hero scroll effect ──────────────────────────────────── */
|
||
(function () {
|
||
var overlay = document.getElementById('story-overlay');
|
||
var cue = document.getElementById('story-scroll-cue');
|
||
var hidden = false;
|
||
var ticking = false;
|
||
if (!overlay) return;
|
||
|
||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||
|
||
function update() {
|
||
var scrollY = window.scrollY;
|
||
var vh = window.innerHeight;
|
||
var heroEnd = vh * 1.4; // img-wrap (100vh) + spacer (40vh)
|
||
var mid = heroEnd * 0.5; // peak darkness at 70vh
|
||
var opacity = scrollY <= mid
|
||
? (scrollY / mid) * 0.65
|
||
: Math.max(0, (1 - (scrollY - mid) / (heroEnd - mid)) * 0.65);
|
||
overlay.style.background = 'rgba(0,0,0,' + opacity.toFixed(3) + ')';
|
||
overlay.style.display = (opacity < 0.002) ? 'none' : '';
|
||
if (!hidden && window.scrollY > 80) {
|
||
cue.classList.add('is-hidden');
|
||
hidden = true;
|
||
}
|
||
ticking = false;
|
||
}
|
||
|
||
window.addEventListener('scroll', function () {
|
||
if (!ticking) { requestAnimationFrame(update); ticking = true; }
|
||
}, { passive: true });
|
||
update();
|
||
})();
|
||
|
||
/* ── Story title in nav (cross-fades as hero content exits viewport top) ── */
|
||
(function () {
|
||
var navTitle = document.getElementById('story-nav-title');
|
||
var heroContent = document.querySelector('.story-hero__content');
|
||
if (!navTitle || !heroContent) return;
|
||
var hasBeenVisible = false;
|
||
var ticking = false;
|
||
|
||
function update() {
|
||
var rect = heroContent.getBoundingClientRect();
|
||
var inView = rect.bottom > 0 && rect.top < window.innerHeight;
|
||
if (inView) hasBeenVisible = true;
|
||
|
||
if (hasBeenVisible) {
|
||
var opacity;
|
||
if (rect.bottom <= 0) {
|
||
opacity = 1; // fully above viewport
|
||
} else if (rect.top <= 0) {
|
||
opacity = 1 - rect.bottom / rect.height; // partially exiting top
|
||
} else {
|
||
opacity = 0; // fully in viewport
|
||
}
|
||
navTitle.style.opacity = opacity.toFixed(3);
|
||
}
|
||
ticking = false;
|
||
}
|
||
|
||
window.addEventListener('scroll', function () {
|
||
if (!ticking) { requestAnimationFrame(update); ticking = true; }
|
||
}, { passive: true });
|
||
update();
|
||
})();
|
||
|
||
/* ── Back to top button ─────────────────────────────────── */
|
||
(function () {
|
||
var btn = document.getElementById('story-totop');
|
||
if (!btn) return;
|
||
var threshold = window.innerHeight * 0.8;
|
||
var shown = false;
|
||
btn.addEventListener('click', function () {
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
});
|
||
window.addEventListener('scroll', function () {
|
||
var shouldShow = window.scrollY > threshold;
|
||
if (shouldShow !== shown) {
|
||
shown = shouldShow;
|
||
btn.classList.toggle('is-visible', shown);
|
||
}
|
||
}, { passive: true });
|
||
})();
|
||
|
||
/* ── ChapterBreak scroll-reveal ─────────────────────────── */
|
||
(function () {
|
||
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
var panels = document.querySelectorAll('.chapter-break__panel');
|
||
if (!panels.length) return;
|
||
if (reduced) { panels.forEach(function (p) { p.classList.add('is-revealed'); }); return; }
|
||
var io = new IntersectionObserver(function (entries) {
|
||
entries.forEach(function (e) {
|
||
if (e.isIntersecting) { e.target.classList.add('is-revealed'); io.unobserve(e.target); }
|
||
});
|
||
}, { threshold: 0.25 });
|
||
panels.forEach(function (p) { io.observe(p); });
|
||
})();
|
||
|
||
/* ── PullQuote scroll-reveal ─────────────────────────────── */
|
||
(function () {
|
||
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
var quotes = document.querySelectorAll('.pull-quote');
|
||
if (!quotes.length) return;
|
||
if (reduced) { quotes.forEach(function (q) { q.classList.add('is-revealed'); }); return; }
|
||
var io = new IntersectionObserver(function (entries) {
|
||
entries.forEach(function (e) {
|
||
if (e.isIntersecting) { e.target.classList.add('is-revealed'); io.unobserve(e.target); }
|
||
});
|
||
}, { threshold: 0.2 });
|
||
quotes.forEach(function (q) { io.observe(q); });
|
||
})();
|
||
|
||
/* ── SnapGallery dot indicator ───────────────────────────── */
|
||
(function () {
|
||
document.querySelectorAll('.pgallery').forEach(function (gallery) {
|
||
var slides = gallery.querySelectorAll('.pgallery__slide');
|
||
var dots = gallery.querySelectorAll('.pgallery__dot');
|
||
if (!slides.length || !dots.length) return;
|
||
var io = new IntersectionObserver(function (entries) {
|
||
entries.forEach(function (e) {
|
||
if (e.isIntersecting) {
|
||
var idx = parseInt(e.target.dataset.index, 10);
|
||
dots.forEach(function (d) { d.classList.remove('is-active'); });
|
||
if (dots[idx]) dots[idx].classList.add('is-active');
|
||
}
|
||
});
|
||
}, { threshold: 0.5, root: gallery.querySelector('.pgallery__frame') });
|
||
slides.forEach(function (s) { io.observe(s); });
|
||
});
|
||
})();
|
||
|
||
/* ── ScrollySection (Scrollama) ──────────────────────────── */
|
||
(function () {
|
||
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
var sections = document.querySelectorAll('.scrolly');
|
||
if (!sections.length || typeof scrollama === 'undefined') return;
|
||
|
||
var panOffsets = ['50% 40%', '50% 50%', '50% 60%', '50% 45%', '50% 55%'];
|
||
|
||
sections.forEach(function (section) {
|
||
var img = section.querySelector('.scrolly__img');
|
||
var overlay = section.querySelector('.scrolly__img-overlay');
|
||
var contentEl = section.querySelector('.scrolly__steps-content');
|
||
var stepsWrap = section.querySelector('.scrolly__steps');
|
||
if (!img || !contentEl || !stepsWrap) return;
|
||
|
||
/* Split slot content on <hr> into individual step divs */
|
||
var nodes = Array.from(contentEl.childNodes);
|
||
var groups = [];
|
||
var curr = [];
|
||
nodes.forEach(function (n) {
|
||
if (n.nodeName === 'HR') { if (curr.length) groups.push(curr); curr = []; }
|
||
else curr.push(n);
|
||
});
|
||
if (curr.length) groups.push(curr);
|
||
if (!groups.length) groups.push(nodes);
|
||
|
||
groups.forEach(function (group) {
|
||
var step = document.createElement('div');
|
||
step.className = 'scrolly-step';
|
||
var inner = document.createElement('div');
|
||
inner.className = 'scrolly-step__inner';
|
||
group.forEach(function (n) { inner.appendChild(n.cloneNode(true)); });
|
||
step.appendChild(inner);
|
||
stepsWrap.appendChild(step);
|
||
});
|
||
|
||
if (reduced) {
|
||
section.querySelectorAll('.scrolly-step').forEach(function (s) { s.classList.add('is-active'); });
|
||
return;
|
||
}
|
||
|
||
/* Initial blur */
|
||
img.style.filter = 'blur(8px)';
|
||
img.style.transform = 'scale(1.04)';
|
||
img.style.transition = 'filter 0.7s cubic-bezier(.16,1,.3,1), transform 0.7s cubic-bezier(.16,1,.3,1), object-position 1.2s cubic-bezier(.16,1,.3,1)';
|
||
|
||
new IntersectionObserver(function (entries, obs) {
|
||
if (entries[0].isIntersecting) {
|
||
img.style.filter = 'blur(0)';
|
||
img.style.transform = 'scale(1)';
|
||
obs.disconnect();
|
||
}
|
||
}, { threshold: 0.1 }).observe(section);
|
||
|
||
scrollama()
|
||
.setup({ step: Array.from(section.querySelectorAll('.scrolly-step')), offset: 0.55 })
|
||
.onStepEnter(function (d) {
|
||
d.element.classList.add('is-active');
|
||
img.style.objectPosition = panOffsets[d.index % panOffsets.length];
|
||
if (overlay) overlay.style.background = 'rgba(0,0,0,' + (0.05 + (d.index % 3) * 0.05) + ')';
|
||
})
|
||
.onStepExit(function (d) {
|
||
if (d.direction === 'up') d.element.classList.remove('is-active');
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
{% endblock %}
|