tiena2cva commited on
Commit
e213e4d
·
1 Parent(s): bb2c905

feat(web): refine movement review dashboard

Browse files
Files changed (3) hide show
  1. web/app.js +250 -116
  2. web/index.html +2 -2
  3. web/styles.css +256 -0
web/app.js CHANGED
@@ -503,6 +503,83 @@ function BriefTile({ eyebrow, title, body, tone = "" }) {
503
  );
504
  }
505
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  function RepTimeline({
507
  report,
508
  compact = false,
@@ -688,7 +765,7 @@ function SummaryTab({ result }) {
688
  )
689
  : h("p", { className: "empty-copy" }, "No movement scores available."),
690
  ),
691
- h(RepTimeline, { report, compact: true }),
692
  h(
693
  "div",
694
  { className: "note-grid report-notes" },
@@ -811,11 +888,11 @@ function MetricsTab({ result }) {
811
  );
812
  }
813
 
814
- function RepDetailPanel({ report, rep, result, playbackTime }) {
815
  if (!rep) {
816
  return h(
817
  "aside",
818
- { className: "rep-detail-panel" },
819
  h("span", { className: "rep-detail-kicker" }, "Current rep"),
820
  h("strong", null, "No rep selected"),
821
  h("p", null, "Play the video or click a rep segment to inspect its metrics."),
@@ -862,6 +939,112 @@ function RepDetailPanel({ report, rep, result, playbackTime }) {
862
  const issueText = repIssues.length
863
  ? `${repIssues.length} issue marker${repIssues.length === 1 ? "" : "s"} attached to this rep.`
864
  : "No issue marker is attached, so use the lowest metric as the review cue.";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
865
 
866
  return h(
867
  "aside",
@@ -1000,7 +1183,7 @@ function RepDetailPanel({ report, rep, result, playbackTime }) {
1000
  );
1001
  }
1002
 
1003
- function RepsTab({ result, videoSrc }) {
1004
  const [playbackTime, setPlaybackTime] = useState(0);
1005
  const [selectedRepId, setSelectedRepId] = useState(null);
1006
  const replayVideoRef = useRef(null);
@@ -1010,16 +1193,8 @@ function RepsTab({ result, videoSrc }) {
1010
  setSelectedRepId(null);
1011
  }, [result?.run_id, videoSrc]);
1012
 
1013
- if (!result)
1014
- return h(
1015
- "section",
1016
- { className: "summary" },
1017
- h("h2", null, "Rep review"),
1018
- h("p", null, "Rep segments appear after analysis."),
1019
- );
1020
  const report = result.report;
1021
  const reps = report.reps?.reps || [];
1022
- const analysisById = repAnalysisById(report);
1023
  const timedRep = repAtTime(report, playbackTime);
1024
  const selectedRep = reps.find((rep) => rep.rep_id === selectedRepId) || null;
1025
  const activeRep = timedRep || selectedRep || reps[0] || null;
@@ -1035,17 +1210,11 @@ function RepsTab({ result, videoSrc }) {
1035
  }
1036
 
1037
  return h(
1038
- "section",
1039
- { className: "summary timeline-panel" },
1040
- h("h2", null, "Rep replay timeline"),
1041
- h(
1042
- "p",
1043
- null,
1044
- "Play the set to follow each rep live, or click a rep segment to jump the video to that moment.",
1045
- ),
1046
  h(
1047
  "div",
1048
- { className: "replay-layout" },
1049
  h(
1050
  "div",
1051
  { className: "replay-video-card" },
@@ -1074,14 +1243,48 @@ function RepsTab({ result, videoSrc }) {
1074
  h("strong", null, "No replay video available"),
1075
  h("span", null, "Upload a clip or use a run with an annotated video to sync the timeline."),
1076
  ),
1077
- h(RepTimeline, {
1078
- report,
1079
- activeRepId: activeRep?.rep_id || null,
1080
- playbackTime,
1081
- onRepSelect: playFromRep,
1082
- }),
1083
  ),
1084
- h(RepDetailPanel, { report, rep: activeRep, result, playbackTime }),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1085
  ),
1086
  reps.length
1087
  ? h(
@@ -1311,74 +1514,6 @@ function IssuesTab({ result }) {
1311
  );
1312
  }
1313
 
1314
- function CoachTab({ result }) {
1315
- if (!result)
1316
- return h(
1317
- "section",
1318
- { className: "summary" },
1319
- h("h2", null, "Coach summary"),
1320
- h("p", null, "Coach notes appear after analysis."),
1321
- );
1322
- const summary = result.report.coach_summary;
1323
- const coachArtifacts = result.report.artifacts || {};
1324
- const coachSource = coachArtifacts.coach_summary_source || "";
1325
- const coachProvider = coachArtifacts.coach_summary_provider || "n/a";
1326
- const coachModel = coachArtifacts.coach_summary_model || "n/a";
1327
- const verifierBypassed = Boolean(
1328
- coachArtifacts.coach_summary_verifier_bypassed,
1329
- );
1330
- const isFallback = coachSource.startsWith("fallback");
1331
- const cueNow = summary.top_fixes?.[0] || summary.next_session_plan?.[0] || summary.summary;
1332
- return h(
1333
- "section",
1334
- { className: "summary coach-panel" },
1335
- h("h2", null, "Coach plan"),
1336
- h("p", null, summary.summary),
1337
- h(
1338
- "div",
1339
- { className: "coach-meta pill-row" },
1340
- [
1341
- ["Provider", coachProvider],
1342
- ["Model", coachModel],
1343
- ["Source", coachSource || "n/a"],
1344
- ].map(([name, value]) => metaChip(`${name}: ${value}`, "", name)),
1345
- ),
1346
- isFallback
1347
- ? h("p", { className: "system-note" }, "A conservative fallback summary was used because the generated summary was unavailable or did not pass verification.")
1348
- : null,
1349
- verifierBypassed
1350
- ? h("p", { className: "system-note warning" }, "Verifier bypass is active, so the model summary is shown even though verification did not pass.")
1351
- : null,
1352
- h(
1353
- "div",
1354
- { className: "coach-plan-grid" },
1355
- h(BriefTile, {
1356
- eyebrow: "Cue now",
1357
- title: "First rep of the next set",
1358
- body: cueNow,
1359
- tone: "volt",
1360
- }),
1361
- h(NoteList, { title: "Keep", items: summary.what_looked_good, className: "coach-note" }),
1362
- h(NoteList, { title: "Fix first", items: summary.top_fixes, className: "coach-note" }),
1363
- h(NoteList, {
1364
- title: "Variation vs issue",
1365
- items: summary.valid_variation_vs_issue,
1366
- className: "coach-note",
1367
- }),
1368
- h(NoteList, {
1369
- title: "Next session",
1370
- items: summary.next_session_plan,
1371
- className: "coach-note",
1372
- }),
1373
- h(NoteList, {
1374
- title: "Confidence notes",
1375
- items: summary.confidence_notes,
1376
- className: "coach-note muted",
1377
- }),
1378
- ),
1379
- );
1380
- }
1381
-
1382
  function JsonTab({ result }) {
1383
  return h(
1384
  "pre",
@@ -1422,20 +1557,18 @@ function ArtifactsTab({ result }) {
1422
 
1423
  const reportTabs = [
1424
  ["summary", "Overview"],
1425
- ["reps", "Timeline"],
1426
  ["metrics", "Metrics"],
1427
  ["issues", "Issues"],
1428
- ["coach", "Coach Plan"],
1429
  ["artifacts", "Artifacts"],
1430
  ];
1431
 
1432
- function ReportPanel({ result, activeTab, onTabChange, videoSrc }) {
1433
  const content = {
1434
  summary: h(SummaryTab, { result }),
1435
  metrics: h(MetricsTab, { result }),
1436
- reps: h(RepsTab, { result, videoSrc }),
1437
  issues: h(IssuesTab, { result }),
1438
- coach: h(CoachTab, { result }),
1439
  artifacts: h(ArtifactsTab, { result }),
1440
  }[activeTab];
1441
 
@@ -1738,20 +1871,22 @@ function App() {
1738
  ? h("span", { className: "status-pill" }, result.run_id)
1739
  : null,
1740
  ),
1741
- h(
1742
- "div",
1743
- { className: "result-stage" },
1744
- result?.annotated_video_url
1745
- ? h("video", {
1746
- className: "result-video",
1747
- src: result.annotated_video_url,
1748
- controls: true,
1749
- })
1750
- : h(StageEmpty, null),
1751
- ),
1752
- result?.annotated_video_url
1753
- ? null
1754
- : h(ProgressPanel, { steps: progressSteps }),
 
 
1755
  h(ReviewInsights, { result }),
1756
  ),
1757
  ),
@@ -1759,7 +1894,6 @@ function App() {
1759
  result,
1760
  activeTab,
1761
  onTabChange: setActiveTab,
1762
- videoSrc: result?.annotated_video_url || previewUrl,
1763
  }),
1764
  );
1765
  }
 
503
  );
504
  }
505
 
506
+ function CoachOverview({ result }) {
507
+ if (!result) return null;
508
+ const summary = result.report.coach_summary;
509
+ const coachArtifacts = result.report.artifacts || {};
510
+ const coachSource = coachArtifacts.coach_summary_source || "";
511
+ const coachProvider = coachArtifacts.coach_summary_provider || "n/a";
512
+ const coachModel = coachArtifacts.coach_summary_model || "n/a";
513
+ const verifierBypassed = Boolean(
514
+ coachArtifacts.coach_summary_verifier_bypassed,
515
+ );
516
+ const isFallback = coachSource.startsWith("fallback");
517
+ const cueNow =
518
+ summary.top_fixes?.[0] ||
519
+ summary.next_session_plan?.[0] ||
520
+ summary.summary;
521
+
522
+ return h(
523
+ "section",
524
+ { className: "coach-spotlight" },
525
+ h(
526
+ "div",
527
+ { className: "coach-command" },
528
+ h(
529
+ "div",
530
+ { className: "coach-command-copy" },
531
+ h("span", null, "Coach intelligence"),
532
+ h("strong", null, cueNow),
533
+ h("p", null, summary.summary),
534
+ ),
535
+ h(
536
+ "div",
537
+ { className: "coach-meta-stack" },
538
+ metaChip(`Provider: ${coachProvider}`, "blue"),
539
+ metaChip(`Model: ${coachModel}`),
540
+ metaChip(`Source: ${coachSource || "n/a"}`, isFallback ? "watch" : "strong"),
541
+ ),
542
+ ),
543
+ isFallback || verifierBypassed
544
+ ? h(
545
+ "div",
546
+ { className: "coach-system-row" },
547
+ isFallback
548
+ ? h(
549
+ "p",
550
+ { className: "system-note" },
551
+ "A conservative fallback summary was used because the generated summary was unavailable or did not pass verification.",
552
+ )
553
+ : null,
554
+ verifierBypassed
555
+ ? h(
556
+ "p",
557
+ { className: "system-note warning" },
558
+ "Verifier bypass is active, so the model summary is shown even though verification did not pass.",
559
+ )
560
+ : null,
561
+ )
562
+ : null,
563
+ h(
564
+ "div",
565
+ { className: "coach-overview-grid" },
566
+ h(NoteList, { title: "Fix first", items: summary.top_fixes, className: "coach-note priority" }),
567
+ h(NoteList, { title: "Next session", items: summary.next_session_plan, className: "coach-note next" }),
568
+ h(NoteList, { title: "Keep", items: summary.what_looked_good, className: "coach-note" }),
569
+ h(NoteList, {
570
+ title: "Variation vs issue",
571
+ items: summary.valid_variation_vs_issue,
572
+ className: "coach-note",
573
+ }),
574
+ h(NoteList, {
575
+ title: "Confidence notes",
576
+ items: summary.confidence_notes,
577
+ className: "coach-note muted",
578
+ }),
579
+ ),
580
+ );
581
+ }
582
+
583
  function RepTimeline({
584
  report,
585
  compact = false,
 
765
  )
766
  : h("p", { className: "empty-copy" }, "No movement scores available."),
767
  ),
768
+ h(CoachOverview, { result }),
769
  h(
770
  "div",
771
  { className: "note-grid report-notes" },
 
888
  );
889
  }
890
 
891
+ function RepDetailPanel({ report, rep, result, playbackTime, compact = false }) {
892
  if (!rep) {
893
  return h(
894
  "aside",
895
+ { className: `rep-detail-panel${compact ? " compact" : ""}` },
896
  h("span", { className: "rep-detail-kicker" }, "Current rep"),
897
  h("strong", null, "No rep selected"),
898
  h("p", null, "Play the video or click a rep segment to inspect its metrics."),
 
939
  const issueText = repIssues.length
940
  ? `${repIssues.length} issue marker${repIssues.length === 1 ? "" : "s"} attached to this rep.`
941
  : "No issue marker is attached, so use the lowest metric as the review cue.";
942
+ const primaryIssue = repIssues[0] || null;
943
+
944
+ if (compact) {
945
+ return h(
946
+ "aside",
947
+ { className: `rep-detail-panel compact ${scoreTone(score)}` },
948
+ h(
949
+ "div",
950
+ { className: "rep-detail-head" },
951
+ h(
952
+ "div",
953
+ null,
954
+ h("span", { className: "rep-detail-kicker" }, "Current rep"),
955
+ h("strong", null, `Rep ${rep.rep_id}`),
956
+ h("small", null, timeRange(rep.start_sec, rep.end_sec)),
957
+ ),
958
+ metaChip(score === null ? "no score" : percent(score), scoreTone(score)),
959
+ ),
960
+ h(
961
+ "div",
962
+ { className: "rep-fact-grid compact" },
963
+ h(
964
+ "div",
965
+ { className: "rep-fact" },
966
+ h("span", null, "Duration"),
967
+ h("strong", null, `${duration.toFixed(2)}s`),
968
+ ),
969
+ h(
970
+ "div",
971
+ { className: "rep-fact" },
972
+ h("span", null, "Focus"),
973
+ h("strong", null, weakestMetric ? `${weakestMetric.name} ${percent(weakestMetric.value)}` : "n/a"),
974
+ ),
975
+ h(
976
+ "div",
977
+ { className: "rep-fact" },
978
+ h("span", null, "Frames"),
979
+ h("strong", null, `${rep.start_frame}-${rep.end_frame}`),
980
+ ),
981
+ h(
982
+ "div",
983
+ { className: "rep-fact" },
984
+ h("span", null, "Issues"),
985
+ h("strong", null, repIssues.length ? String(repIssues.length) : "clear"),
986
+ ),
987
+ ),
988
+ h(
989
+ "div",
990
+ { className: "rep-progress-card compact" },
991
+ h(
992
+ "div",
993
+ { className: "rep-now" },
994
+ h("span", null, "playhead"),
995
+ h("strong", null, `${playbackTime.toFixed(2)}s`),
996
+ ),
997
+ h(
998
+ "div",
999
+ { className: "rep-progress-track", "aria-hidden": "true" },
1000
+ h("span", {
1001
+ className: "rep-progress-fill",
1002
+ style: { width: `${Math.round(repProgress * 100)}%` },
1003
+ }),
1004
+ h("span", { className: "rep-progress-mid" }),
1005
+ ),
1006
+ h(
1007
+ "div",
1008
+ { className: "rep-progress-labels" },
1009
+ h("span", null, `${rep.start_sec.toFixed(2)}s`),
1010
+ h("span", null, `${rep.mid_sec.toFixed(2)}s mid`),
1011
+ h("span", null, `${rep.end_sec.toFixed(2)}s`),
1012
+ ),
1013
+ ),
1014
+ primaryIssue
1015
+ ? h(
1016
+ "article",
1017
+ { className: `rep-issue-brief ${severityLevel(primaryIssue)}` },
1018
+ h(
1019
+ "div",
1020
+ { className: "rep-issue-brief-head" },
1021
+ h(
1022
+ "div",
1023
+ null,
1024
+ h("span", null, "Issue detail"),
1025
+ h("strong", null, issueTitle(primaryIssue)),
1026
+ ),
1027
+ h(
1028
+ "span",
1029
+ { className: `severity-chip ${severityLevel(primaryIssue)}` },
1030
+ severityText(primaryIssue),
1031
+ ),
1032
+ ),
1033
+ h("p", null, issueEvidence(primaryIssue)),
1034
+ h(
1035
+ "div",
1036
+ { className: "rep-issue-meta" },
1037
+ h("span", null, issueFocus(primaryIssue)),
1038
+ h("span", null, issueClipText(result, primaryIssue, 0)),
1039
+ h("span", null, `confidence ${percent(primaryIssue.evidence?.confidence)}`),
1040
+ repIssues.length > 1
1041
+ ? h("span", null, `+${repIssues.length - 1} more`)
1042
+ : null,
1043
+ ),
1044
+ )
1045
+ : null,
1046
+ );
1047
+ }
1048
 
1049
  return h(
1050
  "aside",
 
1183
  );
1184
  }
1185
 
1186
+ function ReplayReviewPanel({ result, videoSrc, className = "" }) {
1187
  const [playbackTime, setPlaybackTime] = useState(0);
1188
  const [selectedRepId, setSelectedRepId] = useState(null);
1189
  const replayVideoRef = useRef(null);
 
1193
  setSelectedRepId(null);
1194
  }, [result?.run_id, videoSrc]);
1195
 
 
 
 
 
 
 
 
1196
  const report = result.report;
1197
  const reps = report.reps?.reps || [];
 
1198
  const timedRep = repAtTime(report, playbackTime);
1199
  const selectedRep = reps.find((rep) => rep.rep_id === selectedRepId) || null;
1200
  const activeRep = timedRep || selectedRep || reps[0] || null;
 
1210
  }
1211
 
1212
  return h(
1213
+ "div",
1214
+ { className: `replay-review${className ? ` ${className}` : ""}` },
 
 
 
 
 
 
1215
  h(
1216
  "div",
1217
+ { className: "replay-main" },
1218
  h(
1219
  "div",
1220
  { className: "replay-video-card" },
 
1243
  h("strong", null, "No replay video available"),
1244
  h("span", null, "Upload a clip or use a run with an annotated video to sync the timeline."),
1245
  ),
 
 
 
 
 
 
1246
  ),
1247
+ h(RepDetailPanel, {
1248
+ report,
1249
+ rep: activeRep,
1250
+ result,
1251
+ playbackTime,
1252
+ compact: true,
1253
+ }),
1254
+ ),
1255
+ h(
1256
+ "div",
1257
+ { className: "replay-timeline-card" },
1258
+ h(RepTimeline, {
1259
+ report,
1260
+ activeRepId: activeRep?.rep_id || null,
1261
+ playbackTime,
1262
+ onRepSelect: playFromRep,
1263
+ }),
1264
+ ),
1265
+ );
1266
+ }
1267
+
1268
+ function RepsTab({ result }) {
1269
+ if (!result)
1270
+ return h(
1271
+ "section",
1272
+ { className: "summary" },
1273
+ h("h2", null, "Rep list"),
1274
+ h("p", null, "Rep segments appear after analysis."),
1275
+ );
1276
+ const report = result.report;
1277
+ const reps = report.reps?.reps || [];
1278
+ const analysisById = repAnalysisById(report);
1279
+
1280
+ return h(
1281
+ "section",
1282
+ { className: "summary reps-panel" },
1283
+ h("h2", null, "All reps"),
1284
+ h(
1285
+ "p",
1286
+ null,
1287
+ "A compact pass over every detected rep, with timing, frames, movement score, and issue markers.",
1288
  ),
1289
  reps.length
1290
  ? h(
 
1514
  );
1515
  }
1516
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1517
  function JsonTab({ result }) {
1518
  return h(
1519
  "pre",
 
1557
 
1558
  const reportTabs = [
1559
  ["summary", "Overview"],
1560
+ ["reps", "Reps"],
1561
  ["metrics", "Metrics"],
1562
  ["issues", "Issues"],
 
1563
  ["artifacts", "Artifacts"],
1564
  ];
1565
 
1566
+ function ReportPanel({ result, activeTab, onTabChange }) {
1567
  const content = {
1568
  summary: h(SummaryTab, { result }),
1569
  metrics: h(MetricsTab, { result }),
1570
+ reps: h(RepsTab, { result }),
1571
  issues: h(IssuesTab, { result }),
 
1572
  artifacts: h(ArtifactsTab, { result }),
1573
  }[activeTab];
1574
 
 
1871
  ? h("span", { className: "status-pill" }, result.run_id)
1872
  : null,
1873
  ),
1874
+ result
1875
+ ? h(ReplayReviewPanel, {
1876
+ result,
1877
+ videoSrc: result?.annotated_video_url || previewUrl,
1878
+ className: "review-output-replay",
1879
+ })
1880
+ : h(
1881
+ React.Fragment,
1882
+ null,
1883
+ h(
1884
+ "div",
1885
+ { className: "result-stage" },
1886
+ h(StageEmpty, null),
1887
+ ),
1888
+ h(ProgressPanel, { steps: progressSteps }),
1889
+ ),
1890
  h(ReviewInsights, { result }),
1891
  ),
1892
  ),
 
1894
  result,
1895
  activeTab,
1896
  onTabChange: setActiveTab,
 
1897
  }),
