"use client";

import {
  useEffect,
  useMemo,
  useRef,
  useState,
  type CSSProperties,
  type ReactNode,
} from "react";
import { useMetricsVersion } from "@enode/core/metrics/hooks";
import { getMetric } from "@enode/core/metrics/store";
import { displayValue } from "@enode/core/training/measurements";
import type { WorkoutSession } from "@enode/core/training/models";
import { buildTechniqueCharts } from "@enode/core/training/rep-chart-service";
import { setTimelineReps } from "@enode/core/training/set-review";
import { MetricBar } from "@enode/ui/charts/metric-bar";
import { EnodeMark } from "@enode/ui/enode-mark";
import { MetricStatCard } from "@enode/ui/metric-stat-card";
import {
  metricCategoryColorHex,
  spacialAltColorHex,
} from "@enode/core/metrics/category-colors";
import { SetReviewHeader } from "@enode/ui/set-review-header";
import { SetTimeline } from "@enode/ui/charts/set-timeline";
import { TechniqueLineChart } from "@enode/ui/charts/technique-line-chart";
import { TechniqueTrajectoryChart } from "@enode/ui/charts/technique-trajectory-chart";
import {
  SagittalBodyBackdrop,
  BackViewBodyBackdrop,
} from "@enode/ui/charts/bar-orientation-illustration";
import videoMock from "./assets/video-mock.png";
import {
  HEADER_BLUR,
  HEADER_MIN_HEIGHT,
  headerEdgeClass,
} from "@enode/ui/header-height";
import { SidebarIcon, XIcon } from "@enode/ui/icons";
import { SessionStats } from "../../workouts/today/session-stats";
import { chartableReps, TechniqueTab } from "../../workouts/today/technique-tab";
import { PerformanceTab } from "../../workouts/today/performance-tab";
import { AdviceCard } from "../../workouts/today/advice-card";
import { LoadAdjustBar } from "../../workouts/today/load-adjust-bar";
import { SwitchRecommendationCard } from "../../workouts/today/switch-recommendation-card";
import { StationRail, StationColumn } from "../../workouts/today/station-rail";
import { SidebarActionsView } from "../../workouts/today/sidebar-actions";
import { RAIL_SURFACE_CLASS } from "../../workouts/today/station-spine";
import { RequestStationSlotOverlay } from "../../workouts/today/request-station-slot-overlay";
import {
  AppTopBar,
  InsightsHeader,
  InsightsBottomNav,
  type InsightsTabId,
} from "./app-chrome";
import { EXERCISE_GROUP } from "@enode/core/training/exercise-constants";
import { squatSessionFixture } from "./fixture-session";
import {
  CLEAN_PIN_FRAC,
  CMJ_PIN_FRAC,
  VIDEO_PIN_FRAC,
  useCapturedCleanSession,
  useCapturedCmjSession,
  useCapturedVideoSession,
} from "./captured-sessions";
import { DeviceMock } from "./device-mock";
import { deviceTargetById, type DeviceTarget } from "./devices";
import {
  CMJ_TRACKING_COLOR,
  CMJ_TRACKING_ROWS,
  CMJ_TRACKING_SESSION_MAX,
  SAMPLE_DURATION,
  SAMPLE_REPS,
  SET_TRACKING_COLOR,
  SET_TRACKING_ROWS,
  SET_TRACKING_SESSION_MAX,
  type MockSetRow,
} from "./sample-data";

export type ScreenSpec = {
  id: string;
  /** Small category label above the headline (brand exception copy). */
  category: string;
  /** Marketing headline. */
  headline: string;
  /** Headline override used only by the left-text Android hero (which has room
   *  for a longer value statement than the compact `headline`). */
  heroHeadline?: string;
  /** Supporting value points, rendered as a bullet list under the hero
   *  headline (Android left-text hero only). */
  heroPoints?: string[];
  /** Tablet-only screen — the tile grid hides phone targets for it. */
  padOnly?: boolean;
  /** Frameless composite tile — the render draws its own device frames. */
  hero?: boolean;
  render: (target: DeviceTarget) => ReactNode;
};

export const SCREENS: ScreenSpec[] = [
  {
    id: "technique",
    category: "Bar path tracking",
    headline: "Every movement, mapped.",
    render: (target) => <BarPathScreen target={target} />,
  },
  {
    id: "cmj",
    category: "Jump analysis",
    headline: "Explosive power, measured.",
    render: (target) => <JumpScreen target={target} />,
  },
  {
    id: "set-tracking",
    category: "Velocity-based training",
    headline: "Auto-tracked, rep by rep.",
    render: (target) => <TrackingScreen target={target} />,
  },
  {
    id: "insights",
    category: "Performance insights",
    headline: "Every metric that matters.",
    render: (target) => <InsightsScreen target={target} />,
  },
  {
    id: "video-review",
    category: "Video review",
    headline: "Watch it back, frame by frame.",
    render: (target) => <VideoReviewScreen target={target} />,
  },
  {
    id: "stations",
    category: "Multi-station",
    headline: "Run the whole floor.",
    padOnly: true,
    render: () => <MultiStationScreen />,
  },
  {
    id: "ecosystem",
    category: "One platform",
    headline: "Every screen, in sync.",
    // The Android hero has a left text column with room for the full pitch.
    heroHeadline: "GPS for the weight room.",
    heroPoints: [
      "Velocity-based training, rep by rep",
      "Frame-by-frame technique analysis",
      "Run the whole floor, live",
      "Built for teams, effortless to use",
    ],
    hero: true,
    render: (target) => <HeroEcosystem target={target} />,
  },
];

const isPad = (target: DeviceTarget) => target.deviceClass === "pad";

// The back-squat session that drives the velocity-based-training screen's
// Performance tab. Rebuilt when the metrics catalog resolves (so the load /
// velocity / power measurements bind to real metric ids). See fixture-session.ts.
function useSquatSession(): WorkoutSession {
  // `squatSessionFixture` reads the metrics store (getMetricByKey) at build time,
  // so rebuild when the catalog version changes — `version` is a real dependency
  // the linter can't see (it's mutable global state, not a closure value).
  const version = useMetricsVersion();
  // eslint-disable-next-line react-hooks/exhaustive-deps
  return useMemo(() => squatSessionFixture(), [version]);
}


