Three single-class polish swaps to retire P0 / P1 drifts the UI-
conformity review caught:
- TrendCard headline number was the only dashboard tile using
font-bold for its hero value; every sibling renders the same
numeric role at font-semibold. Align so the trend strip reads
uniformly.
- account-section password form was the only form in the codebase
on space-y-3 outer rhythm; every other form uses space-y-4. Lift
to match.
- mood-chart YAxis tickMargin sat at 6 while every other line chart
passes 10 from the same canonical recipe. Match so mood labels
sit at the same distance from the axis as siblings.
`<RangeBar>` painted its red / orange / green zones in raw Tailwind
palette (`bg-red-500/8`, `bg-yellow-500/12`, `bg-green-500/20`),
while the marker dot already drew from `var(--dracula-red|orange|green)`.
That split meant the zone backgrounds drifted out of the theme on
dark-mode and never tracked the rest of the token surface (PR
badges, alerts, status pills). Swap the zone tints to the matching
Dracula tokens and pin the absence of the legacy raw palette in the
existing extraction-contract test.
Tailwind v4's content scanner walks the entire project root by default
and emits a CSS rule for every class-shaped token it finds — including
literal class fragments quoted inside .planning report markdown and
docs/ design specs. The bracketed CSS-variable arbitrary-value form
combined with an opacity modifier (e.g. `bg-[var(--name)]/12`)
generates an escaped selector whose `\(...\)\/N` token sequence
crashes Turbopack's CSS parser with `Unexpected token Delim('.')`,
500-ing every page that imports globals.css.
The crash:
./src/app/globals.css:1853:27
Parsing CSS source code failed
1852 | .bg-\[var\(...\)\]\/12 {
> 1853 | background-color: var(...);
| ^
Three coordinated changes:
- Add `@source not "../../.planning"` and `@source not "../../docs"`
to globals.css so the scanner skips planning + docs trees. Stops
fixture-style class names in research/audit reports from poisoning
the generated CSS.
- Migrate `bg-[var(--dracula-*)]/N` / `ring-[var(--dracula-*)]/N` in
target-card.tsx to the Tailwind v4 parenthesised shorthand
`bg-(--dracula-*)/N` / `ring-(--dracula-*)/N`. The shorthand emits
a clean selector that LightningCSS parses cleanly. Per the Tailwind
v4 upgrade guide, the bracketed form is deprecated for CSS
variables.
- Replace `shadow-[0_0_0_1px_var(--dracula-green)]/20` in
consistency-strip.tsx with `shadow-[0_0_0_1px_color-mix(in_oklab,var(--dracula-green)_20%,transparent)]`.
The arbitrary shadow with opacity modifier hits the same parser
issue; encoding the alpha directly in the value via color-mix
removes the modifier and keeps the visual identical.
Verification: `pnpm dev --port 3030` then `curl -sI /auth/login` ->
HTTP/1.1 200 OK (was 500). Zero CSS parse errors in the dev server
log. Affected tests (targets + api-handler + api/version) still pass
(31/31).
7 conflict files resolved by taking the upstream/HEAD side (which carries
the W4d/W5/W6c/W3f feature additions). Stashed-side changes that were
genuine new features re-land in subsequent commits if any were lost.
This sweeps up the loose state the W4e+W5e+W5d combined agent left
mid-stash. Resolves develop's working tree for clean push.
Removes the duplicate `targets.mood` + `targets.moodStability` flat
strings that collided with the new `targets.mood.stability.{stable,
variable,highlyVariable}` object added in Phase 3. The flat versions
were unreferenced — the canonical mood label lookup goes through
`targets.label.MOOD_SCORE` / `targets.label.MOOD_STABILITY` — so
removing them is a no-op for the UI and unblocks the
i18n-locale-integrity duplicate-key test.
Also rolls in the prettier-normalised formatting on the targets
route + target-card files (no semantic change). All i18n keys
added across phases 3-8 are documented inline:
• targets.summary.{weekTitle,weekTitleAll,streakHighlight}
• targets.card.{lastMet,streak}
• targets.consistency.{ariaLabel,daysInRange,daysLogged,
dotInRange,dotNearRange,dotOutOfRange,
dotNotLogged,insufficientData,relativeDay.*}
• targets.relativeDay.{today,yesterday,daysAgo}
• targets.coach.cta
• targets.mood.stability.{stable,variable,highlyVariable}
Per-target Coach prefills (the W3e plan called for
targets.coach.prefill.{bp,weight,…}) are NOT added as i18n strings
— they're generated by `buildTargetPrompt()` in
src/lib/ai/coach/target-prompts.ts which interpolates live state
values. Keeping the templating in JS avoids 12 long-form bilingual
strings with i18n interpolation gymnastics and lets the prompt
include conditional fragments (streak, cadence) without exploding
the key count.
Verification:
- pnpm typecheck: clean
- pnpm test: 2325 / 2325 passing
- pnpm lint: clean
- i18n-locale-integrity: 9 / 9 passing (duplicate keys removed)
Per Marc's "mobile is co-equal with desktop" directive, every
breakpoint gets a deliberate layout — no afterthought reflow:
• default (<640px): grid-cols-1, full-width cards, gap-4. The
per-card Coach CTA spans full width (justify-center) so the
touch target is generous; the source link sits beneath
right-aligned.
• sm (640-1023px): grid-cols-2, gap-4. Card footer reverts to
horizontal — Coach CTA left, source link right.
• lg (1024px+): grid-cols-3, gap-6. Three cards per row matches
the dashboard / insights rhythm and lets the user see all six
core targets above the fold on a 1440px viewport.
Card internals also reflow:
• Headline row: icon + label stack with status pill below on
mobile (sm:flex-row sm:justify-between), inline on sm+.
• Footer row: flex-col items-stretch on mobile,
sm:flex-row sm:items-center sm:justify-between on sm+.
New `targets-responsive.test.tsx` pins the breakpoint classes so a
future refactor can't quietly collapse the grid back to v1.4.22's
sm:grid-cols-2 only layout.
Verified by inspection (Tailwind class audit). A Playwright sweep
across 375 / 768 / 1280 / 1440 viewports is queued for the W3e
verification phase.
The card used to render the raw σ value ("0.42 σ") as the big headline
number. Most users cannot interpret a standard deviation; the W3e
research found this was the single biggest "what does this mean?"
friction point in the v1.4.22 page.
Replace with a three-state verbal label keyed by the
`moodStabilityLabel(sigma)` helper landed in the previous phase
(thresholds: < 1.0 stable, < 2.0 variable, ≥ 2.0 highly variable).
Translations under `targets.mood.stability.{stable,variable,highlyVariable}`
in EN + DE.
The raw σ value stays one hover away — the headline carries a
tooltip surfacing "σ = 0.42" so power users keep the precise number
without forcing every visitor to decode it. Headline also gets a
`capitalize` class so the lowercase i18n string ("stable" / "stabil")
renders sentence-cased.
Replaces the inline 790-line page.tsx composition with the W3e
primitives extracted in the previous phase. The page is now ~270
lines of orchestration:
- TanStack-Query pulls the redesigned targets payload (with the
v1.4.25 consistency + summary additions).
- A second query reads /api/insights/provider-chain to derive
`aiEnabled = chainStatus?.activeProvider != null`. The flag threads
down into every <TargetCard>, which uses it to gate the per-card
Coach CTA — no broken-button state when the user has no provider
configured.
- A new `useCoachHandoff()` hook (src/hooks/use-coach-handoff.ts)
owns the drawer's open + prefill + scope triple. Per-card buttons
call `askCoach()`; the hook flips state and the drawer mounts
inline at the page level.
- Cards now render in a fixed order (BP / BP-in-target / WEIGHT /
PULSE / BMI / MOOD / COMPLIANCE / SLEEP / BODY_FAT / STEPS) — Marc
directive: stable visual hierarchy beats status-sorting because
the user's eye lands on the same metric in the same place every
visit.
- Source-citation URLs stay on the page (declarative routing) and
thread into <TargetCard> via the `sourceLink` prop.
The old inline TargetCard / RangeBar / TrendIcon / status-category
map are deleted along with the dead `Badge` + lucide imports.
Existing targets-i18n + targets-spacing tests updated to mock the
new useQueryClient + useMutation surfaces that the embedded
<CoachDrawer> needs at SSR; both suites still assert the same
visual contracts and pass.
Wave 2 deliverables also folded into this commit:
- pageSummary mounts via the W3e header above the grid
- consistency strip renders only when insufficientData === false
- recency caption ("last met goal: 2 days ago") renders only when
lastMetGoalAt !== null
- streak chip ("4-day streak") renders only when streakDays ≥ 3
Adds a one-line summary above the card grid that surfaces the
account-level state at a glance:
"4 of 6 targets met this week" / "All 6 targets met this week ✓"
When every target hits the bar we surface a calm green check mark; no
animation, no celebration. Per Marc's "muss nicht so sein wie die KI
Sache" — the summary stays scannable and quieter than the v1.4.20
Insights hero strip.
A streak highlight chip ("5-day streak: Blood pressure") renders to
the right when one metric carries a current ≥ 3 day streak. Resolves
via the existing targets.label.{TYPE} i18n map so the metric name
respects the user's locale.
The new <TargetsSummaryHeader> primitive lives under
src/components/targets/ alongside the rest of the W3e extraction.
4 tests cover partial / all-met / streak / empty branches.
The /targets page composed everything inline in a 790-line page.tsx.
Break it into focused primitives under src/components/targets/ so the
W3e redesign can iterate per-card without growing the page file:
- target-card.tsx — the redesigned card composition. Headline + verbal
status pill, big-number row with inline trend chevron, range bar,
consistency strip, recency + streak chips (both conditional on real
data), per-card Coach CTA gated by aiEnabled, source link footer.
MOOD_STABILITY swaps the raw σ headline for a verbal label via the
moodStabilityLabel helper. Fixed card order is owned by the page;
this component is presentational.
- range-bar.tsx — extracted intact from page.tsx. Same behaviour, same
data-slot. Now carries its own test suite.
- consistency-strip.tsx — NEW. 7 dots (one per Berlin-tz day), each
filled / amber / red / hollow per the day's band classification.
Caption flips between "X of 7 in range" and "X of 7 logged" when
cadence is the more informative signal. Accessible: <ul role="list">
with per-dot aria-labels seeded from i18n. No empty-state copy
inside the strip — Marc directive: hide entirely when insufficient.
- target-coach-button.tsx — NEW. Per-card "Ask Coach about this" CTA
that returns null when aiEnabled is false. No greyed-out button,
no tooltip placeholder — the gate is total per Marc's
"no broken-button state" directive.
Supporting helpers (each unit-tested):
- src/lib/targets/mood-stability-label.ts — pure σ → verbal-label
threshold logic (< 1.0 stable, < 2.0 variable, ≥ 2.0
highlyVariable). Thresholds documented in the helper and confirmed
in the W3e plan.
- src/lib/ai/coach/target-prompts.ts — 8 metric × 2 locale templates
for the per-card Coach prefill. Falls back to a general template for
glucose contexts. PROMPT_VERSION unchanged (the assistant-facing
prompt does not shift; only user-side prefills do).
- src/lib/ai/coach/target-scope.ts — per-target CoachScope.sources
mapping. Derived metrics (BMI → weight, MOOD_STABILITY → mood) point
at the underlying source; unmapped targets return an empty array
so the drawer falls back to defaults.
i18n keys added: targets.consistency.* (cap + dot aria + relative day
+ insufficient-data), targets.card.{lastMet,streak}, targets.relativeDay.*,
targets.summary.{weekTitle,weekTitleAll,streakHighlight},
targets.coach.cta, targets.mood.stability.* — all EN + DE.
Tests: 17 cases across 5 suites (consistency-strip render variants,
target-coach-button gate branches, range-bar zone painting,
target-card full-data + insufficient-data + aiEnabled-off +
MOOD_STABILITY verbal-headline, mood-stability-label thresholds).
Page.tsx still uses the old inline composition for now — Phase 5
wires it to the new components.