1898
  );
1899
  }
web/index.html CHANGED
@@ -11,10 +11,10 @@
11
  href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap"
12
  rel="stylesheet"
13
  />
14
- <link rel="stylesheet" href="/static/styles.css?v=20260614-rep-detail-readout" />
15
  </head>
16
  <body>
17
  <div id="root"></div>
18
- <script type="module" src="/static/app.js?v=20260614-rep-detail-readout"></script>
19
  </body>
20
  </html>
 
11
  href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap"
12
  rel="stylesheet"
13
  />
14
+ <link rel="stylesheet" href="/static/styles.css?v=20260614-compact-rep-card-height" />
15
  </head>
16
  <body>
17
  <div id="root"></div>
18
+ <script type="module" src="/static/app.js?v=20260614-compact-rep-card-height"></script>
19
  </body>
20
  </html>
web/styles.css CHANGED
@@ -743,6 +743,7 @@ input[type="text"]:focus-visible,
743
  .overview-panel,
744
  .metrics-panel,
745
  .timeline-panel,
 
746
  .issue-board,
747
  .coach-panel {
748
  position: relative;
@@ -752,6 +753,7 @@ input[type="text"]:focus-visible,
752
  .overview-panel::before,
753
  .metrics-panel::before,
754
  .timeline-panel::before,
 
755
  .issue-board::before,
756
  .coach-panel::before {
757
  content: "";
@@ -767,10 +769,15 @@ input[type="text"]:focus-visible,
767
  .score-board,
768
  .rep-timeline,
769
  .report-notes,
 
770
  .metrics-panel > h2,
771
  .metrics-panel > p,
772
  .timeline-panel > h2,
773
  .timeline-panel > p,
 
 
 
 
774
  .issue-board > .timeline-head,
775
  .coach-panel > h2,
776
  .coach-panel > p,
@@ -1007,6 +1014,54 @@ input[type="text"]:focus-visible,
1007
  margin-top: 18px;
1008
  }
1009
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1010
  .replay-layout {
1011
  position: relative;
1012
  z-index: 1;
@@ -1018,6 +1073,7 @@ input[type="text"]:focus-visible,
1018
  }
1019
 
1020
  .replay-video-card,
 
1021
  .rep-detail-panel {
1022
  border: 1px solid rgba(245, 242, 232, 0.12);
1023
  border-radius: var(--radius);
@@ -1192,6 +1248,12 @@ input[type="text"]:focus-visible,
1192
  align-self: stretch;
1193
  }
1194
 
 
 
 
 
 
 
1195
  .rep-detail-panel.strong {
1196
  border-color: rgba(87, 217, 163, 0.26);
1197
  }
@@ -1232,6 +1294,10 @@ input[type="text"]:focus-visible,
1232
  line-height: 1;
1233
  }