// ─── Ecosystem hero — a frameless composite tile ──────────────────────────────
// The iMac (Studio Display frame) sits in the back showing Insights, with the
// iPhone (velocity-based training) and iPad (technique) in front. Each device is
// a real DeviceMock rendering the real screen for its device class — so this is
// the actual app on three devices at once, not a mock-up.
function HeroEcosystem({ target }: { target: DeviceTarget }) {
  const w = target.exportW;
  const mac = deviceTargetById("mac");
  const ipad = deviceTargetById("ipad-13");
  const iphone = deviceTargetById("iphone-6-9");
  if (!mac || !ipad || !iphone) return null;
  const macScreen = <InsightsScreen target={mac} />;
  const ipadScreen = <BarPathScreen target={ipad} />;
  const iphoneScreen = <TrackingScreen target={iphone} />;

  // Portrait (iPhone store tile): a layered scene that bleeds off the edges
  // rather than a tall stack. The iMac is oversized in the back and runs off the
  // RIGHT (only its left side shows); the iPad slides in from the LEFT edge (only
  // its right half shows); the iPhone sits in front at the bottom-right. The
  // wrapper spans the full tile width (past the tile's content padding) so the
  // devices reach the tile edges.
  if (target.exportH > target.exportW) {
    const pad = Math.round(80 * (w / 1320));
    return (
      <div className="absolute inset-y-0" style={{ left: -pad, width: w }}>
        {/* iMac — Insights, oversized backdrop that comes in from the RIGHT:
            its left border sits just inside the left edge (with a margin) and the
            rest runs off the right. */}
        <div className="absolute z-0" style={{ left: w * 0.08, top: "6%" }}>
          <DeviceMock target={mac} frameWidthPx={w * 1.28}>
            {macScreen}
          </DeviceMock>
        </div>
        {/* iPad — Technique, entering from the left (right half only). */}
        <div className="absolute z-10" style={{ left: -w * 0.46, top: "45%" }}>
          <DeviceMock target={ipad} frameWidthPx={w * 0.9}>
            {ipadScreen}
          </DeviceMock>
        </div>
        {/* iPhone — velocity-based training, in front, bottom-right. */}
        <div className="absolute bottom-0 z-20" style={{ right: w * 0.05 }}>
          <DeviceMock target={iphone} frameWidthPx={w * 0.46}>
            {iphoneScreen}
          </DeviceMock>
        </div>
      </div>
    );
  }

  // Android wide feature graphic: text lives on the LEFT (drawn by the tile);
  // this composition fills the RIGHT region, so the devices are sized as
  // fractions of that ~58%-wide area and positioned within it. The desktop
  // display sits back-centre with the Android tablet + phone in front — the
  // same three-screen arrangement as the landscape hero, but Android hardware.
  if (target.id === "hero-android") {
    const androidPhone = deviceTargetById("android-phone");
    const androidTablet = deviceTargetById("android-tablet");
    if (!androidPhone || !androidTablet) return null;
    return (
      <div className="relative h-full w-full">
        {/* Desktop display — Insights, back-centre of the region. */}
        <div className="absolute left-1/2 top-[6%] z-0 -translate-x-1/2">
          <DeviceMock target={mac} frameWidthPx={w * 0.34}>
            <InsightsScreen target={mac} />
          </DeviceMock>
        </div>
        {/* Android tablet — Technique, front-right. */}
        <div className="absolute bottom-[3%] z-10" style={{ right: w * 0.01 }}>
          <DeviceMock target={androidTablet} frameWidthPx={w * 0.28}>
            <BarPathScreen target={androidTablet} />
          </DeviceMock>
        </div>
        {/* Android phone — velocity-based training, front-left. */}
        <div className="absolute bottom-0 z-20" style={{ left: w * 0.02 }}>
          <DeviceMock target={androidPhone} frameWidthPx={w * 0.12}>
            <TrackingScreen target={androidPhone} />
          </DeviceMock>
        </div>
      </div>
    );
  }

  // Landscape: iMac centered in the back, iPhone + iPad in front.
  return (
    <div className="relative h-full w-full">
      <div className="absolute left-1/2 top-0 z-0 -translate-x-1/2">
        <DeviceMock target={mac} frameWidthPx={w * 0.5}>
          {macScreen}
        </DeviceMock>
      </div>
      <div className="absolute bottom-0 z-10" style={{ right: w * 0.03 }}>
        <DeviceMock target={ipad} frameWidthPx={w * 0.36}>
          {ipadScreen}
        </DeviceMock>
      </div>
      <div className="absolute bottom-0 z-20" style={{ left: w * 0.06 }}>
        <DeviceMock target={iphone} frameWidthPx={w * 0.145}>
          {iphoneScreen}
        </DeviceMock>
      </div>
    </div>
  );
}

// ─── Insights pane — the REAL Technique / Performance tab under reproduced chrome
// The tab (charts, value header, bar path, set/rep selector) is the actual app
// component driven by the fixture session. Only the frosted "Insights" header
// bar and the tab-pill row are reproduced (they live inside the route-level
// InsightsSheet and aren't exported). `--insights-top/-bottom` reserve space for
// those overlays exactly as InsightsSheet publishes them.

