diff --git a/themes/intotheeast/blueprints/story.yaml b/themes/intotheeast/blueprints/story.yaml
index 5d60bfc..7b3e287 100644
--- a/themes/intotheeast/blueprints/story.yaml
+++ b/themes/intotheeast/blueprints/story.yaml
@@ -1,5 +1,12 @@
title: 'Story'
+# Inherits Grav's default page form — this is what supplies the page-media
+# uploader, plus the standard Options and Advanced tabs. Without it the form is
+# standalone and has no way to upload images (see entry.yaml, same pattern).
+'@extends':
+ type: default
+ context: blueprints://pages
+
form:
fields:
tabs:
@@ -8,6 +15,7 @@ form:
fields:
content:
+ ordering@: 0
type: tab
title: Content
fields:
@@ -34,7 +42,7 @@ form:
type: text
label: Hero Image
placeholder: 'hero.jpg'
- help: 'Filename of the hero image (upload via Media tab)'
+ help: 'Filename of an image uploaded in the Images field below'
header.hero_alt:
type: text
@@ -44,10 +52,53 @@ form:
content:
type: markdown
label: Content
+ help: 'Prose renders as a normal reading column. For layout blocks (scrolly sections, galleries, pull quotes) use the shortcodes listed on the Blocks tab.'
validate:
required: true
+ header.media_order:
+ type: pagemedia
+ label: Images
+ help: 'Upload every image the story needs here first — shortcodes refer to them by bare filename (photo-1.jpg), never by path.'
+
+ blocks:
+ ordering@: 1
+ type: tab
+ title: Blocks
+ fields:
+ blocks_intro:
+ type: spacer
+ text: "
Paste these into the Content field. Image parameters take a bare filename of an image uploaded in the Images field on the Content tab. The hero image is not a block \u2014 it has its own field on the Content tab.
"
+
+ blocks_scrolly:
+ type: spacer
+ text: "Scrolly section \u2014 text panels scroll over a pinned image
[scrolly-section image=\"hero.jpg\" alt=\"What the image shows\" caption=\"Optional caption\"]\nFirst panel.\n\n---\n\nSecond panel. A --- starts a new panel.\n[/scrolly-section]
"
+
+ blocks_pullquote:
+ type: spacer
+ text: "Pull quote \u2014 large extracted quote, image optional
[pull-quote image=\"photo-1.jpg\" alt=\"What the image shows\"]\nThe quote itself. Markdown works in here.\n[/pull-quote]
Drop image and alt for the plain no-image variant.
"
+
+ blocks_chapter:
+ type: spacer
+ text: "Chapter break \u2014 section transition over a background image
[chapter-break image=\"photo-1.jpg\" title=\"After Dark\" number=\"II\" alt=\"What the image shows\" /]
number is optional.
"
+
+ blocks_gallery:
+ type: spacer
+ text: "Snap gallery \u2014 swipeable carousel
[snap-gallery images=\"hero.jpg,photo-1.jpg\" captions=\"First caption,Second caption\" alts=\"First alt,Second alt\" /]
No commas inside captions. The three lists are split on commas and zipped by position, so one comma in a caption shifts every caption after it. Use em dashes or semicolons.
"
+
+ blocks_fullbleed:
+ type: spacer
+ text: "Full bleed \u2014 single edge-to-edge image as a visual pause
[full-bleed image=\"photo-2.jpg\" alt=\"What the image shows\" caption=\"Optional\" credit=\"Optional\" /]
"
+
+ blocks_imagecaption:
+ type: spacer
+ text: "Image with caption \u2014 photo at a chosen width
[image-caption image=\"photo-2.jpg\" alt=\"What the image shows\" caption=\"Optional\" credit=\"Optional\" width=\"column\" /]
width: column (default), full, or bleed.
"
+
+ blocks_gotchas:
+ type: spacer
+ text: "Two things that bite
Self-closing tags need the space before /]. And a mistyped shortcode fails silently \u2014 it renders as literal text or vanishes, with no warning. Preview the page after adding each block.
"
location:
+ ordering@: 2
type: tab
title: Location
fields:
@@ -97,21 +148,13 @@ form:
validate:
type: bool
+ # Published now comes from the inherited Options tab — defining it here
+ # too would render two toggles bound to the same header key.
publishing:
+ ordering@: 3
type: tab
title: Publishing
fields:
- header.published:
- type: toggle
- label: Published
- highlight: 1
- default: 1
- options:
- 1: 'Yes'
- 0: 'No'
- validate:
- type: bool
-
header.featured:
type: toggle
label: Featured highlight
@@ -123,3 +166,12 @@ form:
0: 'No'
validate:
type: bool
+
+ # Drop the inherited Date field — Story defines its own required
+ # "Start Date" on the Content tab, and both bind to header.date.
+ options:
+ fields:
+ publishing:
+ fields:
+ header.date:
+ unset@: true
diff --git a/themes/intotheeast/blueprints/trip.yaml b/themes/intotheeast/blueprints/trip.yaml
index f913581..9ce51b6 100644
--- a/themes/intotheeast/blueprints/trip.yaml
+++ b/themes/intotheeast/blueprints/trip.yaml
@@ -1,5 +1,13 @@
title: 'Trip'
+# Inherits Grav's default page form — this is what supplies the page-media
+# uploader (used for cover images and GPX files), plus the standard Options and
+# Advanced tabs. Without it the form is standalone and can only *select*
+# existing media, never upload any.
+'@extends':
+ type: default
+ context: blueprints://pages
+
form:
fields:
tabs:
@@ -8,6 +16,7 @@ form:
fields:
content:
+ ordering@: 0
type: tab
title: Content
fields:
@@ -21,7 +30,15 @@ form:
type: markdown
label: Description
+ # Inherited from the default blueprint; relabelled because this is
+ # also where GPX files and cover images get uploaded.
+ header.media_order:
+ type: pagemedia
+ label: Images and GPX files
+ help: 'Upload cover images and .gpx route files here. GPX tracks are auto-detected on the map — no manual linking. The Cover Image field below picks from what is uploaded here.'
+
trip:
+ ordering@: 1
type: tab
title: Trip
fields:
@@ -78,17 +95,6 @@ form:
'manual': 'Manual — force connect only'
'intelligent_gpx': 'Intelligent GPX — suppress where route is covered'
- publishing:
- type: tab
- title: Publishing
- fields:
- header.published:
- type: toggle
- label: Published
- highlight: 1
- default: 1
- options:
- 1: 'Yes'
- 0: 'No'
- validate:
- type: bool
+ # No Publishing tab — it held only the Published toggle, which now comes
+ # from the inherited Options tab. Defining it here too would render two
+ # toggles bound to the same header key.
diff --git a/themes/intotheeast/css/style.css b/themes/intotheeast/css/style.css
index 180099d..94c3822 100644
--- a/themes/intotheeast/css/style.css
+++ b/themes/intotheeast/css/style.css
@@ -2086,7 +2086,7 @@ body::after {
.scrolly-step:last-child { padding-bottom: 50vh; }
@media (max-width: 768px), (pointer: coarse) {
.scrolly { display: block; }
- .scrolly__steps { margin-top: calc(-(100vh - var(--site-header-height))); position: relative; z-index: 1; }
+ .scrolly__steps { margin-top: calc(-1 * (100vh - var(--site-header-height))); position: relative; z-index: 1; }
.scrolly-step { min-height: 80vh; padding: var(--space-8) var(--space-6); align-items: center; justify-content: center; }
.scrolly-step:last-child { padding-bottom: 50vh; }
}
diff --git a/themes/intotheeast/templates/story.html.twig b/themes/intotheeast/templates/story.html.twig
index 2f27eb3..7d9cc5a 100644
--- a/themes/intotheeast/templates/story.html.twig
+++ b/themes/intotheeast/templates/story.html.twig
@@ -132,23 +132,11 @@
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 });
-})();
+/* ── Back to top button ───────────────────────────────────
+ Owned by initBackToTop('story-totop') in js/src/main.js, called from its
+ DOMContentLoaded boot. An inline copy used to live here and double-bound
+ both the click and scroll handlers; it also lacked the history.pushState
+ that makes Back return to the reading position. Do not re-add it. */
/* ── ChapterBreak scroll-reveal ─────────────────────────── */
(function () {
@@ -216,11 +204,17 @@
update();
})();
-/* ── ScrollySection (Scrollama) ──────────────────────────── */
-(function () {
+/* ── ScrollySection (Scrollama) ────────────────────────────
+ Deferred to DOMContentLoaded on purpose: `scrollama` is published by the
+ js/main.js bundle, which Grav emits via assets.js('bottom') *after* this
+ inline script (base.html.twig renders block content first). Running inline
+ would hit the scrollama guard every time, leaving .scrolly__steps-content
+ hidden and the text column blank. DOMContentLoaded fires only after all
+ classic scripts have executed, so scrollama is available by then. */
+document.addEventListener('DOMContentLoaded', function () {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var sections = document.querySelectorAll('.scrolly');
- if (!sections.length || typeof scrollama === 'undefined') return;
+ if (!sections.length) return;
var panOffsets = ['50% 40%', '50% 50%', '50% 60%', '50% 45%', '50% 55%'];
@@ -252,7 +246,10 @@
stepsWrap.appendChild(step);
});
- if (reduced) {
+ /* Step-building above must never be gated on scrollama: the slot content is
+ display:none in CSS, so bailing before this point loses the text entirely.
+ Degrade to plain visible panels instead — no pinning, but readable. */
+ if (reduced || typeof scrollama === 'undefined') {
section.querySelectorAll('.scrolly-step').forEach(function (s) { s.classList.add('is-active'); });
return;
}
@@ -281,6 +278,6 @@
if (d.direction === 'up') d.element.classList.remove('is-active');
});
});
-})();
+});
{% endblock %}