1234
 
 
 
 
 
1235
  .rep-detail-head small {
1236
  display: block;
1237
  margin-top: 7px;
@@ -1245,6 +1311,10 @@ input[type="text"]:focus-visible,
1245
  gap: 8px;
1246
  }
1247
 
 
 
 
 
1248
  .rep-fact,
1249
  .rep-progress-card,
1250
  .rep-readout {
@@ -1260,6 +1330,11 @@ input[type="text"]:focus-visible,
1260
  padding: 11px;
1261
  }
1262
 
 
 
 
 
 
1263
  .rep-fact strong {
1264
  overflow-wrap: anywhere;
1265
  color: var(--paper);
@@ -1267,12 +1342,21 @@ input[type="text"]:focus-visible,
1267
  font-variant-numeric: tabular-nums;
1268
  }
1269
 
 
 
 
 
1270
  .rep-progress-card {
1271
  display: grid;
1272
  gap: 11px;
1273
  padding: 12px;
1274
  }
1275
 
 
 
 
 
 
1276
  .rep-now {
1277
  display: flex;
1278
  justify-content: space-between;
@@ -1319,6 +1403,10 @@ input[type="text"]:focus-visible,
1319
  font-variant-numeric: tabular-nums;
1320
  }
1321
 
 
 
 
 