function InsightsPane({
  tab,
  session,
  showClose,
  barbell = true,
  exerciseGroupID,
  pinCrosshairFrac,
}: {
  tab: InsightsTabId;
  // Null while a captured session is still decoding — the technique tab then
  // shows its loading skeleton until the swap.
  session: WorkoutSession | null;
  showClose: boolean;
  // Off for non-barbell sessions (the jump screen) — drops the bar-path +
  // orientation charts exactly like the real app.
  barbell?: boolean;
  // Forwarded to the real TechniqueTab so the phase detector dispatches by
  // exercise group (e.g. the CMJ detector for the jump screen).
  exerciseGroupID?: string;
  // Pins the shared crosshair at this fraction of the chart's width — the
  // hand-picked "hero" moment of the captured rep (see captured-sessions.ts).
  pinCrosshairFrac?: number;
}) {
  // Portal target for the Technique tab's real SetRepSelector (into the nav chip).
  const [navSlot, setNavSlot] = useState<HTMLDivElement | null>(null);
  const rootRef = useRef<HTMLDivElement>(null);

  // The bar path renders LAST in the real Technique tab. For the "Bar path
  // tracking" tile, pin the tab's scroll body to the bottom so the bar-path
  // charts are what the frame shows (the sticky value header stays on top). Kept
  // pinned as the charts finish sizing (they measure via ResizeObserver).
  // Non-barbell (jump) technique has no bar path — the tile frames the top
  // (values header + velocity chart), so no pin.
  useEffect(() => {
    if (tab !== "technique" || !barbell) return;
    const root = rootRef.current;
    const scroller = root?.querySelector<HTMLElement>(
      ".scrollbar-none.overflow-y-auto",
    );
    if (!scroller) return;
    const pin = () => {
      scroller.scrollTop = scroller.scrollHeight;
    };
    pin();
    const ro = new ResizeObserver(pin);
    ro.observe(scroller);
    for (const child of Array.from(scroller.children)) ro.observe(child);
    const timers = [80, 200, 400].map((ms) => setTimeout(pin, ms));
    return () => {
      ro.disconnect();
      timers.forEach(clearTimeout);
    };
  }, [tab, session, barbell]);

  // Pin the shared crosshair at the marketing-chosen moment: synthesize ONE
  // pointer press on the first chart plot at the given width fraction. It goes
  // through the chart's REAL pointer handler (which snaps to the nearest
  // sample and lifts the time via the shared onCrosshairChange), so the values
  // header, phase-strip highlight, and every synced chart react exactly as if
  // a user had scrubbed there. Two subtleties:
  // - The device mock scales the subtree, so the X offset is computed from
  //   `offsetWidth` (layout px — the chart's own coordinate space), not the
  //   visually scaled bounding rect.
  // - Dispatch exactly once per session: a second press at the same spot would
  //   land ON the crosshair handle, whose drag path calls setPointerCapture —
  //   which throws for synthetic events (no active pointer).
  useEffect(() => {
    if (tab !== "technique" || pinCrosshairFrac == null || !session) return;
    const root = rootRef.current;
    if (!root) return;
    let pinned = false;
    const pin = () => {
      if (pinned) return;
      const plot = root.querySelector<HTMLElement>(".touch-pan-y");
      if (!plot || plot.offsetWidth === 0) return;
      // Already pinned (e.g. an HMR/effect re-run with live crosshair state) —
      // the sample dots are the only circles in the plot. Never re-press: the
      // press would land ON the crosshair handle, whose drag path calls
      // setPointerCapture and throws for synthetic events.
      if (plot.querySelector("circle")) {
        pinned = true;
        return;
      }
      const rect = plot.getBoundingClientRect();
      const clientX = rect.left + plot.offsetWidth * pinCrosshairFrac;
      const clientY = rect.top + rect.height / 2;
      pinned = true;
      for (const type of ["pointerdown", "pointerup"] as const) {
        plot.dispatchEvent(
          new PointerEvent(type, { bubbles: true, clientX, clientY }),
        );
      }
    };
    const timers = [150, 400, 900].map((ms) => setTimeout(pin, ms));
    return () => timers.forEach(clearTimeout);
  }, [tab, session, pinCrosshairFrac]);

  return (
    <div
      ref={rootRef}
      className="relative flex h-full flex-col overflow-hidden bg-background"
      style={
        {
          "--insights-top": "84px",
          "--insights-bottom": "128px",
        } as CSSProperties
      }
    >
      {tab === "technique" ? (
        <TechniqueTab
          session={session ?? undefined}
          isBarbell={barbell}
          exerciseGroupID={exerciseGroupID}
          navSlot={navSlot}
        />
      ) : tab === "overview" ? (
        <OverviewGrid />
      ) : (
        <PerformanceTab session={session ?? undefined} />
      )}

      {/* Frosted "Insights" header, overlaying the scroll body (the tab reserves
          its height via --insights-top). */}
      <div className="absolute inset-x-0 top-0 z-30">
        <InsightsHeader showClose={showClose} />
      </div>

      {/* Bottom nav — the Technique selector portals into it; Performance shows
          just the pills. */}
      <InsightsBottomNav
        active={tab}
        slotRef={tab === "technique" ? setNavSlot : undefined}
      />
    </div>
  );
}

// ─── 1. Bar path — the app's Insights › Technique view ────────────────────────
// Driven by the REAL captured "Karo × Olympic Clean" session (see
// captured-sessions.ts); the crosshair pins at the picked peak-velocity moment.
function BarPathScreen({ target }: { target: DeviceTarget }) {
  const session = useCapturedCleanSession();
  if (isPad(target)) {
    return (
      <SplitInsights
        tab="technique"
        session={session}
        exerciseGroupID={EXERCISE_GROUP.weightlifting}
        overview={<CleanSetOverviewPane />}
        pinCrosshairFrac={CLEAN_PIN_FRAC}
        loadPillValue="85"
      />
    );
  }
  return (
    <InsightsPane
      tab="technique"
      session={session}
      showClose
      exerciseGroupID={EXERCISE_GROUP.weightlifting}
      pinCrosshairFrac={CLEAN_PIN_FRAC}
    />
  );
}

// ─── 1b. Jump analysis — the Technique view on a counter-movement jump ────────
// The same real TechniqueTab in jump mode, driven by the REAL captured
// "Marcel × Counter Movement Jump" session (see captured-sessions.ts): jump
// charts (Velocity & Acceleration, Power & Force — no bar path / orientation)
// with the real CMJ phase detector's unweighting → yielding → braking →
// propulsive → flight strip; the crosshair pins at the propulsion peak.
function JumpScreen({ target }: { target: DeviceTarget }) {
  const session = useCapturedCmjSession();
  if (isPad(target)) {
    return (
      <SplitInsights
        tab="technique"
        session={session}
        barbell={false}
        exerciseGroupID={EXERCISE_GROUP.jump}
        overview={<JumpSetOverviewPane />}
        pinCrosshairFrac={CMJ_PIN_FRAC}
        loadPillValue="+0"
        loadPillPrefix="BW"
      />
    );
  }
  return (
    <InsightsPane
      tab="technique"
      session={session}
      showClose
      barbell={false}
      exerciseGroupID={EXERCISE_GROUP.jump}
      pinCrosshairFrac={CMJ_PIN_FRAC}
    />
  );
}

// ─── 2. Velocity-based training — the app's active-tracking view ──────────────
// Kept as the reproduced set-tracking view for now (top bar + set list); the
// bar-path and insights screens are the ones on the real components.
function TrackingScreen({ target }: { target: DeviceTarget }) {
  const session = useSquatSession();
  // iPad: the app's active split — tracking (with the coaching cards) on the
  // left, live performance on the right.
  if (isPad(target)) {
    return (
      <div className="relative flex h-full bg-background">
        <StationSpine />
        <div className="relative flex w-[380px] shrink-0 flex-col border-r border-surface-stroke">
          <TrackingView />
        </div>
        <div className="relative min-w-0 flex-1">
          <InsightsPane tab="performance" session={session} showClose={false} />
        </div>
      </div>
    );
  }
  return <TrackingView />;
}

