{/* HEADER */}
@@ -211,6 +232,7 @@ export default function GameCard({ game: g, onAddParlay, onOpen, preferredBooks
props={ps.props}
variant="compact"
onPlayerClick={() => { window.location.href = playerHref(ps.player, g.sport); }}
+ {...stripHandlers(ps)}
/>
))}
diff --git a/web/src/components/vyndr/ParlayPanel.tsx b/web/src/components/vyndr/ParlayPanel.tsx
new file mode 100644
index 0000000..31831ba
--- /dev/null
+++ b/web/src/components/vyndr/ParlayPanel.tsx
@@ -0,0 +1,133 @@
+'use client';
+
+import { useEffect } from 'react';
+import { useParlay } from '@/contexts/ParlayContext';
+import { useAuth } from '@/contexts/AuthContext';
+import GradeBadge from '@/components/vyndr/GradeBadge';
+import ArchetypeBadge from '@/components/vyndr/ArchetypeBadge';
+
+/**
+ * ParlayPanel (Session 50) — the Parlay Lab. A bottom slide-up that shows the
+ * selected legs, the correlation-aware combined grade, the correlation warning,
+ * and the estimated payout. Tier-gated: free 2 legs (payout blurred), Analyst 4,
+ * Desk 6. A floating badge (bottom-right) opens it when legs are pending.
+ * Mounted globally in the layout — visible across pages.
+ */
+
+const TIER_MAX = { free: 2, analyst: 4, desk: 6 } as const;
+function tierMaxLegs(tier: string): number {
+ return TIER_MAX[(tier as keyof typeof TIER_MAX)] ?? 2;
+}
+
+export default function ParlayPanel() {
+ const { legs, isOpen, toggle, close, removeLeg, clear, combined, correlation, payout, grading, maxLegs, setMaxLegs } = useParlay();
+ const { tier } = useAuth();
+ const fullLab = tier === 'desk' || tier === 'analyst';
+
+ // Sync the leg cap to the user's tier.
+ useEffect(() => { setMaxLegs(tierMaxLegs(tier || 'free')); }, [tier, setMaxLegs]);
+
+ if (legs.length === 0) return null;
+
+ // Floating trigger when closed.
+ if (!isOpen) {
+ return (
+
+
+
+ PARLAY LAB ({legs.length})
+
+
+
+
+ {/* Legs */}
+
+ {legs.map((l) => (
+
+ {l.archetype &&
}
+
{l.player}
+
{l.stat} {l.direction === 'under' ? 'U' : 'O'}{l.line}
+
+
+
+ ))}
+
+
+ {legs.length >= maxLegs && (
+
+ {fullLab ? `Max ${maxLegs} legs on your plan.` : 'Free tier caps at 2 legs — upgrade to Desk for 6.'}
+
+ )}
+
+ {/* Correlation warning */}
+ {correlation?.warning && (
+
{correlation.warning}
+ )}
+
+ {/* Metrics */}
+ {legs.length >= 2 ? (
+
+
+
COMBINED GRADE
+ {combined ?
:
{grading ? '…' : '—'}}
+
+
+
CORRELATION
+
0.3 ? 'var(--amber)' : 'var(--g-a)' }}>
+ {correlation ? `${correlation.avg > 0.5 ? 'High' : correlation.avg > 0.2 ? 'Medium' : 'Low'} (${correlation.avg.toFixed(2)})` : '—'}
+
+
+
+
EST. PAYOUT · $10
+ {fullLab ? (
+
+ {payout ? `$${payout.amount.toFixed(2)} (${payout.multiplier.toFixed(2)}x)` : (grading ? '…' : '—')}
+
+ ) : (
+
+
$38.50 (3.85x)
+
+
+ )}
+
+
+ ) : (
+
Add another leg to see the combined grade.
+ )}
+
+
+
+ );
+}
diff --git a/web/src/components/vyndr/StatStrip.tsx b/web/src/components/vyndr/StatStrip.tsx
index f8945b6..4a371f2 100644
--- a/web/src/components/vyndr/StatStrip.tsx
+++ b/web/src/components/vyndr/StatStrip.tsx
@@ -31,6 +31,9 @@ interface StatStripProps {
meta?: string; // expanded: "ATL · 3B · #27"
variant?: 'compact' | 'expanded';
onPlayerClick?: () => void;
+ // Session 50 — Parlay Lab "+" per graded prop (wired by the card via useParlay).
+ onAddLeg?: (p: StripProp) => void;
+ isLegActive?: (p: StripProp) => boolean;
}
const Sep = ({ ch = '|' }: { ch?: string }) => (
@@ -54,7 +57,31 @@ export default function StatStrip({
meta,
variant = 'compact',
onPlayerClick,
+ onAddLeg,
+ isLegActive,
}: StatStripProps) {
+ const ParlayBtn = ({ p }: { p: StripProp }) => {
+ if (!onAddLeg || !p.grade) return null;
+ const active = isLegActive ? isLegActive(p) : false;
+ return (
+