1322
  .rep-readout {
1323
  display: grid;
1324
  gap: 7px;
@@ -1339,6 +1427,76 @@ input[type="text"]:focus-visible,
1339
  line-height: 1.38;
1340
  }
1341
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1342
  .rep-mini-metrics {
1343
  display: grid;
1344
  gap: 8px;
@@ -1838,6 +1996,85 @@ td {
1838
  min-height: 100%;
1839
  }
1840
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1841
  .coach-note.muted {
1842
  opacity: 0.86;
1843
  }
@@ -1955,6 +2192,7 @@ td {
1955
  .brief-grid,
1956
  .score-board,
1957
  .coach-plan-grid,
 
1958
  .rep-score-row,
1959
  .note-grid,
1960
  .stat-grid,
@@ -1966,7 +2204,10 @@ td {
1966
  }
1967
 
1968
  .report-hero,
 
1969
  .replay-layout,
 
 
1970
  .issue-card-feature {
1971
  grid-template-columns: 1fr;
1972
  }
@@ -2002,6 +2243,7 @@ td {
2002
  .brief-grid,
2003
  .score-board,
2004
  .coach-plan-grid,
 
2005
  .rep-score-row,
2006
  .note-grid,
2007
  .stat-grid,
@@ -2016,6 +2258,15 @@ td {
2016
  min-height: 0;
2017
  }
2018
 
 
 
 
 
 
 
 
 
 
2019
  .rep-track {
2020
  height: 118px;
2021
  }
@@ -2033,6 +2284,11 @@ td {
2033
  min-height: 240px;
2034
  }
2035
 
 
 
 
 
 
2036
  .hero-lab {
2037
  display: none;
2038
  }
 
743
  .overview-panel,
744
  .metrics-panel,
745
  .timeline-panel,
746
+ .reps-panel,
747
  .issue-board,
748
  .coach-panel {
749
  position: relative;
 
753
  .overview-panel::before,
754
  .metrics-panel::before,
755
  .timeline-panel::before,
756
+ .reps-panel::before,
757
  .issue-board::before,
758
  .coach-panel::before {
759
  content: "";
 
769
  .score-board,
770
  .rep-timeline,
771
  .report-notes,
772
+ .coach-spotlight,
773
  .metrics-panel > h2,
774
  .metrics-panel > p,
775
  .timeline-panel > h2,
776
  .timeline-panel > p,
777
+ .reps-panel > h2,
778
+ .reps-panel > p,
779
+ .reps-panel .rep-grid,
780
+ .reps-panel .table-wrap,
781
  .issue-board > .timeline-head,
782
  .coach-panel > h2,
783
  .coach-panel > p,
 
1014
  margin-top: 18px;
1015
  }
1016
 
1017
+ .replay-review {
1018
+ position: relative;
1019
+ z-index: 1;
1020
+ display: grid;
1021
+ gap: 12px;
1022
+ }
1023
+
1024
+ .replay-main {
1025
+ display: grid;
1026
+ grid-template-columns: minmax(0, 1fr) minmax(260px, 0.42fr);
1027
+ gap: 14px;
1028
+ align-items: start;
1029
+ }
1030
+
1031
+ .review-output-replay .replay-main {
1032
+ margin-top: 0;
1033
+ grid-template-columns: minmax(0, 1.22fr) minmax(260px, 0.52fr);
1034
+ }
1035
+
1036
+ .review-output-replay .timeline-video,
1037
+ .review-output-replay .timeline-video-placeholder {
1038
+ min-height: 280px;
1039
+ max-height: min(52vh, 520px);
1040
+ }
1041
+
1042
+ .review-output-replay .rep-detail-panel {
1043
+ max-height: min(52vh, 620px);
1044
+ overflow-y: auto;
1045
+ overscroll-behavior: contain;
1046
+ scrollbar-gutter: stable;
1047
+ }
1048
+
1049
+ .replay-timeline-card {
1050
+ border: 1px solid rgba(245, 242, 232, 0.12);
1051
+ border-radius: var(--radius);
1052
+ padding: 12px;
1053
+ background: rgba(9, 10, 8, 0.64);
1054
+ box-shadow: 0 18px 50px rgba(0, 0, 0, 0.2);
1055
+ }
1056
+
1057
+ .replay-timeline-card .rep-timeline {
1058
+ margin-top: 0;
1059
+ }
1060
+
1061
+ .review-output-replay .replay-timeline-card .rep-track.interactive {
1062
+ height: 112px;
1063
+ }
1064
+
1065
  .replay-layout {
1066
  position: relative;
1067
  z-index: 1;
 
1073
  }
1074
 
1075
  .replay-video-card,
1076
+ .replay-timeline-card,
1077
  .rep-detail-panel {
1078
  border: 1px solid rgba(245, 242, 232, 0.12);
1079
  border-radius: var(--radius);
 
1248
  align-self: stretch;
1249
  }
1250
 
1251
+ .rep-detail-panel.compact {
1252
+ gap: 10px;
1253
+ padding: 14px;
1254
+ align-self: start;
1255
+ }
1256
+
1257
  .rep-detail-panel.strong {
1258
  border-color: rgba(87, 217, 163, 0.26);
1259
  }
 
1294
  line-height: 1;
1295
  }
1296
 
1297
+ .rep-detail-panel.compact .rep-detail-head strong {
1298
+ font-size: 1.45rem;
1299
+ }
1300
+
1301
  .rep-detail-head small {
1302
  display: block;
1303
  margin-top: 7px;
 
1311
  gap: 8px;
1312
  }
1313
 
1314
+ .rep-fact-grid.compact {
1315
+ gap: 7px;
1316
+ }
1317
+
1318
  .rep-fact,
1319
  .rep-progress-card,
1320
  .rep-readout {
 
1330
  padding: 11px;
1331
  }
1332
 
1333
+ .rep-detail-panel.compact .rep-fact {
1334
+ gap: 5px;
1335
+ padding: 9px;
1336
+ }
1337
+
1338
  .rep-fact strong {
1339
  overflow-wrap: anywhere;
1340
  color: var(--paper);
 
1342
  font-variant-numeric: tabular-nums;
1343
  }
1344
 
1345
+ .rep-detail-panel.compact .rep-fact strong {
1346
+ font-size: 0.9rem;
1347
+ }
1348
+
1349
  .rep-progress-card {
1350
  display: grid;
1351
  gap: 11px;
1352
  padding: 12px;
1353
  }
1354
 
1355
+ .rep-progress-card.compact {
1356
+ gap: 8px;
1357
+ padding: 10px;
1358
+ }
1359
+
1360
  .rep-now {
1361
  display: flex;
1362
  justify-content: space-between;
 
1403
  font-variant-numeric: tabular-nums;
1404
  }
1405
 
1406
+ .rep-progress-card.compact .rep-progress-track {
1407
+ height: 8px;
1408
+ }
1409
+
1410
  .rep-readout {
1411
  display: grid;
1412
  gap: 7px;
 
1427
  line-height: 1.38;
1428
  }
1429
 
1430
+ .rep-issue-brief {
1431
+ display: grid;
1432
+ gap: 8px;
1433
+ border: 1px solid rgba(255, 129, 87, 0.28);
1434
+ border-radius: var(--radius);
1435
+ padding: 10px;
1436
+ background:
1437
+ linear-gradient(135deg, rgba(255, 129, 87, 0.12), transparent 60%),
1438
+ rgba(245, 242, 232, 0.055);
1439
+ }
1440
+
1441
+ .rep-issue-brief.high {
1442
+ border-color: rgba(255, 139, 116, 0.42);
1443
+ background:
1444
+ linear-gradient(135deg, rgba(255, 139, 116, 0.16), transparent 60%),
1445
+ rgba(245, 242, 232, 0.055);
1446
+ }
1447
+
1448
+ .rep-issue-brief.minor {
1449
+ border-color: rgba(95, 183, 255, 0.26);
1450
+ background:
1451
+ linear-gradient(135deg, rgba(95, 183, 255, 0.12), transparent 60%),
1452
+ rgba(245, 242, 232, 0.055);
1453
+ }
1454
+
1455
+ .rep-issue-brief-head {
1456
+ display: flex;
1457
+ justify-content: space-between;
1458
+ gap: 10px;
1459
+ align-items: flex-start;
1460
+ }
1461
+
1462
+ .rep-issue-brief span,
1463
+ .rep-issue-meta span {
1464
+ color: var(--muted);
1465
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
1466
+ font-size: 0.7rem;
1467
+ font-weight: 600;
1468
+ text-transform: uppercase;
1469
+ }
1470
+
1471
+ .rep-issue-brief strong {
1472
+ display: block;
1473
+ margin-top: 5px;
1474
+ color: var(--paper);
1475
+ font-size: 0.98rem;
1476
+ line-height: 1.2;
1477
+ }
1478
+
1479
+ .rep-issue-brief p {
1480
+ margin: 0;
1481
+ color: rgba(245, 242, 232, 0.84);
1482
+ font-size: 0.9rem;
1483
+ line-height: 1.36;
1484
+ }
1485
+
1486
+ .rep-issue-meta {
1487
+ display: flex;
1488
+ flex-wrap: wrap;
1489
+ gap: 6px;
1490
+ }
1491
+
1492
+ .rep-issue-meta span {
1493
+ border: 1px solid rgba(245, 242, 232, 0.1);
1494
+ border-radius: 999px;
1495
+ padding: 5px 7px;
1496
+ background: rgba(9, 10, 8, 0.36);
1497
+ text-transform: none;
1498
+ }
1499
+
1500
  .rep-mini-metrics {
1501
  display: grid;
1502
  gap: 8px;
 
1996
  min-height: 100%;
1997
  }
1998
 
1999
+ .coach-spotlight {
2000
+ display: grid;
2001
+ gap: 12px;
2002
+ border: 1px solid rgba(212, 255, 91, 0.24);
2003
+ border-radius: var(--radius);
2004
+ margin-top: 16px;
2005
+ padding: 14px;
2006
+ background:
2007
+ linear-gradient(120deg, rgba(212, 255, 91, 0.11), transparent 34%, rgba(95, 183, 255, 0.07)),
2008
+ rgba(9, 10, 8, 0.34);
2009
+ }
2010
+
2011
+ .coach-command {
2012
+ display: grid;
2013
+ grid-template-columns: minmax(0, 1fr) minmax(220px, 0.34fr);
2014
+ gap: 14px;
2015
+ align-items: start;
2016
+ }
2017
+
2018
+ .coach-command-copy {
2019
+ min-width: 0;
2020
+ }
2021
+
2022
+ .coach-command-copy span {
2023
+ color: var(--volt);
2024
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
2025
+ font-size: 0.78rem;
2026
+ font-weight: 700;
2027
+ text-transform: uppercase;
2028
+ }
2029
+
2030
+ .coach-command-copy strong {
2031
+ display: block;
2032
+ margin-top: 8px;
2033
+ color: var(--paper);
2034
+ font-size: clamp(1.35rem, 2.4vw, 2rem);
2035
+ line-height: 1.08;
2036
+ }
2037
+
2038
+ .coach-command-copy p {
2039
+ max-width: 920px;
2040
+ margin-top: 10px;
2041
+ color: rgba(245, 242, 232, 0.82);
2042
+ }
2043
+
2044
+ .coach-meta-stack {
2045
+ display: flex;
2046
+ flex-wrap: wrap;
2047
+ justify-content: flex-end;
2048
+ gap: 8px;
2049
+ }
2050
+
2051
+ .coach-meta-stack .meta-chip {
2052
+ overflow-wrap: anywhere;
2053
+ }
2054
+
2055
+ .coach-system-row {
2056
+ display: grid;
2057
+ gap: 8px;
2058
+ }
2059
+
2060
+ .coach-overview-grid {
2061
+ display: grid;
2062
+ grid-template-columns: repeat(4, minmax(0, 1fr));
2063
+ gap: 10px;
2064
+ }
2065
+
2066
+ .coach-overview-grid .note {
2067
+ min-height: 100%;
2068
+ background: rgba(9, 10, 8, 0.28);
2069
+ }
2070
+
2071
+ .coach-overview-grid .note.priority,
2072
+ .coach-overview-grid .note.next {
2073
+ grid-column: span 2;
2074
+ border-color: rgba(212, 255, 91, 0.26);
2075
+ background: rgba(212, 255, 91, 0.075);
2076
+ }
2077
+
2078
  .coach-note.muted {
2079
  opacity: 0.86;
2080
  }
 
2192
  .brief-grid,
2193
  .score-board,
2194
  .coach-plan-grid,
2195
+ .coach-overview-grid,
2196
  .rep-score-row,
2197
  .note-grid,
2198
  .stat-grid,
 
2204
  }
2205
 
2206
  .report-hero,
2207
+ .coach-command,
2208
  .replay-layout,
2209
+ .replay-main,
2210
+ .review-output-replay .replay-main,
2211
  .issue-card-feature {
2212
  grid-template-columns: 1fr;
2213
  }
 
2243
  .brief-grid,
2244
  .score-board,
2245
  .coach-plan-grid,
2246
+ .coach-overview-grid,
2247
  .rep-score-row,
2248
  .note-grid,
2249
  .stat-grid,
 
2258
  min-height: 0;
2259
  }
2260
 
2261
+ .coach-meta-stack {
2262
+ justify-content: flex-start;
2263
+ }
2264
+
2265
+ .coach-overview-grid .note.priority,
2266
+ .coach-overview-grid .note.next {
2267
+ grid-column: auto;
2268
+ }
2269
+
2270
  .rep-track {
2271
  height: 118px;
2272
  }
 
2284
  min-height: 240px;
2285
  }
2286
 
2287
+ .review-output-replay .rep-detail-panel {
2288
+ max-height: min(70vh, 620px);
2289
+ overflow-y: auto;
2290
+ }
2291
+
2292
  .hero-lab {
2293
  display: none;
2294
  }