diff --git a/src/components/AppShell.vue b/src/components/AppShell.vue
index 7496192..004fdab 100644
--- a/src/components/AppShell.vue
+++ b/src/components/AppShell.vue
@@ -18,6 +18,7 @@ import { useAudioIntent } from '@/composables/useAudioIntent';
import { useJourneySections } from '@/composables/useJourneySections';
import { useKeyboardNavigation } from '@/composables/useKeyboardNavigation';
import { useMotionPreferences } from '@/composables/useMotionPreferences';
+import { useRouteMemory } from '@/composables/useRouteMemory';
import { assetUrl } from '@/content/asset-url';
import { signRouteFor } from '@/journey/sign-routes';
import type { PortfolioContent, PublicAssetManifest, SectionId, SectionLayout } from '@/content/types';
@@ -60,6 +61,10 @@ const sectionLabel = (id: SectionId): string => {
/** One wayfinding vocabulary: chapter plates reuse the roadside sign markers. */
const exitFor = (id: SectionId): string => signRouteFor(id).marker;
+const nextExitFor = (id: SectionId): string | undefined => {
+ const next = canonicalSectionIds[canonicalSectionIds.indexOf(id) + 1];
+ return next === undefined ? undefined : exitFor(next);
+};
const nextLabelFor = (id: SectionId): string | undefined => {
const index = canonicalSectionIds.indexOf(id);
const next = canonicalSectionIds[index + 1];
@@ -75,6 +80,18 @@ const journeyBinding = useJourneySections(
computed(() => motion.policy.value.allowSceneRequest),
);
const journey = journeyBinding.snapshot;
+const route = useRouteMemory(canonicalSectionIds, () => journey.value.activeSectionId);
+const resume = computed(() => {
+ const id = route.resumeFrom.value;
+ return id === null ? null : { id, exit: exitFor(id), label: sectionLabel(id) };
+});
+/** Decorative trip log for the destination (CSS generated content). */
+const tripLog = computed(() => {
+ const minutes = route.tripMinutes.value;
+ if (minutes === null) return undefined;
+ const exits = route.visited.value.size;
+ return `${exits} OF ${canonicalSectionIds.length} EXITS \u00b7 ${minutes} MIN ON THE ROAD`;
+});
const audioIntent = useAudioIntent({
canonicalIds: canonicalSectionIds,
journey: journeyBinding,
@@ -241,6 +258,7 @@ onUnmounted(() => removePageListeners());
@@ -249,9 +267,9 @@ onUnmounted(() => removePageListeners());
-
+
-
+
{{ content.profile.introduction }}
@@ -261,15 +279,15 @@ onUnmounted(() => removePageListeners());
-
+
-
+
-
+
removePageListeners());
-
+
-
+
-
+
diff --git a/src/components/HeroJourney.vue b/src/components/HeroJourney.vue
index 1564375..e5ad302 100644
--- a/src/components/HeroJourney.vue
+++ b/src/components/HeroJourney.vue
@@ -5,11 +5,14 @@ import NameParticles from './NameParticles.vue';
withDefaults(defineProps<{
identity: Readonly;
allowMotion?: boolean;
-}>(), { allowMotion: false });
+ /** Where a returning visitor left the road (client-only, after mount). */
+ resume?: { id: string; exit: string; label: string } | null;
+}>(), { allowMotion: false, resume: null });
const emit = defineEmits<{
explore: [event: MouseEvent];
contact: [event: MouseEvent];
+ resume: [event: MouseEvent];
}>();
@@ -24,6 +27,17 @@ const emit = defineEmits<{
Contact Me
Explore work
+
+
Portfolio
diff --git a/src/components/SiteNavigation.vue b/src/components/SiteNavigation.vue
index 7c9d928..53428da 100644
--- a/src/components/SiteNavigation.vue
+++ b/src/components/SiteNavigation.vue
@@ -1,11 +1,13 @@
-