// The active-tracking view: the top bar + set overview, with the coaching cards
// (athlete-switch recommendation + load advice) floating at the bottom — the
// real `SwitchRecommendationCard` + `AdviceCard` the app stacks over the set list.
function TrackingView() {
  return (
    <div className="relative flex h-full flex-col bg-background">
      <AppTopBar />
      <div className="min-h-0 flex-1 overflow-hidden">
        <SetOverviewPane />
      </div>
      <div className="absolute inset-x-0 bottom-0 z-30 space-y-2 p-4">
        <SwitchRecommendationCard
          view={{
            kind: "switchAthlete",
            userID: "athlete-jordan",
            athleteName: "Jordan Miller",
            exerciseName: "Snatch",
          }}
          onSwitchAthlete={() => {}}
          onSwitchExercise={() => {}}
          onFinish={() => {}}
          onDismiss={() => {}}
        />
        <AdviceCard
          advice={{
            direction: "increase",
            loadText: "125",
            loadUnit: "kg",
            exertion: 2,
            repCount: null,
            scheduledPercent: null,
          }}
          onAccept={() => {}}
          onDismiss={() => {}}
        />
        {/* The real floating load-adjust pill — bottom-most of the stack,
            centered, showing the active set's load (like ExerciseDetailView). */}
        <div className="flex justify-center pt-1">
          <LoadAdjustBar
            value="120"
            unit="kg"
            onStep={() => {}}
            onCommit={() => {}}
          />
        </div>
      </div>
    </div>
  );
}

// ─── 3. Performance insights — the app's Insights › Performance view ──────────
function InsightsScreen({ target }: { target: DeviceTarget }) {
  const session = useSquatSession();
  // The Overview tab (session summary + positive trends) — a different part of
  // the app than the Technique bar-path insights.
  if (isPad(target)) {
    return (
      <SplitInsights tab="overview" session={session} loadPillValue="120" />
    );
  }
  return <InsightsPane tab="overview" session={session} showClose />;
}

// The Insights Overview tab reproduced with the REAL MetricStatCard: a grid of
// session-summary stats, each with a positive trend badge + rising sparkline
// (the live OverviewTab derives trends from server session history, which a
// fixture can't provide — so the trend series are supplied here).
function OverviewGrid() {
  return (
    <div
      className="scrollbar-none h-full overflow-y-auto"
      style={{ paddingTop: "calc(var(--insights-top, 5rem) + 0.5rem)" }}
    >
      <div className="mx-auto grid max-w-2xl grid-cols-2 gap-3 px-6 pb-[var(--insights-bottom,7rem)]">
        {OVERVIEW_STATS.map((s) => (
          <MetricStatCard
            key={s.label}
            label={s.label}
            value={s.value}
            unit={s.unit}
            trendPct={s.trendPct}
            series={s.series}
            color={metricCategoryColorHex(s.category)}
            highlightIndex={s.series.length - 1}
            variant="technique"
          />
        ))}
      </div>
    </div>
  );
}

// Session-summary stats for the Overview grid. `series` is oldest → newest;
// the last point is today's live value (ringed via highlightIndex). The trends
// are deliberately MIXED (strong gains, a flat mean, a couple of dips) so the
// grid reads like real training history rather than eight identical climbs —
// each `trendPct` matches its series' first → last change.
const OVERVIEW_STATS: {
  label: string;
  value: string;
  unit: string;
  trendPct: number;
  category: Parameters<typeof metricCategoryColorHex>[0];
  series: number[];
}[] = [
  { label: "1RM", value: "120", unit: "kg", trendPct: 7, category: "loading", series: [112, 115, 113, 118, 120] },
  { label: "Peak power", value: "3,522", unit: "W", trendPct: 9, category: "power", series: [3240, 3410, 3290, 3350, 3522] },
  { label: "Peak velocity", value: "1.97", unit: "m/s", trendPct: -3, category: "velocity", series: [2.03, 1.98, 2.04, 1.95, 1.97] },
  { label: "Peak force", value: "1,893", unit: "N", trendPct: 5, category: "force", series: [1810, 1770, 1852, 1830, 1893] },
  { label: "Mean velocity", value: "1.42", unit: "m/s", trendPct: 1, category: "velocity", series: [1.41, 1.45, 1.38, 1.43, 1.42] },
  { label: "Mean power", value: "2,240", unit: "W", trendPct: -4, category: "power", series: [2330, 2260, 2350, 2190, 2240] },
  { label: "Volume load", value: "4,860", unit: "kg", trendPct: 12, category: "loading", series: [4340, 4610, 4480, 4700, 4860] },
  { label: "Total reps", value: "42", unit: "", trendPct: -7, category: "time", series: [45, 41, 44, 40, 42] },
];

// ─── Shared iPad split-insights shell ─────────────────────────────────────────
// The app's wide-screen layout: the tracking / set overview on the LEFT (its own
// top bar), the inline Insights pane on the RIGHT (the real tab under the
// frosted "Insights" header + bottom-nav pills).
function SplitInsights({
  tab,
  session,
  barbell = true,
  exerciseGroupID,
  overview = <SetOverviewPane />,
  pinCrosshairFrac,
  loadPillValue,
  loadPillPrefix,
}: {
  tab: InsightsTabId;
  session: WorkoutSession | null;
  barbell?: boolean;
  exerciseGroupID?: string;
  // The left column's set-overview content — the clean pane by default, the
  // jump pane for the jump-analysis screen.
  overview?: ReactNode;
  pinCrosshairFrac?: number;
  // Active set's load for the floating load-adjust pill at the tracking
  // column's bottom (the real split always shows it during an active set).
  // Omit to hide; `loadPillPrefix` = "BW" for bodyweight-delta mode.
  loadPillValue?: string;
  loadPillPrefix?: string;
}) {
  return (
    <div className="relative flex h-full bg-background">
      <StationSpine />
      <div className="relative flex w-[380px] shrink-0 flex-col border-r border-surface-stroke">
        <AppTopBar />
        <div className="min-h-0 flex-1 overflow-hidden">{overview}</div>
        {loadPillValue != null && (
          <FloatingLoadPill value={loadPillValue} prefix={loadPillPrefix} />
        )}
      </div>
      <div className="relative min-w-0 flex-1">
        <InsightsPane
          tab={tab}
          session={session}
          showClose={false}
          barbell={barbell}
          exerciseGroupID={exerciseGroupID}
          pinCrosshairFrac={pinCrosshairFrac}
        />
      </div>
    </div>
  );
}

// The floating load-adjust pill anchored to a tracking column's bottom — the
// real `LoadAdjustBar`, centered like ExerciseDetailView's floating stack.
function FloatingLoadPill({
  value,
  prefix = "",
  unit = "kg",
}: {
  value: string;
  prefix?: string;
  unit?: string;
}) {
  return (
    <div className="absolute inset-x-0 bottom-6 z-30 flex justify-center px-3">
      <LoadAdjustBar
        value={value}
        prefix={prefix}
        unit={unit}
        onStep={() => {}}
        onCommit={() => {}}
      />
    </div>
  );
}

