From 0be009ad4d5e2ca16a075491c5469c203ce24622 Mon Sep 17 00:00:00 2001
From: anikamw <147226875+anikamw@users.noreply.github.com>
Date: Sun, 14 Jun 2026 15:11:05 -0700
Subject: [PATCH 1/2] feat(ui): visual journey map + pure-state interactivity
(gamification, trust-safe)
Design-research-led: make the relocation a VISIBLE journey while keeping every
mechanic a faithful projection of state the app already computes.
- Flagship JOURNEY MAP: the flat 'Your journey' list becomes a connected,
chapter-grouped visual path (chapters from an authored category->chapter map
in src/utils/journeyChapters.ts). Each node shows real state
(done/available/current/locked) from statusFor() + a per-node document meter
from docProgress(); available nodes clickable; LOCKED nodes stay readable
(only Mark-done gated) and show the specific 'Unlocks after: ' reason.
- Per-task document meter on the active card (nearer goal-gradient).
- Instant motivation micro-confirmation (from the real applicableTasks count).
- Non-punitive 'Welcome back' continuity (no streaks/countdowns/shaming).
- Per-chapter milestone notes extending the existing momentum toast.
NO dark patterns: no streaks/countdowns/points/false-approval; rewards reflect
only real done/skip state; a unit test (tests/journeyChapters.test.ts) bans
outcome/approval words in chapter copy. Disclaimer modal gate, provenance,
Impressum, Russia-sanctions + route-not-covered notices untouched;
prefers-reduced-motion respected; keyboard-accessible nodes.
Deferred: endowed head-start pre-credit, wizard auto-advance, interactive
canton map (needs sourced boundary geometry).
Verified: astro check 0 errors; build + provenance gate (15 pages); 281/281
tests; a11y PASS (WCAG 2.2 AA audited); trust/dark-pattern review PASS; full
flow confirmed live (6-node journey map, honest unlocks-after, doc meters).
Co-Authored-By: Claude Opus 4.8 (1M context)
---
FOUNDER-TLDR.md | 12 +
ROADMAP.md | 2 +-
src/components/CorridorApp.tsx | 556 ++++++++++++++++++++++++++++-----
src/utils/journeyChapters.ts | 126 ++++++++
tests/journeyChapters.test.ts | 41 +++
5 files changed, 653 insertions(+), 84 deletions(-)
create mode 100644 src/utils/journeyChapters.ts
create mode 100644 tests/journeyChapters.test.ts
diff --git a/FOUNDER-TLDR.md b/FOUNDER-TLDR.md
index cc84bd5..b1183ac 100644
--- a/FOUNDER-TLDR.md
+++ b/FOUNDER-TLDR.md
@@ -10,6 +10,18 @@ coming next. Newest updates are at the top.
---
+## 2026-06-14 — Your move now reads as a visible journey, not a flat to-do list
+
+**What changed.** The little "Your journey" list in the side panel is now a real **path you can see** — your steps strung along a line like stops on a route, grouped into clear stages ("Before you go", "Your permit", "After you arrive", "Settling in"). Each stop shows whether it's done (a tick), the one you're on (highlighted), open, or still locked. Locked stops now say *exactly* what unlocks them ("Unlocks after: register your address") instead of a blank padlock — and you can still read everything inside a locked step; you just can't tick it off until the step before it is done. Every step that needs documents shows a thin "gather-your-papers" bar that fills as you check them off (on the path and on the step itself). When you pick your reason for moving, we confirm in one line what that does to your plan. Returning visitors get a calm "Welcome back — you left off at…" note, and finishing a whole stage gives a quiet "you've finished Your permit" pat on the back. Crucially, **none of this is made up**: every tick, bar, and message just mirrors what you've actually done — no streaks, no countdowns, no points, and nothing that sounds like an official approval.
+
+**Why it matters.** A move is overwhelming; seeing it as a clear, honest path with visible progress makes people more likely to keep going and finish — without ever overstating where they stand or pressuring them.
+
+**What you'd notice today.** A friendlier, map-like journey panel with stages, progress bars, and gentle encouragement. (Review copy — **not live** yet.)
+
+**What's next.** Your review, then the same disclaimer/lawyer sign-off that gates the rest of the new plan experience.
+
+---
+
## 2026-06-14 — A calmer, friendlier plan (and a one-time disclaimer you confirm up front)
**What changed.** The plan used to feel like paperwork: a gold "not legal advice" warning sat over everything, questions used legal codes, and each step buried the to-dos under reference text. We rebuilt the *feel*. The gold banner is gone; instead, right after you answer the questions, a single pop-up shows the legal disclaimer and you tap **"I've read and understand"** once before your plan opens. The plan now leads with **what to do** (the steps), tucking legal citations and fine print into a "Reference" section you can open if you want. Questions use plain words ("Less than a year" instead of "L permit path"), progress shows friendly stages, and you get a small "nice work" nudge as you finish steps. The legal notice is still everywhere it needs to be — the confirm pop-up, the footer on every page, and a short line on each step — just calm, not shouting.
diff --git a/ROADMAP.md b/ROADMAP.md
index 986a5b3..fbc290e 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -6,7 +6,7 @@
>
> Legend: ✅ done · 🚧 in progress · ⏳ planned · 🔁 ongoing · 🗓️ backlog
-Last updated: 2026-06-14 (Calmer/encouraging UI overhaul (design-research-led; pending human-lawyer sign-off): removed the gold "not legal advice" banner everywhere; after the intake a REQUIRED disclaimer modal gate (role=dialog, focus-trapped, no bypass, compliance-owned wording single-sourced in `src/utils/disclaimer.ts`, acked in localStorage `rg-disclaimer-ack-v1`) must be confirmed before the plan renders — also gates shared-link opens; task cards re-sequenced action-first (tldr→steps→documents) with key facts/legal citations/detail behind one "Reference" fold and the caveats consolidated into one "Sources & legal notes" fold (a short "not legal advice" line stays always-visible per card per compliance); plain-language intake options (dropped L/B/C permit codes & "concubinage"); stable phase-based progress ("Your route→situation→details") with progressbar a11y; warm all-breakpoint intro; quiet per-step momentum + calmer document gate. Disclaimer still in the modal + footer (every page) + per-card + Impressum + no-JS guide. Compliance verdict: CONDITIONAL — the modal-acknowledgement posture (vs a persistent banner) needs human-lawyer sign-off before merge; astro check 0 / build+gate / 277 tests green; a11y reviewed (modal focus-trap OK). Cantonal fee/timeline transparency: 12 us-ch steps that have no single federal fee/processing-time figure (the employer-sponsored work permit, the family-reunification routes, the retirement permit, the study permits/sub-steps) now show a user-facing "Fees & processing time" note that these are set by the canton — or, for study admission, the institution — and to confirm locally; driven by a presentation-only `localCostTimeline` task hint (no invented numbers, no new sourced claims, ADR-0007/ADR-0021), plus a "varies by canton" caveat now always shown in the "Your canton" panel. Canton-aware plan-step links: the six "apply at / contact your cantonal authority of the canton where you live/study" steps (family reunification ×3, unmarried-partner concubinage, retirement, study) now deep-link the user's *selected* canton's own migration office (ZH Migrationsamt, GE OCPM, VD SPOP, ZG, BS) instead of the federal SEM directory of all 26 — falling back to that directory for not-yet-sourced cantons, so no dead ends. A `StepLinkSchema.cantonOffice` rendering hint (asserts no fact, no provenance) drives a frontend-only swap to the already-VERIFIED canton migrationOffice claim; static SSR full-guide stays federal (can't know the canton). Build/gate/`astro check`/277 tests green; fact-verifier (accurate, no overreach) + provenance-integrity verified; ADR-0021. Outbound links: the written-guide's 109 external source links now open in a new tab (target=_blank + rel=noopener noreferrer), matching the planner — readers don't lose their place. Plan UI: the desktop plan sidebar (progress / answers / journey) is now collapsible (Hide/Show), giving the task panel full width; mobile already had this. Canton dimension for Switzerland (ADR-0021): corridor-embedded `cantons` schema + provenance gate; CH-only "which canton?" intake step (searchable combobox over 26 cantons, optional) + `cn` share-param + "Your canton" plan panel with a verified SEM-directory fallback; per-canton sourced detail for ZH/GE/VD/ZG/BS in the two-agent pipeline. Earlier: Link audit: all 950 source links render-checked — 147 good, 23 Fedlex "under preparation" links flagged + the us-ch fix applied (79 occurrences → static-HTML, independently fact-verifier re-verified; re-audit now 0 suspect / 170 good). The weekly link-auditor now also checks anchor-reach — a `#article` link that loads but whose section never renders (SPA shell) goes RED on SPA domains (fedlex-class regressions); france-visas added to manual-check (403s the bot, fine for users). Live run: 0 errors / 142 URLs. Plan-personalisation correctness: "Who's joining you?" companion multi-select (partner/children/"It's just me") replaces the children yes/no; family/dependant steps gated to who is actually coming; full-guide route labels; cross-corridor plan-correctness regression suite — ADR-0020. UI/UX "Trust & Authority" design system adopted — audit fixes 1–10: AA-contrast CTAs, 44px touch targets, readable legal text, EB Garamond + Lato self-hosted fonts, brand tokens, consent-overlap fix, home/wizard trust framing, plus polish 11–13 (SVG icons, consistent focus rings, tighter line-length); ADR-0019. Earlier: 8 corridors published + intake availability dots, ADR-0018; source-snapshot cache, ADR-0017; corridor production via agent teams in two runs, ADR-0016; all-routes intake + launch waitlist, ADR-0015; launch-readiness drafts, og:image, dormant consent+analytics, ADR-0013; us-ch founder approval, ADR-0014; go-live blocked on domain + lawyer review)
+Last updated: 2026-06-14 (Interactive "journey map" + pure-state gamification (design-research-led; builds on the calmer-UI branch; stacked on it): the flat "Your journey" sidebar list is now a connected, chapter-grouped VISUAL PATH (chapters from an authored category→chapter map in `src/utils/journeyChapters.ts`: Before you go / Your permit / After you arrive / Settling in), each node carrying its real state (done/available/current/locked) from `statusFor()` + a per-node document meter from `docProgress()`; available nodes are clickable, LOCKED nodes stay readable (only Mark-done gated) and show the SPECIFIC "Unlocks after: …" prerequisite from `dependsOn`. Plus a per-task document meter on the card, an instant motivation micro-confirmation, a non-punitive "Welcome back" continuity line, and per-chapter milestone notes extending the existing momentum toast. Everything is a pure projection of existing state — NO dark patterns (no streaks/countdowns/points/false-approval), enforced by a unit test (`tests/journeyChapters.test.ts`) that BANS outcome/approval words in chapter copy. Disclaimer modal gate + provenance + Impressum + Russia/route-not-covered notices untouched; prefers-reduced-motion respected; keyboard-accessible nodes. Verified: astro check 0 / build+gate (15 pages) / 281 tests; a11y PASS (WCAG 2.2 AA audited); trust/dark-pattern review PASS. Deferred: endowed head-start pre-credit, wizard auto-advance, interactive canton map (needs sourced boundary geometry). Calmer/encouraging UI overhaul (design-research-led; pending human-lawyer sign-off): removed the gold "not legal advice" banner everywhere; after the intake a REQUIRED disclaimer modal gate (role=dialog, focus-trapped, no bypass, compliance-owned wording single-sourced in `src/utils/disclaimer.ts`, acked in localStorage `rg-disclaimer-ack-v1`) must be confirmed before the plan renders — also gates shared-link opens; task cards re-sequenced action-first (tldr→steps→documents) with key facts/legal citations/detail behind one "Reference" fold and the caveats consolidated into one "Sources & legal notes" fold (a short "not legal advice" line stays always-visible per card per compliance); plain-language intake options (dropped L/B/C permit codes & "concubinage"); stable phase-based progress ("Your route→situation→details") with progressbar a11y; warm all-breakpoint intro; quiet per-step momentum + calmer document gate. Disclaimer still in the modal + footer (every page) + per-card + Impressum + no-JS guide. Compliance verdict: CONDITIONAL — the modal-acknowledgement posture (vs a persistent banner) needs human-lawyer sign-off before merge; astro check 0 / build+gate / 277 tests green; a11y reviewed (modal focus-trap OK). Cantonal fee/timeline transparency: 12 us-ch steps that have no single federal fee/processing-time figure (the employer-sponsored work permit, the family-reunification routes, the retirement permit, the study permits/sub-steps) now show a user-facing "Fees & processing time" note that these are set by the canton — or, for study admission, the institution — and to confirm locally; driven by a presentation-only `localCostTimeline` task hint (no invented numbers, no new sourced claims, ADR-0007/ADR-0021), plus a "varies by canton" caveat now always shown in the "Your canton" panel. Canton-aware plan-step links: the six "apply at / contact your cantonal authority of the canton where you live/study" steps (family reunification ×3, unmarried-partner concubinage, retirement, study) now deep-link the user's *selected* canton's own migration office (ZH Migrationsamt, GE OCPM, VD SPOP, ZG, BS) instead of the federal SEM directory of all 26 — falling back to that directory for not-yet-sourced cantons, so no dead ends. A `StepLinkSchema.cantonOffice` rendering hint (asserts no fact, no provenance) drives a frontend-only swap to the already-VERIFIED canton migrationOffice claim; static SSR full-guide stays federal (can't know the canton). Build/gate/`astro check`/277 tests green; fact-verifier (accurate, no overreach) + provenance-integrity verified; ADR-0021. Outbound links: the written-guide's 109 external source links now open in a new tab (target=_blank + rel=noopener noreferrer), matching the planner — readers don't lose their place. Plan UI: the desktop plan sidebar (progress / answers / journey) is now collapsible (Hide/Show), giving the task panel full width; mobile already had this. Canton dimension for Switzerland (ADR-0021): corridor-embedded `cantons` schema + provenance gate; CH-only "which canton?" intake step (searchable combobox over 26 cantons, optional) + `cn` share-param + "Your canton" plan panel with a verified SEM-directory fallback; per-canton sourced detail for ZH/GE/VD/ZG/BS in the two-agent pipeline. Earlier: Link audit: all 950 source links render-checked — 147 good, 23 Fedlex "under preparation" links flagged + the us-ch fix applied (79 occurrences → static-HTML, independently fact-verifier re-verified; re-audit now 0 suspect / 170 good). The weekly link-auditor now also checks anchor-reach — a `#article` link that loads but whose section never renders (SPA shell) goes RED on SPA domains (fedlex-class regressions); france-visas added to manual-check (403s the bot, fine for users). Live run: 0 errors / 142 URLs. Plan-personalisation correctness: "Who's joining you?" companion multi-select (partner/children/"It's just me") replaces the children yes/no; family/dependant steps gated to who is actually coming; full-guide route labels; cross-corridor plan-correctness regression suite — ADR-0020. UI/UX "Trust & Authority" design system adopted — audit fixes 1–10: AA-contrast CTAs, 44px touch targets, readable legal text, EB Garamond + Lato self-hosted fonts, brand tokens, consent-overlap fix, home/wizard trust framing, plus polish 11–13 (SVG icons, consistent focus rings, tighter line-length); ADR-0019. Earlier: 8 corridors published + intake availability dots, ADR-0018; source-snapshot cache, ADR-0017; corridor production via agent teams in two runs, ADR-0016; all-routes intake + launch waitlist, ADR-0015; launch-readiness drafts, og:image, dormant consent+analytics, ADR-0013; us-ch founder approval, ADR-0014; go-live blocked on domain + lawyer review)
> **Current focus:** USA → Switzerland (`us-ch`) **only**, until it is perfected end-to-end.
> All other corridors are deferred (see DECISIONS.md **ADR-0006**). India → Germany (`in-de`)
diff --git a/src/components/CorridorApp.tsx b/src/components/CorridorApp.tsx
index 3f564ce..92419a5 100644
--- a/src/components/CorridorApp.tsx
+++ b/src/components/CorridorApp.tsx
@@ -4,6 +4,7 @@ import { DISCLAIMER_LEAD, DISCLAIMER_BODY, DISCLAIMER_PROGRESS_NOTE, IMPRESSUM_P
import { CH_CANTONS, cantonName } from '../utils/cantons';
import { flagUrl } from '../utils/flags';
import { topoOrder, statusOf, currentTaskId, type TaskStatus } from '../utils/journey';
+import { chapterForCategory, compareChapters, type Chapter } from '../utils/journeyChapters';
import { evaluateAppliesIf, type AppliesIfContext } from '../utils/appliesIf';
import { INTAKE_PARAM_KEYS, readIntakeParams, applyIntakeParams, intakeSearchString, hasIntakeParams } from '../utils/urlState';
import { submitWaitlist, isValidEmail, alreadyOnWaitlist, type WaitlistStatus } from '../utils/waitlist';
@@ -486,6 +487,24 @@ function useIsMobile() {
return isMobile;
}
+/**
+ * True when the user has asked the OS to reduce motion. Every new animation in
+ * the journey map (the path-draw, node scale, milestone slide) is gated on this
+ * being `false`. Starts `false` so the server and first client render agree;
+ * corrected from matchMedia after mount.
+ */
+function usePrefersReducedMotion() {
+ const [reduced, setReduced] = useState(false);
+ useEffect(() => {
+ const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
+ setReduced(mq.matches);
+ const handler = (e: MediaQueryListEvent) => setReduced(e.matches);
+ mq.addEventListener('change', handler);
+ return () => mq.removeEventListener('change', handler);
+ }, []);
+ return reduced;
+}
+
/** localStorage key recording that the user has read & acknowledged the
* post-intake disclaimer in this browser. Bumped to -v2 etc. if the wording
* materially changes and re-acknowledgement is required. */
@@ -1002,16 +1021,6 @@ function buildRecap(a: Intake): { label: ReactNode; stepId: string }[] {
// ── Journey sidebar (the "map + history") ────────────────────────────────────
-function StatusDot({ status }: { status: TaskStatus }) {
- if (status === 'done') {
- return ;
- }
- if (status === 'available') {
- return ;
- }
- return ;
-}
-
/**
* Share the current plan URL (answers ride in the URL fragment — never sent to
* servers). The pre-copy warning is compliance-required: the link encodes the
@@ -1211,8 +1220,261 @@ function CantonPanel({ canton, data }: { canton: string; data: CantonData | unde
);
}
+// ── Visual journey map (the connected, chapter-grouped "Your journey" path) ──
+
+/**
+ * Group the already-ordered tasks into authored chapters WITHOUT reordering the
+ * journey. We keep the existing topological order intact (it respects real
+ * dependencies); a chapter "starts" at the first task whose chapter differs
+ * from the previous task's. This means a chapter can in principle appear once
+ * (the common case) and the path still reads top-to-bottom in dependency order.
+ * Each entry also carries the task's GLOBAL step index (1-based) so node labels
+ * can say "Step 3" against the whole journey, not within the chapter.
+ */
+interface JourneyGroup {
+ chapter: Chapter;
+ items: { task: TaskData; stepIndex: number }[];
+}
+
+function groupTasksByChapter(orderedTasks: TaskData[]): JourneyGroup[] {
+ // Build chapter order from the chapters actually present, in authored order.
+ const present = new Map();
+ for (const t of orderedTasks) {
+ const ch = chapterForCategory(t.category);
+ if (!present.has(ch.id)) present.set(ch.id, ch);
+ }
+ const orderedChapters = [...present.values()].sort(compareChapters);
+
+ return orderedChapters.map((chapter) => ({
+ chapter,
+ items: orderedTasks
+ .map((task, i) => ({ task, stepIndex: i + 1 }))
+ .filter(({ task }) => chapterForCategory(task.category).id === chapter.id),
+ }));
+}
+
+/** A single node on the visual path. Accessible name conveys step + state. */
+function JourneyNode({
+ task, stepIndex, totalSteps, status, isActive, isLast, docCompleted, docTotal,
+ prerequisiteTitle, reducedMotion, onSelect,
+}: {
+ task: TaskData;
+ stepIndex: number;
+ totalSteps: number;
+ status: TaskStatus;
+ isActive: boolean;
+ /** Last node overall — its connector tail is not drawn. */
+ isLast: boolean;
+ docCompleted: number;
+ docTotal: number;
+ /** Title of the specific prerequisite that gates a locked node (item 5). */
+ prerequisiteTitle: string | null;
+ reducedMotion: boolean;
+ onSelect: (id: string) => void;
+}) {
+ const done = status === 'done';
+ const locked = status === 'locked';
+ const available = status === 'available';
+
+ // Accessible name: state-first so screen-reader users hear what they can do.
+ // "current" is an emphasis layered on "available" — convey both.
+ const stateWord = done
+ ? 'done'
+ : isActive
+ ? 'current step, available'
+ : available
+ ? 'available'
+ : 'locked';
+ const lockHint =
+ locked && prerequisiteTitle ? `, unlocks after: ${prerequisiteTitle}` : '';
+ const docHint = docTotal > 0 ? `, ${docCompleted} of ${docTotal} documents handled` : '';
+ const accessibleName = `Step ${stepIndex} of ${totalSteps}, ${stateWord}: ${task.title}${lockHint}${docHint}`;
+
+ const docPct = docTotal > 0 ? Math.round((docCompleted / docTotal) * 100) : 0;
+
+ return (
+
+
+ {/* Connector + node marker column (decorative — aria-hidden). The line
+ is a thin CSS rule behind the marker; the marker is an inline SVG. */}
+
+ {/* Connector tail to the next node (not on the last node). The fill is
+ solid for a done→done segment, faint otherwise — a quiet "progress
+ threading the path" cue, never an invented reward. */}
+ {!isLast && (
+
+ )}
+ {/* Node marker. */}
+
+ {done ? (
+
+ ) : locked ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* The clickable node body. AUTONOMY: locked nodes stay clickable so
+ their content is readable — only Mark-done is gated elsewhere. */}
+
+
+
+ );
+}
+
+/**
+ * The signature visual journey: a vertical connected PATH of task nodes,
+ * grouped under authored chapters. Every node's state is a faithful projection
+ * of `statusFor(id)` + the active task id; the per-node meter projects
+ * `docProgress`. Built with inline SVG/CSS only (no heavy deps); fully
+ * keyboard-operable (each node is a button with an accessible name); the
+ * connector / visual flourishes are aria-hidden; honours prefers-reduced-motion.
+ */
+function JourneyMap({
+ orderedTasks, statusFor, activeId, docState, prerequisiteTitleFor, reducedMotion, onSelect,
+}: {
+ orderedTasks: TaskData[];
+ statusFor: (id: string) => TaskStatus;
+ activeId: string | null;
+ docState: DocState;
+ /** Returns the title of the first incomplete prerequisite for a task, or null. */
+ prerequisiteTitleFor: (task: TaskData) => string | null;
+ reducedMotion: boolean;
+ onSelect: (id: string) => void;
+}) {
+ const groups = useMemo(() => groupTasksByChapter(orderedTasks), [orderedTasks]);
+ const totalSteps = orderedTasks.length;
+ const lastTaskId = orderedTasks[orderedTasks.length - 1]?.id ?? null;
+
+ return (
+
+ );
+}
+
function Sidebar({
- orderedTasks, statusFor, activeId, onSelect, recap, onEdit, doneCount, total, docState, onToggleDoc, cantonPanel,
+ orderedTasks, statusFor, activeId, onSelect, recap, onEdit, doneCount, total, docState, cantonPanel,
+ prerequisiteTitleFor, reducedMotion,
}: {
orderedTasks: TaskData[];
statusFor: (id: string) => TaskStatus;
@@ -1223,13 +1485,12 @@ function Sidebar({
doneCount: number;
total: number;
docState: DocState;
- onToggleDoc: (taskId: string, docName: string, mark: DocMark) => void;
/** Rendered "Your canton" card (CH + canton chosen); null otherwise. */
cantonPanel?: ReactNode;
+ /** Returns the title of the first incomplete prerequisite for a task, or null. */
+ prerequisiteTitleFor: (task: TaskData) => string | null;
+ reducedMotion: boolean;
}) {
- // Substeps: the active step auto-expands; any unlocked step can be toggled.
- const [expandedId, setExpandedId] = useState(null);
- const shownExpandedId = expandedId ?? activeId;
const pct = total > 0 ? Math.round((doneCount / total) * 100) : 0;
return (
@@ -1266,58 +1527,21 @@ function Sidebar({
{cantonPanel}
+ {/* SIGNATURE: the connected, chapter-grouped visual PATH. Every node's
+ state, the per-node document meter and the why-locked prerequisite
+ are faithful projections of statusFor / docProgress / dependsOn —
+ no invented copy or reward. Locked nodes stay clickable & readable;
+ only Mark-done is gated on the task card. */}
+
);
@@ -1327,7 +1551,7 @@ function Sidebar({
function TaskCard({
task, status, hasPrev, onBack, onMarkDone, onNext, headingRef, docState, onToggleDoc,
- cantonOfficeClaim, selectedCantonName,
+ cantonOfficeClaim, selectedCantonName, reducedMotion, prerequisiteTitle,
}: {
task: TaskData;
status: TaskStatus;
@@ -1343,8 +1567,12 @@ function TaskCard({
cantonOfficeClaim: ClaimData | null;
/** Display name of the selected canton, for the personalised link cue. */
selectedCantonName?: string;
+ reducedMotion: boolean;
+ /** When this task is locked, the specific prerequisite that gates it (item 5). */
+ prerequisiteTitle?: string | null;
}) {
const done = status === 'done';
+ const locked = status === 'locked';
const { completed: docsHandled, total: docsTotal } = docProgress(task, docState);
const docsRemaining = docsTotal - docsHandled;
@@ -1377,10 +1605,14 @@ function TaskCard({
{task.title}
-
- {done ? 'Completed' : 'Ready to start'}
+ {locked ? (
+
+ ) : (
+
+ )}
+ {done ? 'Completed' : locked ? 'Locked for now' : 'Ready to start'}
{docsTotal > 0 && (
@@ -1388,6 +1620,15 @@ function TaskCard({
)}
+ {/* HONEST WHY-LOCKED on the card too: locked content stays fully
+ readable; this names the real prerequisite. Only Mark-done is gated
+ (see the footer). */}
+ {locked && prerequisiteTitle && (
+
+ You can read this step now. It unlocks once you finish{' '}
+ {prerequisiteTitle}.
+
+ )}
{/* ACTION FIRST (progressive disclosure). (a) one-line "what and why",
@@ -1477,6 +1718,20 @@ function TaskCard({
Documents needed
{docsHandled}/{docsTotal} handled
+ {/* PER-TASK DOCUMENT METER (item 2): the same docProgress data as the
+ journey map, so the nearer-goal gradient is felt on the card too.
+ Skipped docs already count as handled upstream — never shown as
+ 'missing'. Decorative bar (the count above is the AT label). */}
+ {docsTotal > 0 && (
+
Check off each document as you gather it — or skip ones that don't apply to your case.
@@ -1612,9 +1867,20 @@ function TaskCard({
+ {/* Mark-done is gated for a LOCKED step (item 5) — the content above stays
+ fully readable, but you can't tick a step done before its real
+ prerequisite. We name the prerequisite rather than greying out
+ silently. */}
+ {locked && (
+
+ {prerequisiteTitle
+ ? <>This step unlocks once you finish {prerequisiteTitle}. You can read everything here in the meantime.>
+ : <>This step unlocks once you finish the steps it depends on. You can read everything here in the meantime.>}
+
+ )}
{/* Calmer gate: a helper near the checklist instead of a scolding line on
a greyed button. Only shows when documents are still outstanding. */}
- {!done && docsRemaining > 0 && (
+ {!done && !locked && docsRemaining > 0 && (
A few documents to go — check the ones you have, skip what doesn't apply, and you can
mark this step done.
@@ -1630,11 +1896,12 @@ function TaskCard({
+ >{locked ? 'Locked until earlier steps are done' : 'Mark done & continue'}
)}
@@ -1894,12 +2161,14 @@ function DisclaimerModal({ onAcknowledge }: { onAcknowledge: () => void }) {
/**
* Brief, dismissible affirmation shown after the user marks a step done. Counts
* ONLY the user's own real progress (no fabricated reassurance). Adds a quiet
- * milestone note as the plan crosses 25 / 50 / 75 %. Auto-dismisses; also
- * dismissible by the user.
+ * milestone note as the plan crosses 25 / 50 / 75 %. When the marked step was
+ * the LAST in its chapter, also shows a one-line, action-anchored chapter note
+ * (item 6) — phrased as the USER's own progress through that stage, never as an
+ * authority's verdict. Auto-dismisses; also dismissible by the user.
*/
function MomentumToast({
- remaining, pct, onDismiss,
-}: { remaining: number; pct: number; onDismiss: () => void }) {
+ remaining, pct, chapterNote, onDismiss,
+}: { remaining: number; pct: number; chapterNote: string | null; onDismiss: () => void }) {
useEffect(() => {
const t = setTimeout(onDismiss, 6000);
return () => clearTimeout(t);
@@ -1926,6 +2195,10 @@ function MomentumToast({
? `${remaining} to go.`
: 'That was the last one.'}
+ {/* PER-CHAPTER MILESTONE (item 6): finishing a chapter's last task. The
+ note describes the USER's action ("You have finished …") and never
+ implies an authority's decision (no "approved"/"ready"/"all set"). */}
+ {chapterNote &&