+ // Card shell at the compact density so the explainer shares the
+ // spine's left text edge (the bare paragraph sat at the container
+ // edge, ~16–24 px left of every neighbouring card's text) — and
+ // foreground, not muted: multi-sentence explainer prose is
+ // content, muted stays reserved for meta.
+
+
+
diff --git a/src/components/insights/derived/coach-read-strip.tsx b/src/components/insights/derived/coach-read-strip.tsx
index 6e5eb68aa..8971fd4ea 100644
--- a/src/components/insights/derived/coach-read-strip.tsx
+++ b/src/components/insights/derived/coach-read-strip.tsx
@@ -8,6 +8,7 @@ import { useMounted } from "@/hooks/use-mounted";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import { apiGet } from "@/lib/api/api-fetch";
+import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { TileHeader } from "@/components/insights/tile-header";
import type { CoachReadStripData } from "@/lib/insights/derived/coach-read-shape";
@@ -109,28 +110,35 @@ export function CoachReadStrip({
// fires defensively.)
if (!baselineLine && !driverLine) return null;
- // Standard card anatomy — the same TileHeader-led shape as every other
- // insights card (foreground icon + title), instead of the former inline
- // mini-icon strip that read as chrome outside the card system. Body prose
- // in the regular foreground; muted stays reserved for meta lines.
+ // Standard card anatomy — the real Card + TileHeader + CardContent
+ // primitives at the compact density (the metric-stat-strip reference),
+ // so background, radius, icon spacing and the left text edge match
+ // every other insights tile. The former hand-rolled shell
+ // (`bg-card/60 … px-4 py-3.5`) painted a translucent background and a
+ // text edge ~8 px left of the card norm on md+. Body prose in the
+ // regular foreground; muted stays reserved for meta lines.
return (
-
-
-
- {baselineLine ? (
-
- {baselineLine}
-
- ) : null}
- {driverLine ? (
-
- {driverLine}
-
- ) : null}
-
-
+
+
+
+
+
+
+ {baselineLine ? (
+
+ {baselineLine}
+
+ ) : null}
+ {driverLine ? (
+
+ {driverLine}
+
+ ) : null}
+
+
+
);
}
diff --git a/src/components/insights/health-score-card.tsx b/src/components/insights/health-score-card.tsx
index 9eab66de4..1dc4a637b 100644
--- a/src/components/insights/health-score-card.tsx
+++ b/src/components/insights/health-score-card.tsx
@@ -302,7 +302,7 @@ export function HealthScoreCard({
data-slot="health-score-card"
data-band={band}
className={cn(
- "bg-card/65 rounded-xl border px-4 py-4 shadow-sm backdrop-blur-sm",
+ "bg-card/65 rounded-xl border p-4 shadow-sm backdrop-blur-sm md:p-6",
BAND_BORDER_CLASS[band],
// v1.4.27 MB7 / CF-34 — basis-based width so the score column
// flexes inside the hero strip's `md:flex-row` split. Earlier
diff --git a/src/components/insights/hero-strip.tsx b/src/components/insights/hero-strip.tsx
index 7cde2c5d2..d97dbf8b9 100644
--- a/src/components/insights/hero-strip.tsx
+++ b/src/components/insights/hero-strip.tsx
@@ -368,7 +368,7 @@ export function HeroStrip({
data-slot="health-score-card-skeleton"
aria-hidden="true"
className={cn(
- "bg-card/65 border-border/60 rounded-xl border px-4 py-4 shadow-sm backdrop-blur-sm",
+ "bg-card/65 border-border/60 rounded-xl border p-4 shadow-sm backdrop-blur-sm md:p-6",
"w-full md:shrink-0 md:grow-0 md:basis-[22rem] xl:basis-[26rem]",
"flex h-full min-h-64 flex-col gap-3.5",
)}
diff --git a/src/components/insights/insight-status-card.tsx b/src/components/insights/insight-status-card.tsx
index 08c86e806..9067a7b5d 100644
--- a/src/components/insights/insight-status-card.tsx
+++ b/src/components/insights/insight-status-card.tsx
@@ -107,7 +107,7 @@ export function InsightStatusCard({
aria-busy="true"
aria-live="polite"
data-testid="insight-status-card-preparing"
- className="gap-1.5 py-4 md:py-5"
+ className="gap-2 py-3 md:py-4"
>
@@ -140,11 +140,10 @@ export function InsightStatusCard({
aria-busy="true"
aria-live="polite"
data-testid="insight-status-card-loading"
- // v1.13.1 — match the canonical `gap-1.5` + `pb-1` heading-to-body
- // rhythm the populated / preparing / empty states use, so the
- // heading and the first skeleton line sit on the same tight
- // baseline across every assessment-card state.
- className="gap-1.5 py-4 md:py-5"
+ // Compact card density — the same tier the populated / preparing /
+ // empty states use, so the heading and the first skeleton line sit
+ // on the same baseline across every assessment-card state.
+ className="gap-2 py-3 md:py-4"
>
@@ -165,7 +164,7 @@ export function InsightStatusCard({
if (!hasProvider) {
return (
-
+
@@ -187,7 +186,7 @@ export function InsightStatusCard({
if (!text) {
return (
-
+
@@ -211,25 +210,15 @@ export function InsightStatusCard({
// v1.12.2 — stable hook for the spine guard test, which asserts the
// assessment is the LAST content block on every bespoke metric page.
data-slot="insight-assessment"
- // v1.8.5 W4a — the `Card` primitive ships `gap-4 md:gap-6` as the flex
- // gap between header and content, which floated the "Einschätzung"
- // heading ~16-24 px above its prose. Override to `gap-2 md:gap-3` (the
- // `CardHeader`'s own `pb-2` does not touch the flex gap) and trim the
- // shell padding to `py-4 md:py-5` so the assessment block reads tight
- // in the denser sub-page rhythm. The same override lands on every
- // status variant below so the gap is consistent across states.
- //
- // v1.8.6 — drop the purple left accent (`border-l-dracula-purple
- // border-l-2`). The coloured rule made the assessment card read as
- // restless against the calmer surrounding cards; the plain card
- // border carries enough separation on its own.
- //
- // v1.12.8 — tighten the header-to-prose gap. The card flex gap is
- // `gap-1.5` (was `gap-2 md:gap-3`) and the `CardHeader` trims its
- // bottom padding to `pb-1` (was `pb-2`), so the `` sits
- // close to the prose and matches the header-to-body rhythm of the
- // sibling tiles. The loading skeleton keeps its own spacing.
- className="animate-insight-in gap-1.5 py-4 md:py-5"
+ // Compact card density (`gap-2 py-3 md:py-4`) — the sanctioned tight
+ // tier, so the assessment shares one header→body rhythm with the
+ // target summary, the read strip, and the stat strip on the same
+ // spine. Earlier bespoke values (`gap-1.5` + `md:py-5`) predate the
+ // two-density contract and were normalised onto it. The same class
+ // lands on every status variant so the gap is consistent across
+ // states. (The one-time purple left accent stays dropped — the plain
+ // card border carries enough separation.)
+ className="animate-insight-in gap-2 py-3 md:py-4"
>
{/* v1.11.5 — the top-right "cached" label was removed: it surfaced
diff --git a/src/components/insights/metric-target-summary.tsx b/src/components/insights/metric-target-summary.tsx
index 2f5622e2b..41d4eec69 100644
--- a/src/components/insights/metric-target-summary.tsx
+++ b/src/components/insights/metric-target-summary.tsx
@@ -296,14 +296,14 @@ function TargetReferencePanel({
target.consistency7d.length > 0;
return (
- // Card-wrapped so the header-to-body offset matches the `` tiles on the same subpage spine rather than the old bare-div
- // `space-y-3`. The body keeps its inter-row `space-y-3` inside
+ // Card-wrapped at the compact density so the header-to-body offset
+ // matches the assessment / read-strip / stat-strip tiles on the same
+ // subpage spine. The body keeps its inter-row `space-y-3` inside
// `CardContent`; every data-slot is preserved.
{/* Row 1: the canonical tile header — a single-word "Ziel" / "Target"
heading (the range numbers live on the range bar below, so the
diff --git a/src/components/insights/mood/mood-insights-sections.tsx b/src/components/insights/mood/mood-insights-sections.tsx
index 025993f3f..5fe0d9385 100644
--- a/src/components/insights/mood/mood-insights-sections.tsx
+++ b/src/components/insights/mood/mood-insights-sections.tsx
@@ -287,7 +287,7 @@ export function MoodInsightsSections({
// v1.13.1 — match the canonical `gap-1.5` + `pb-1` heading-to-body
// rhythm the other assessment cards use, so the heading sits tight
// above its body instead of floating ~16-24 px above it.
- className="animate-insight-in gap-1.5 py-4 md:py-5"
+ className="animate-insight-in gap-2 py-3 md:py-4"
>
+