// ─── Video review — the app's full-screen set review ──────────────────────────
// The recorded-set review: a big square video with the whole-set scrub timeline,
// and (on wide screens) the best-reps / technique panel beside it. The real
// SetReviewView is viewport-media-query + getBoundingClientRect driven, which
// misbehaves inside the scaled device mock, so the outer layout is reproduced —
// the header, timeline, and charts inside are the real components. The `<video>`
// is a static image mock (a WebGL/video element won't rasterize in the capture).
// Timeline, playhead, charts and stats all come from the captured 35 kg clean
// warmup set (see captured-sessions.ts), pinned at the video still's moment.

// Everything the video-review tile derives from the captured set.
type VideoReviewData = {
  timelineReps: ReturnType<typeof setTimelineReps>;
  durationSec: number;
  playheadSec: number;
  crosshairTime: number;
  charts: ReturnType<typeof buildTechniqueCharts>;
  repCount: number;
  bestPowerText: string;
  bestRepLabel: string;
};

function useVideoReviewData(): VideoReviewData | null {
  const session = useCapturedVideoSession();
  // setTimelineReps + the load lookup resolve metrics from the store — rebuild
  // when the catalog lands (mutable global state, like useCleanSession).
  const version = useMetricsVersion();
  return useMemo(() => {
    if (!session) return null;
    const set = session.sets[0];
    const concentrics = set.reps.filter((r) => r.phase === "concentric");
    const rep = concentrics[0];
    if (!rep) return null;

    // The captured set has no video, so anchor a pseudo first-frame a couple
    // of seconds before the first motion — the timeline then reads exactly
    // like a real recording (lead-in, rep blocks, tail).
    const LEAD_SEC = 2.5;
    const TAIL_SEC = 2;
    const probe = setTimelineReps(set, 1);
    if (probe.length === 0) return null;
    const minStart = Math.min(
      ...probe.flatMap((r) => r.segments.map((s) => s.startSec)),
    );
    const shift = minStart - LEAD_SEC;
    const timelineReps = probe.map((r) => ({
      ...r,
      segments: r.segments.map((s) => ({
        ...s,
        startSec: s.startSec - shift,
        endSec: s.endSec - shift,
      })),
    }));
    const maxEnd = Math.max(
      ...timelineReps.flatMap((r) => r.segments.map((s) => s.endSec)),
    );
    const durationSec = maxEnd + TAIL_SEC;

    const loadMeasurement = set.measurements.find(
      (m) => getMetric(m.metricID)?.loading,
    );
    const loadKg = loadMeasurement ? displayValue(loadMeasurement) : undefined;
    const charts = buildTechniqueCharts(
      chartableReps(session),
      rep.id,
      session,
      loadKg,
      EXERCISE_GROUP.weightlifting,
    );
    const domain = charts.timeDomain;
    const crosshairTime = domain
      ? domain.min + VIDEO_PIN_FRAC * (domain.max - domain.min)
      : 0;
    // The pinned moment in video time: the rep's concentric start plus the
    // crosshair's offset into the chart domain.
    const repBlock = timelineReps.find((r) => r.id === rep.id);
    const conStart =
      repBlock?.segments.find((s) => s.phase === "concentric")?.startSec ??
      LEAD_SEC;
    const playheadSec = conStart + (crosshairTime - (domain?.min ?? 0));

    // Best rep by peak power (the same (az+1)·v·G·load the charts plot).
    const G = 9.81;
    let bestIdx = 0;
    let bestPower = 0;
    concentrics.forEach((r, i) => {
      for (const p of r.rawData ?? []) {
        if (p.kind !== "strength") continue;
        const w = (p.data.a.z + 1) * p.data.v * G * (loadKg ?? 1);
        if (w > bestPower) {
          bestPower = w;
          bestIdx = i;
        }
      }
    });

    return {
      timelineReps,
      durationSec,
      playheadSec,
      crosshairTime,
      charts,
      repCount: concentrics.length,
      bestPowerText: Math.round(bestPower).toLocaleString("en-US"),
      bestRepLabel: `Rep ${bestIdx + 1}`,
    };
    // `version` is a real dependency the linter can't see (metrics store state).
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [session, version]);
}

function VideoReviewScreen({ target }: { target: DeviceTarget }) {
  const noop = () => {};
  const review = useVideoReviewData();
  const header = <SetReviewHeader onClose={noop} />;
  const timeline = (
    // Every tile shows the filmstrip ("scroll") timeline the app uses on touch
    // devices, with the pad's wider window — mirrors SetReviewOverlay's device
    // gating. The export preserves the strip's scroll offset
    // (restoreScrollPosition in export.ts), so the JPEG shows the playhead
    // moment, not a strip reset to 0:00.
    <SetTimeline
      durationSec={review?.durationSec ?? SAMPLE_DURATION}
      reps={review?.timelineReps ?? SAMPLE_REPS}
      playheadSec={review?.playheadSec ?? 18.6}
      onScrub={noop}
      mode="scroll"
      windowSec={isPad(target) ? 30 : 15}
    />
  );

  if (isPad(target)) {
    return (
      <div className="flex h-full flex-col bg-background">
        {header}
        <div className="flex min-h-0 flex-1 gap-6 px-4 pb-6">
          <div className="flex min-w-0 flex-1 flex-col gap-3">
            <div className="flex min-h-0 flex-1 items-center justify-center">
              <div className="aspect-square h-full max-w-full overflow-hidden rounded-2xl bg-black">
                <VideoMock review={review} />
              </div>
            </div>
            <div className="w-full">{timeline}</div>
          </div>
          <aside className="scrollbar-none w-96 shrink-0 overflow-hidden">
            <SetReviewPanel review={review} />
          </aside>
        </div>
      </div>
    );
  }

  return (
    <div className="flex h-full flex-col bg-background">
      {header}
      <div className="scrollbar-none flex-1 overflow-hidden px-4 pb-6">
        <div className="flex flex-col gap-3">
          <div className="aspect-square w-full overflow-hidden rounded-2xl bg-black">
            <VideoMock review={review} />
          </div>
          {timeline}
          <SetReviewPanel review={review} />
        </div>
      </div>
    </div>
  );
}

// The video still (`assets/video-mock.png`, any aspect — object-cover cropped to
// the square) + a play affordance and a scrub bar so it reads as the review
// player. The scrub position/clock mirror the captured set's pinned playhead.
function VideoMock({ review }: { review: VideoReviewData | null }) {
  const clock = (s: number) =>
    `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
  const progress = review
    ? Math.min(1, Math.max(0, review.playheadSec / review.durationSec))
    : 0.46;
  const pct = `${(progress * 100).toFixed(1)}%`;
  const current = review ? clock(review.playheadSec) : "0:18";
  const total = review ? clock(review.durationSec) : "0:34";
  return (
    <div className="relative h-full w-full bg-black">
      {/* eslint-disable-next-line @next/next/no-img-element */}
      <img
        src={videoMock.src}
        alt=""
        aria-hidden
        className="h-full w-full object-cover"
      />
      <div className="absolute inset-0 flex items-center justify-center">
        <span className="flex size-16 items-center justify-center rounded-full bg-black/45 text-white backdrop-blur">
          <svg viewBox="0 0 24 24" fill="currentColor" className="size-8">
            <path d="M8 5v14l11-7z" />
          </svg>
        </span>
      </div>
      <div className="absolute inset-x-0 bottom-0 p-4">
        <div className="flex items-center gap-3 rounded-full bg-black/40 px-4 py-2 backdrop-blur">
          <span className="text-xs font-medium tabular-nums text-white/90">{current}</span>
          <div className="relative h-1 flex-1 rounded-full bg-white/25">
            <div
              className="absolute inset-y-0 left-0 rounded-full bg-white"
              style={{ width: pct }}
            />
            <div
              className="absolute top-1/2 size-3 -translate-y-1/2 rounded-full bg-white"
              style={{ left: pct }}
            />
          </div>
          <span className="text-xs font-medium tabular-nums text-white/60">{total}</span>
        </div>
      </div>
    </div>
  );
}

// The best-reps / technique panel beside the video — the set's headline stats
// plus the captured rep's real bar-path (Sagittal + Frontal) and Velocity &
// Acceleration charts, crosshair pinned at the video still's moment.
function SetReviewPanel({ review }: { review: VideoReviewData | null }) {
  const noop = () => {};
  const power = metricCategoryColorHex("power");
  const force = metricCategoryColorHex("force");
  const velocity = metricCategoryColorHex("velocity");
  const spacial = metricCategoryColorHex("spacial");
  const emptySecondary = { label: "", color: "transparent", data: [] };
  if (!review) return null;
  const { charts, crosshairTime } = review;
  const vel = charts.metricCharts.find((c) => c.key === "velocity");
  const acc = charts.metricCharts.find((c) => c.key === "acceleration");
  return (
    <div className="flex flex-col gap-4">
      <div className="grid grid-cols-2 gap-3">
        <ReviewStat label="Reps" value={String(review.repCount)} />
        <ReviewStat
          label="Best rep"
          value={review.bestPowerText}
          unit="W"
          sub={review.bestRepLabel}
          color={power}
        />
      </div>
      {charts.barPath && (
        <div className="flex gap-3">
          <div className="min-w-0 flex-1">
            <TechniqueTrajectoryChart
              title="Sagittal"
              crosshairTime={crosshairTime}
              onCrosshairChange={noop}
              minXExtent={{ min: -5, max: 5 }}
              phases={charts.phases}
              forceColor={force}
              primary={{
                label: "Sagittal",
                color: spacial,
                data: charts.barPath.sagittalData,
              }}
              secondary={emptySecondary}
              background={<SagittalBodyBackdrop />}
            />
          </div>
          <div className="min-w-0 flex-1">
            <TechniqueTrajectoryChart
              title="Frontal"
              crosshairTime={crosshairTime}
              onCrosshairChange={noop}
              minXExtent={{ min: -5, max: 5 }}
              phases={charts.phases}
              primary={{
                label: "Frontal",
                color: spacialAltColorHex(),
                data: charts.barPath.frontalData,
              }}
              secondary={emptySecondary}
              background={<BackViewBodyBackdrop />}
            />
          </div>
        </div>
      )}
      {vel && acc && (
        <TechniqueLineChart
          title="Velocity & Acceleration"
          leftSeries={{
            label: "Velocity",
            unit: vel.rawData[0].unit,
            data: vel.rawData[0].series[0].data,
            color: velocity,
            fractionDigits: 2,
          }}
          rightSeries={{
            label: "Acceleration",
            unit: acc.rawData[0].unit,
            data: acc.rawData[0].series[0].data,
            color: force,
            fractionDigits: 1,
          }}
          crosshairTime={crosshairTime}
          onCrosshairChange={noop}
          phases={charts.phases}
          plotHeight={150}
        />
      )}
    </div>
  );
}

function ReviewStat({
  label,
  value,
  unit,
  sub,
  color,
}: {
  label: string;
  value: string;
  unit?: string;
  sub?: string;
  color?: string;
}) {
  return (
    <div className="rounded-2xl border border-surface-stroke bg-surface-base p-4">
      <h3 className="heading-section">{label}</h3>
      <div
        className="mt-1 flex items-baseline gap-1"
        style={{ color: color ?? "var(--foreground-default)" }}
      >
        <span className="stat-large-digits">{value}</span>
        {unit && <span className="stat-small">{unit}</span>}
      </div>
      {sub && <span className="body-small mt-0.5 text-foreground-muted">{sub}</span>}
    </div>
  );
}

// ─── Multi-station (tablet only) ──────────────────────────────────────────────
// The app's wide-screen station shell: a pinned left spine + a rail of station
// columns, each in its own state. Reuses the real `StationRail` / `StationColumn`
// (pure) and the real `RequestStationSlotOverlay`; the spine + station headers +
// workouts list are reproduced (the live shell is `page.tsx`, wired to the
// stations/workouts/sensor stores). Station 1 trains, station 2 lists workouts,
// station 3 is locked behind the request-slot dialog.
function MultiStationScreen() {
  return (
    <div className="flex h-full bg-background">
      <StationSpine />
      <StationRail>
        {/* Station 1 — in training. */}
        <StationColumn ariaLabel="Station 1">
          <div className="relative flex h-full flex-col">
            <AppTopBar />
            <div className="min-h-0 flex-1 overflow-hidden">
              <SetOverviewPane />
            </div>
            <FloatingLoadPill value="120" />
          </div>
        </StationColumn>

        {/* Station 2 — open, listing today's workouts. */}
        <StationColumn ariaLabel="Station 2">
          <div className="flex h-full flex-col">
            <StationHeader title="Station 2" />
            <div className="min-h-0 flex-1 overflow-hidden">
              <WorkoutsListPane />
            </div>
          </div>
        </StationColumn>

        {/* Station 3 — locked (device at capacity), request-slot dialog. */}
        <StationColumn ariaLabel="Station 3">
          <div className="relative flex h-full flex-col">
            <StationHeader title="Station 3" />
            <div className="min-h-0 flex-1 overflow-hidden" />
            <RequestStationSlotOverlay
              title="Station at capacity"
              message="This device is already recording the most stations it can. Request another slot to start training here."
              onRequest={() => {}}
              requesting={false}
              secondaryLabel="Feedback training"
              onSecondary={() => {}}
            />
          </div>
        </StationColumn>
      </StationRail>
    </div>
  );
}

// Reproduced collapsed station spine — the soft platinum-green rail with the
// Enode mark (real gradient class + mark), a sidebar affordance, and the REAL
// icon-only sidebar action rows (`SidebarActionsView` is pure — the dev
// gallery renders it the same way; the state here is a healthy idle device:
// workouts downloaded, one open station, nothing pending).
function StationSpine() {
  const noop = () => {};
  return (
    <div
      className={`flex h-full w-16 shrink-0 flex-col items-center gap-2 pb-3 pt-3 ${RAIL_SURFACE_CLASS}`}
    >
      <div className="flex h-12 shrink-0 items-center justify-center">
        <EnodeMark className="size-7" />
      </div>
      <div className="h-px w-6 shrink-0 bg-current/20" />
      <span className="flex size-11 shrink-0 items-center justify-center rounded-full">
        <SidebarIcon className="size-6" />
      </span>
      <div className="h-px w-6 shrink-0 bg-current/20" />
      <div className="mt-1 flex flex-col items-center gap-1">
        <SidebarActionsView
          state={{
            collapsed: true,
            loading: false,
            hasWorkouts: true,
            online: true,
            offlineReady: true,
            downloading: false,
            failed: false,
            pendingCount: 0,
            pendingVideoCount: 0,
            storageNearFull: false,
            storageUsage: null,
            openStationCount: 1,
            stationLimit: 3,
            canOpenStation: true,
            overflowCount: 0,
          }}
          callbacks={{
            onOpenProfile: noop,
            onOpenSettings: noop,
            onOpenPortal: noop,
            onRefresh: noop,
            onOpenStation: noop,
            onDownload: noop,
            onRetryUploads: noop,
            onCloseHidden: noop,
          }}
        />
      </div>
    </div>
  );
}

// A frosted station-column header (title + close), matching the empty-station
// header in the real shell.
function StationHeader({ title }: { title: string }) {
  return (
    <header
      className={`relative ${HEADER_MIN_HEIGHT} ${headerEdgeClass(HEADER_BLUR)} flex shrink-0 items-center gap-4 px-4 pb-4 pt-4`}
    >
      <span className="flex size-10 shrink-0 items-center justify-center rounded-full bg-surface-base text-foreground-default shadow ring-1 ring-surface-stroke-muted">
        <XIcon className="size-5" />
      </span>
      <h2 className="heading-large min-w-0 truncate">{title}</h2>
    </header>
  );
}

// Reproduced "For Today" workouts list for the open station (the real
// WorkoutsTodayList resolves exercise names via the text-content store, which a
// fixture can't populate — so the cards are reproduced with clean names).
function WorkoutsListPane() {
  const workouts = [
    { name: "Olympic Lifting", time: "09:00", exercises: ["Clean", "Front Squat", "Snatch"] },
    { name: "Lower Strength", time: "11:30", exercises: ["Back Squat", "Romanian Deadlift", "Hip Thrust"] },
    { name: "Conditioning", time: "16:00", exercises: ["Push Press", "Row"] },
  ];
  return (
    <div className="scrollbar-none h-full overflow-hidden px-5 pt-4">
      <div>
        <h2 className="heading-large">For Today</h2>
        <span className="body-small text-foreground-muted">Monday, July 19</span>
      </div>
      <div className="mt-5 space-y-3">
        {workouts.map((w, i) => (
          <div
            key={w.name}
            className={`rounded-2xl border bg-surface-base p-4 ${
              i === 0
                ? "border-foreground-red-orange/40 shadow-prominent"
                : "border-surface-stroke"
            }`}
          >
            <div className="flex items-start justify-between gap-2">
              <h3 className="heading-normal">{w.name}</h3>
              <span className="body-small shrink-0 text-foreground-muted">{w.time}</span>
            </div>
            <ol className="mt-2 space-y-0.5">
              {w.exercises.map((ex, j) => (
                <li key={ex} className="body-normal flex items-baseline gap-1.5 text-foreground-default">
                  <span className="stat-small w-4 shrink-0 text-right text-foreground-muted">
                    {j + 1}
                  </span>
                  <span className="truncate">{ex}</span>
                </li>
              ))}
            </ol>
          </div>
        ))}
      </div>
    </div>
  );
}

// ─── Set overview pane (reproduced) ───────────────────────────────────────────
// The split's left column: the sticky session-stats card over the auto-tracked
// set list with per-rep focus-metric MetricBars. Reproduced (the real SetRow
// needs the exercise's focus-metric config); low-drift, simple cards. Defaults
// to the clean session; the jump screen passes its own content.
function SetOverviewPane({
  title = "Back Squat",
  subtitle = "Barbell",
  focusLabel = "1RM",
  focusValue = "120",
  focusUnit = "kg",
  focusMetricLabel = "›› Peak velocity",
  focusMetricUnit = "[M/S]",
  rows = SET_TRACKING_ROWS,
  barColor = SET_TRACKING_COLOR,
  barMax = SET_TRACKING_SESSION_MAX,
}: {
  title?: string;
  subtitle?: string;
  focusLabel?: string;
  focusValue?: string;
  focusUnit?: string;
  focusMetricLabel?: string;
  focusMetricUnit?: string;
  rows?: MockSetRow[];
  barColor?: string;
  barMax?: number;
}) {
  return (
    <div className="scrollbar-none h-full overflow-hidden px-4 pt-4">
      <div className="mx-auto flex max-w-2xl flex-col gap-4">
        <SessionStats
          header={
            <div className="flex items-center justify-between gap-3 px-1">
              <div className="min-w-0">
                <h2 className="heading-large truncate">{title}</h2>
                <p className="body-small truncate text-foreground-muted">{subtitle}</p>
              </div>
              <AthleteAvatar />
            </div>
          }
          focusLabel={focusLabel}
          focusValue={focusValue}
          focusUnit={focusUnit}
          restSinceMs={null}
          targetRestSec={150}
        />

        <div className="flex items-center gap-2 px-1">
          <span className="stat-small font-semibold uppercase tracking-wide text-foreground-red-orange">
            {focusMetricLabel}
          </span>
          <span className="body-small uppercase text-foreground-muted">{focusMetricUnit}</span>
        </div>

        <div className="space-y-1.5">
          {rows.map((row, i) => (
            <SetTrackingCard
              key={row.id}
              row={row}
              color={barColor}
              max={barMax}
              isLast={i === rows.length - 1}
            />
          ))}
        </div>
      </div>
    </div>
  );
}

// The bar-path split's left column, aligned with the captured Karo clean the
// right-hand charts show (85 kg working sets, ~2.0 m/s peak velocity).
function CleanSetOverviewPane() {
  return (
    <SetOverviewPane
      title="Olympic Clean"
      subtitle="Barbell"
      focusLabel="1RM"
      focusValue="102"
      focusUnit="kg"
      rows={KARO_TRACKING_ROWS}
      barMax={2.2}
    />
  );
}

// Set rows matching the captured clean: 85 kg across the working sets, peak
// velocity around the real rep's 2.03 m/s.
const KARO_TRACKING_ROWS: MockSetRow[] = [
  {
    id: "k1",
    title: "Set 1",
    state: "completed",
    load: "85",
    loadUnit: "kg",
    focusText: "2.03 … 1.98 M/S ↓2%",
    barValues: [2.03, 1.98],
    marker: { kind: "line", value: 2.0 },
  },
  {
    id: "k2",
    title: "Set 2",
    state: "completed",
    load: "85",
    loadUnit: "kg",
    focusText: "1.99 … 1.96 M/S ↓2%",
    barValues: [1.99, 1.96],
    marker: { kind: "line", value: 1.98 },
  },
  // The next, not-yet-performed set carries the Complete button.
  {
    id: "k3",
    title: "Set 3",
    state: "active",
    load: "85",
    loadUnit: "kg",
    focusText: "Target ≥ 1.9 M/S",
    barValues: [],
    marker: null,
  },
  {
    id: "k4",
    title: "Set 4",
    state: "template",
    load: "85",
    loadUnit: "kg",
    focusText: "Target ≥ 1.9 M/S",
    barValues: [],
    marker: null,
  },
];

// The jump-analysis split's left column: the CMJ session with a jump-height
// focus (cm) over bodyweight sets — hero value = the captured session's best.
function JumpSetOverviewPane() {
  return (
    <SetOverviewPane
      title="Counter Movement Jump"
      subtitle="Bodyweight"
      focusLabel="Best jump"
      focusValue="44.3"
      focusUnit="cm"
      focusMetricLabel="›› Jump height"
      focusMetricUnit="[CM]"
      rows={CMJ_TRACKING_ROWS}
      barColor={CMJ_TRACKING_COLOR}
      barMax={CMJ_TRACKING_SESSION_MAX}
    />
  );
}

function AthleteAvatar() {
  return (
    <span className="flex size-11 shrink-0 items-center justify-center rounded-full bg-foreground-platinum-green text-sm font-semibold text-white">
      CI
    </span>
  );
}

function SetTrackingCard({
  row,
  color = SET_TRACKING_COLOR,
  max = SET_TRACKING_SESSION_MAX,
  isLast = false,
}: {
  row: MockSetRow;
  color?: string;
  max?: number;
  isLast?: boolean;
}) {
  const isActive = row.state === "active";
  const isTemplate = row.state === "template";
  const bars = row.barValues.map((value, i) => ({ id: `${row.id}-${i}`, value }));
  const cardBase = "flex-1 rounded-2xl border border-surface-stroke p-2";
  const cardClass = isActive
    ? `${cardBase} bg-surface-base shadow-prominent`
    : isTemplate
      ? `${cardBase} bg-surface-base opacity-50`
      : `${cardBase} bg-surface-base`;

  return (
    <div className="flex gap-3">
      <TrackMarker state={row.state} isLast={isLast} />
      <div className={cardClass}>
        <div className="flex items-start justify-between gap-4">
          <div className="min-w-0 flex-1">
            <h4 className="heading-normal">{row.title}</h4>
            <p className="body-small mt-0.5 text-foreground-muted">{row.focusText}</p>
          </div>
          <p className="heading-normal flex items-baseline gap-1 leading-tight">
            {row.load}
            <span className="stat-small uppercase text-foreground-muted">{row.loadUnit}</span>
          </p>
        </div>
        {bars.length > 0 && (
          <div className="mt-2">
            <MetricBar bars={bars} marker={row.marker} max={max} color={color} />
          </div>
        )}
        {isActive && (
          <div className="mt-3 flex justify-end">
            <span className="clickable-small rounded-full bg-foreground-red-orange px-4 py-2 text-white shadow">
              Complete
            </span>
          </div>
        )}
      </div>
    </div>
  );
}

// Timeline node + connector, mirroring the real set list's `Marker`: the
// connector runs down to the next node — bright platinum green out of a
// completed set, muted otherwise — spanning the 6px row gap (bottom-[-20px]
// assumes `space-y-1.5`); the last node draws none.
function TrackMarker({
  state,
  isLast,
}: {
  state: MockSetRow["state"];
  isLast: boolean;
}) {
  const lineColor =
    state === "completed"
      ? "bg-foreground-platinum-green-bright"
      : "bg-foreground-most-muted";
  return (
    <div className="relative flex w-5 shrink-0 flex-col items-center self-stretch">
      {!isLast && (
        <span
          aria-hidden
          className={`absolute left-1/2 top-[36px] bottom-[-20px] w-0.5 -translate-x-1/2 ${lineColor}`}
        />
      )}
      <TrackMarkerDot state={state} />
    </div>
  );
}

function TrackMarkerDot({ state }: { state: MockSetRow["state"] }) {
  if (state === "completed") {
    return (
      <span className="relative mt-4 flex size-[18px] items-center justify-center rounded-full bg-foreground-platinum-green">
        <svg viewBox="0 0 24 24" fill="none" className="size-[11px]">
          <path
            d="M5 13l4 4L19 7"
            stroke="var(--background)"
            strokeWidth={3}
            strokeLinecap="round"
            strokeLinejoin="round"
          />
        </svg>
      </span>
    );
  }
  if (state === "active") {
    return (
      <span className="relative mt-4 flex size-[18px] items-center justify-center rounded-full border-2 border-foreground-red-orange">
        <span className="block size-2 rounded-full bg-foreground-red-orange" />
      </span>
    );
  }
  return (
    <span className="relative mt-4 size-[18px] rounded-full border-2 border-foreground-most-muted bg-background" />
  );
}
